package daecommon

import (
	"bytes"
	"fmt"
	"io"
	"isle/bootstrap"
	"isle/toolkit"
	"isle/yamlutil"
	"net"

	_ "embed"

	"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
	"github.com/sirupsen/logrus"
	"github.com/slackhq/nebula"
	"github.com/slackhq/nebula/cert"
	"github.com/slackhq/nebula/config"
	"gopkg.in/yaml.v3"
)

const (
	// Network ID used when translating from the old single-network daemon
	// config to the multi-network config.
	DeprecatedNetworkID = "_" // DEPRECATED
)

//go:embed daemon.yml
var defaultConfigB []byte

// ConfigFirewall defines all firewall configuration for a VPN.
type ConfigFirewall struct {
	Outbound []ConfigFirewallRule `yaml:"outbound"`
	Inbound  []ConfigFirewallRule `yaml:"inbound"`
}

// validate returns an error if the ConfigFirewall is invalid.
func (f ConfigFirewall) validate() error {
	// This method is implemented by actually constructing nebula's firewall
	// using its own internals.
	l := logrus.New()
	l.SetOutput(io.Discard)

	nebulaConfig := struct {
		Firewall ConfigFirewall `yaml:"firewall"`
	}{f}
	nebulaConfigB, err := yaml.Marshal(nebulaConfig)
	if err != nil {
		return fmt.Errorf("yaml marshaling firewall config: %w", err)
	}

	config := config.NewC(l)
	if err := config.LoadString(string(nebulaConfigB)); err != nil {
		return fmt.Errorf("loading firewall config into nebula config: %w", err)
	}

	// The NebulaCertificate is used internally only to pull out relevant IP
	// ranges, which the firewall doesn't use for instantiation/validation.
	nc := new(cert.NebulaCertificate)

	_, err = nebula.NewFirewallFromConfig(l, nc, config)
	return err
}

// ConfigFirewallRule represents a firewall rule which can allow either incoming
// or outgoing packets via the VPN.
type ConfigFirewallRule struct {
	Port   string   `yaml:"port,omitempty"`
	Code   string   `yaml:"code,omitempty"`
	Proto  string   `yaml:"proto,omitempty"`
	Host   string   `yaml:"host,omitempty"`
	Group  string   `yaml:"group,omitempty"`
	Groups []string `yaml:"groups,omitempty"`
	CIDR   string   `yaml:"cidr,omitempty"`
	CASha  string   `yaml:"ca_sha,omitempty"`
	CAName string   `yaml:"ca_name,omitempty"`
}

// ConfigStorageAllocation describes the structure of each storage allocation
// within the daemon config file.
type ConfigStorageAllocation struct {
	DataPath  string `yaml:"data_path"`
	MetaPath  string `yaml:"meta_path"`
	Capacity  int    `yaml:"capacity"`
	S3APIPort int    `yaml:"s3_api_port"`
	RPCPort   int    `yaml:"rpc_port"`
	AdminPort int    `yaml:"admin_port"`
}

// Annotate implements the mctx.Annotator interface.
func (csa ConfigStorageAllocation) Annotate(aa mctx.Annotations) {
	aa["allocDataPath"] = csa.DataPath
	aa["allocMetaPath"] = csa.MetaPath
	aa["allocCapacity"] = csa.Capacity
	aa["allocS3APIPort"] = csa.S3APIPort
	aa["allocRPCPort"] = csa.RPCPort
	aa["allocAdminPort"] = csa.AdminPort
}

// NetworkConfig describes the configuration of a single network.
type NetworkConfig struct {
	DNS struct {
		Resolvers []string `yaml:"resolvers"`
	} `yaml:"dns"`
	VPN struct {
		PublicAddr string         `yaml:"public_addr"`
		Firewall   ConfigFirewall `yaml:"firewall"`
	} `yaml:"vpn"`
	Storage struct {
		Allocations []ConfigStorageAllocation `yaml:"allocations"`
	} `yaml:"storage"`
}

// NewNetworkConfig returns a new NetworkConfig populated with its default
// values. If a callback is given the NetworkConfig will be passed to it for
// modification prior to having defaults populated.
func NewNetworkConfig(fn func(*NetworkConfig)) NetworkConfig {
	var c NetworkConfig
	if fn != nil {
		fn(&c)
	}
	c.fillDefaults()
	return c
}

