// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cli

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"

	"github.com/peterbourgon/ff/v3/ffcli"
	"tailscale.com/ipn"
	"tailscale.com/util/mak"
)

var serveCmd = newServeCommand(&serveEnv{})

// newServeCommand returns a new "serve" subcommand using e as its environmment.
func newServeCommand(e *serveEnv) *ffcli.Command {
	return &ffcli.Command{
		Name:       "serve",
		ShortHelp:  "TODO",
		ShortUsage: "serve {show-config|https|tcp|ingress} <args>",
		LongHelp:   "", // TODO
		Exec:       e.runServe,
		FlagSet:    e.newFlags("serve", func(fs *flag.FlagSet) {}),
		Subcommands: []*ffcli.Command{
			{
				Name:      "show-config",
				Exec:      e.runServeShowConfig,
				ShortHelp: "show current serve config",
			},
			{
				Name:      "tcp",
				Exec:      e.runServeTCP,
				ShortHelp: "add or remove a TCP port forward",
				FlagSet: e.newFlags("serve-tcp", func(fs *flag.FlagSet) {
					fs.BoolVar(&e.terminateTLS, "terminate-tls", false, "terminate TLS before forwarding TCP connection")
				}),
			},
			{
				Name:      "ingress",
				Exec:      e.runServeIngress,
				ShortHelp: "enable or disable ingress",
				FlagSet:   e.newFlags("serve-ingress", func(fs *flag.FlagSet) {}),
			},
		},
	}
}

// serveEnv is the environment the serve command runs within. All I/O should be
// done via serveEnv methods so that it can be faked out for tests.
//
// It also contains the flags, as registered with newServeCommand.
type serveEnv struct {
	// flags
	terminateTLS bool

	// optional stuff for tests:
	testFlagOut        io.Writer
	testGetServeConfig func(context.Context) (*ipn.ServeConfig, error)
	testSetServeConfig func(context.Context, *ipn.ServeConfig) error
	testStdout         io.Writer
}

func (e *serveEnv) newFlags(name string, setup func(fs *flag.FlagSet)) *flag.FlagSet {
	onError, out := flag.ExitOnError, Stderr
	if e.testFlagOut != nil {
		onError, out = flag.ContinueOnError, e.testFlagOut
	}
	fs := flag.NewFlagSet(name, onError)
	fs.SetOutput(out)
	if setup != nil {
		setup(fs)
	}
	return fs
}

func (e *serveEnv) getServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
	if e.testGetServeConfig != nil {
		return e.testGetServeConfig(ctx)
	}
	return localClient.GetServeConfig(ctx)
}

func (e *serveEnv) setServeConfig(ctx context.Context, c *ipn.ServeConfig) error {
	if e.testSetServeConfig != nil {
		return e.testSetServeConfig(ctx, c)
	}
	return localClient.SetServeConfig(ctx, c)
}

func (e *serveEnv) stdout() io.Writer {
	if e.testStdout != nil {
		return e.testStdout
	}
	return os.Stdout
}

func (e *serveEnv) runServe(ctx context.Context, args []string) error {
	// Undocumented debug command (not using ffcli subcommands) to set raw
	// configs from stdin for now (2022-11-13).
	if len(args) == 1 && args[0] == "set-raw" {
		valb, err := io.ReadAll(os.Stdin)
		if err != nil {
			return err
		}
		sc := new(ipn.ServeConfig)
		if err := json.Unmarshal(valb, sc); err != nil {
			return fmt.Errorf("invalid JSON: %w", err)
		}
		return localClient.SetServeConfig(ctx, sc)
	}
	panic("TODO")
}

func (e *serveEnv) runServeShowConfig(ctx context.Context, args []string) error {
	sc, err := e.getServeConfig(ctx)
	if err != nil {
		return err
	}
	j, err := json.MarshalIndent(sc, "", "  ")
	if err != nil {
		return err
	}
	j = append(j, '\n')
	e.stdout().Write(j)
	return nil
}

func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
	panic("TODO")
}

func (e *serveEnv) runServeIngress(ctx context.Context, args []string) error {
	if len(args) != 1 {
		return flag.ErrHelp
	}
	var on bool
	switch args[0] {
	case "on", "off":
		on = args[0] == "on"
	default:
		return flag.ErrHelp
	}
	sc, err := e.getServeConfig(ctx)
	if err != nil {
		return err
	}
	var key ipn.HostPort = "foo:123" // TODO(bradfitz,shayne): fix
	if on && sc != nil && sc.AllowIngress[key] ||
		!on && (sc == nil || !sc.AllowIngress[key]) {
		// Nothing to do.
		return nil
	}
	if sc == nil {
		sc = &ipn.ServeConfig{}
	}
	if on {
		mak.Set(&sc.AllowIngress, "foo:123", true)
	} else {
		delete(sc.AllowIngress, "foo:123")
	}
	return e.setServeConfig(ctx, sc)
}