func (c *NetworkConfig) fillDefaults() {
	if c.DNS.Resolvers == nil {
		c.DNS.Resolvers = []string{
			"1.1.1.1",
			"8.8.8.8",
		}
	}

	if c.VPN.Firewall.Outbound == nil {
		c.VPN.Firewall.Outbound = []ConfigFirewallRule{
			{
				Port:  "any",
				Proto: "any",
				Host:  "any",
			},
		}
	}

	if c.VPN.Firewall.Inbound == nil {
		c.VPN.Firewall.Inbound = []ConfigFirewallRule{
			{
				Port:  "any",
				Proto: "icmp",
				Host:  "any",
			},
		}
	}

	nextRPCPort := 3900

	for i := range c.Storage.Allocations {
		if c.Storage.Allocations[i].RPCPort == 0 {
			c.Storage.Allocations[i].RPCPort = nextRPCPort
		}

		nextRPCPort += 10

		if c.Storage.Allocations[i].S3APIPort == 0 {
			c.Storage.Allocations[i].S3APIPort = c.Storage.Allocations[i].RPCPort + 1
		}

		if c.Storage.Allocations[i].AdminPort == 0 {
			c.Storage.Allocations[i].AdminPort = c.Storage.Allocations[i].RPCPort + 2
		}
	}
}

// Validate asserts that the NetworkConfig is valid as far as can be determined
// without further context.
func (c NetworkConfig) Validate() error {
	if err := c.VPN.Firewall.validate(); err != nil {
		return fmt.Errorf("invalid VPN firewall config: %w", err)
	}

	return nil
}

// UnmarshalYAML implements the yaml.Unmarshaler interface. It will attempt to
// fill in default values where it can.
func (c *NetworkConfig) UnmarshalYAML(n *yaml.Node) error {
	type wrap NetworkConfig
	if err := n.Decode((*wrap)(c)); err != nil {
		return fmt.Errorf("decoding into %T: %w", c, err)
	}

	c.fillDefaults()
	return nil
}

// Config describes the structure of the daemon config file.
type Config struct {
	Networks map[string]NetworkConfig `yaml:"networks"`
}

// Validate asserts that the Config has no internal inconsistencies which would
// render it unusable.
func (c Config) Validate() error {
	nebulaPorts := map[string]string{}

	for id, network := range c.Networks {
		if network.VPN.PublicAddr == "" {
			continue
		}

		_, port, err := net.SplitHostPort(network.VPN.PublicAddr)
		if err != nil {
			return fmt.Errorf(
				"invalid vpn.public_addr %q: %w", network.VPN.PublicAddr, err,
			)
		} else if otherID, ok := nebulaPorts[port]; ok {
			return fmt.Errorf(
				"two networks with the same vpn.public_addr: %q and %q",
				id,
				otherID,
			)
		}

		nebulaPorts[port] = id

		if err := network.Validate(); err != nil {
			return fmt.Errorf("invalid config for network %q: %w", id, err)
		}
	}

	return nil
}

// CopyDefaultConfig copies the daemon config file embedded in the AppDir into
// the given io.Writer.
func CopyDefaultConfig(into io.Writer) error {
	_, err := io.Copy(into, bytes.NewReader(defaultConfigB))
	return err
}

// UnmarshalYAML implements the yaml.Unmarshaler interface. It will attempt to
// fill in default values where it can.
func (c *Config) UnmarshalYAML(n *yaml.Node) error {
	{ // DEPRECATED
		// We decode into a wrapped NetworkConfig, so that its UnmarshalYAML
		// doesn't get invoked. This prevents fillDefaults from getting
		// automatically called, so the IsZero check will work as intended. This
		// means we need to call fillDefaults manually though.
		type wrap NetworkConfig
		var networkConfig NetworkConfig
		_ = n.Decode((*wrap)(&networkConfig))
		if !toolkit.IsZero(networkConfig) {
			networkConfig.fillDefaults()
			*c = Config{
				Networks: map[string]NetworkConfig{
					DeprecatedNetworkID: networkConfig,
				},
			}
			return c.Validate()
		}
	}

	type wrap Config
	if err := n.Decode((*wrap)(c)); err != nil {
		return fmt.Errorf("yaml unmarshaling back into Config struct: %w", err)
	}

	return c.Validate()
}

// LoadConfig loads the daemon config from userConfigPath.
//
// If userConfigPath is not given then the default is loaded and returned.
func LoadConfig(userConfigPath string) (Config, error) {
	if userConfigPath == "" {
		return Config{}, nil
	}

	var config Config
	err := yamlutil.LoadYamlFile(&config, userConfigPath)
	return config, err
}

// BootstrapGarageHostForAlloc returns the bootstrap.GarageHostInstance which
// corresponds with the given alloc from the daemon config. This will panic if
// no associated instance can be found.
func BootstrapGarageHostForAlloc(
	host bootstrap.Host, alloc ConfigStorageAllocation,
) bootstrap.GarageHostInstance {
	for _, inst := range host.Garage.Instances {
		if inst.RPCPort == alloc.RPCPort {
			return inst
		}
	}

	panic(fmt.Sprintf("could not find alloc %+v in the bootstrap data", alloc))
}