package internal

import (
	"context"
	"fmt"
	"net/url"
	"os"

	log "github.com/sirupsen/logrus"
	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/netbirdio/netbird/client/ssh"
	"github.com/netbirdio/netbird/iface"
	mgm "github.com/netbirdio/netbird/management/client"
	"github.com/netbirdio/netbird/util"
)

const (
	// managementLegacyPortString is the port that was used before by the Management gRPC server.
	// It is used for backward compatibility now.
	// NB: hardcoded from github.com/netbirdio/netbird/management/cmd to avoid import
	managementLegacyPortString = "33073"
	// DefaultManagementURL points to the NetBird's cloud management endpoint
	DefaultManagementURL = "https://api.netbird.io:443"
	// oldDefaultManagementURL points to the NetBird's old cloud management endpoint
	oldDefaultManagementURL = "https://api.wiretrustee.com:443"
	// DefaultAdminURL points to NetBird's cloud management console
	DefaultAdminURL = "https://app.netbird.io:443"
)

var defaultInterfaceBlacklist = []string{
	iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
	"Tailscale", "tailscale", "docker", "veth", "br-", "lo",
}

// ConfigInput carries configuration changes to the client
type ConfigInput struct {
	ManagementURL       string
	AdminURL            string
	ConfigPath          string
	PreSharedKey        *string
	ServerSSHAllowed    *bool
	NATExternalIPs      []string
	CustomDNSAddress    []byte
	RosenpassEnabled    *bool
	RosenpassPermissive *bool
	InterfaceName       *string
	WireguardPort       *int
	DisableAutoConnect  *bool
	ExtraIFaceBlackList []string
}

// Config Configuration type
type Config struct {
	// Wireguard private key of local peer
	PrivateKey           string
	PreSharedKey         string
	ManagementURL        *url.URL
	AdminURL             *url.URL
	WgIface              string
	WgPort               int
	IFaceBlackList       []string
	DisableIPv6Discovery bool
	RosenpassEnabled     bool
	RosenpassPermissive  bool
	ServerSSHAllowed     *bool
	// SSHKey is a private SSH key in a PEM format
	SSHKey string

	// ExternalIP mappings, if different from the host interface IP
	//
	//   External IP must not be behind a CGNAT and port-forwarding for incoming UDP packets from WgPort on ExternalIP
	//   to WgPort on host interface IP must be present. This can take form of single port-forwarding rule, 1:1 DNAT
	//   mapping ExternalIP to host interface IP, or a NAT DMZ to host interface IP.
	//
	//   A single mapping will take the form of: external[/internal]
	//    external (required): either the external IP address or "stun" to use STUN to determine the external IP address
	//    internal (optional): either the internal/interface IP address or an interface name
	//
	//   examples:
	//      "12.34.56.78"          => all interfaces IPs will be mapped to external IP of 12.34.56.78
	//      "12.34.56.78/eth0"     => IPv4 assigned to interface eth0 will be mapped to external IP of 12.34.56.78
	//      "12.34.56.78/10.1.2.3" => interface IP 10.1.2.3 will be mapped to external IP of 12.34.56.78

	NATExternalIPs []string
	// CustomDNSAddress sets the DNS resolver listening address in format ip:port
	CustomDNSAddress string

	// DisableAutoConnect determines whether the client should not start with the service
	// it's set to false by default due to backwards compatibility
	DisableAutoConnect bool
}

// ReadConfig read config file and return with Config. If it is not exists create a new with default values
func ReadConfig(configPath string) (*Config, error) {
	if configFileIsExists(configPath) {
		config := &Config{}
		if _, err := util.ReadJson(configPath, config); err != nil {
			return nil, err
		}

		return config, nil
	}

	cfg, err := createNewConfig(ConfigInput{ConfigPath: configPath})
	if err != nil {
		return nil, err
	}

	err = WriteOutConfig(configPath, cfg)
	return cfg, err
}

// UpdateConfig update existing configuration according to input configuration and return with the configuration
func UpdateConfig(input ConfigInput) (*Config, error) {
	if !configFileIsExists(input.ConfigPath) {
		return nil, status.Errorf(codes.NotFound, "config file doesn't exist")
	}

	return update(input)
}

// UpdateOrCreateConfig reads existing config or generates a new one
func UpdateOrCreateConfig(input ConfigInput) (*Config, error) {
	if !configFileIsExists(input.ConfigPath) {
		log.Infof("generating new config %s", input.ConfigPath)
		cfg, err := createNewConfig(input)
		if err != nil {
			return nil, err
		}
		err = WriteOutConfig(input.ConfigPath, cfg)
		return cfg, err
	}

	if isPreSharedKeyHidden(input.PreSharedKey) {
		input.PreSharedKey = nil
	}
	return update(input)
}

// CreateInMemoryConfig generate a new config but do not write out it to the store
func CreateInMemoryConfig(input ConfigInput) (*Config, error) {
	return createNewConfig(input)
}

// WriteOutConfig write put the prepared config to the given path
func WriteOutConfig(path string, config *Config) error {
	return util.WriteJson(path, config)
}

// createNewConfig creates a new config generating a new Wireguard key and saving to file
func createNewConfig(input ConfigInput) (*Config, error) {
	wgKey := generateKey()
	pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
	if err != nil {
		return nil, err
	}

	config := &Config{
		SSHKey:               string(pem),
		PrivateKey:           wgKey,
		IFaceBlackList:       []string{},
		DisableIPv6Discovery: false,
		NATExternalIPs:       input.NATExternalIPs,
		CustomDNSAddress:     string(input.CustomDNSAddress),
		ServerSSHAllowed:     util.False(),
		DisableAutoConnect:   false,
	}

	defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
	if err != nil {
		return nil, err
	}

	config.ManagementURL = defaultManagementURL
	if input.ManagementURL != "" {
		URL, err := parseURL("Management URL", input.ManagementURL)
		if err != nil {
			return nil, err
		}
		config.ManagementURL = URL
	}

	config.WgPort = iface.DefaultWgPort
	if input.WireguardPort != nil {
		config.WgPort = *input.WireguardPort
	}

	config.WgIface = iface.WgInterfaceDefault
	if input.InterfaceName != nil {
		config.WgIface = *input.InterfaceName
	}

	if input.PreSharedKey != nil {
		config.PreSharedKey = *input.PreSharedKey
	}

	if input.RosenpassEnabled != nil {
		config.RosenpassEnabled = *input.RosenpassEnabled
	}

	if input.RosenpassPermissive != nil {
		config.RosenpassPermissive = *input.RosenpassPermissive
	}

	if input.ServerSSHAllowed != nil {
		config.ServerSSHAllowed = input.ServerSSHAllowed
	}

	defaultAdminURL, err := parseURL("Admin URL", DefaultAdminURL)
	if err != nil {
		return nil, err
	}

	config.AdminURL = defaultAdminURL
	if input.AdminURL != "" {
		newURL, err := parseURL("Admin Panel URL", input.AdminURL)
		if err != nil {
			return nil, err
		}
		config.AdminURL = newURL
	}

	// nolint:gocritic
	config.IFaceBlackList = append(defaultInterfaceBlacklist, input.ExtraIFaceBlackList...)
	return config, nil
}

func update(input ConfigInput) (*Config, error) {
	config := &Config{}

	if _, err := util.ReadJson(input.ConfigPath, config); err != nil {
		return nil, err
	}

	refresh := false

	if input.ManagementURL != "" && config.ManagementURL.String() != input.ManagementURL {
		log.Infof("new Management URL provided, updated to %s (old value %s)",
			input.ManagementURL, config.ManagementURL)
		newURL, err := parseURL("Management URL", input.ManagementURL)
		if err != nil {
			return nil, err
		}
		config.ManagementURL = newURL
		refresh = true
	}

	if input.AdminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != input.AdminURL) {
		log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
			input.AdminURL, config.AdminURL)
		newURL, err := parseURL("Admin Panel URL", input.AdminURL)
		if err != nil {
			return nil, err
		}
		config.AdminURL = newURL
		refresh = true
	}

	if input.PreSharedKey != nil && config.PreSharedKey != *input.PreSharedKey {
		log.Infof("new pre-shared key provided, replacing old key")
		config.PreSharedKey = *input.PreSharedKey
		refresh = true
	}

	if config.SSHKey == "" {
		pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
		if err != nil {
			return nil, err
		}
		config.SSHKey = string(pem)
		refresh = true
	}

	if config.WgPort == 0 {
		config.WgPort = iface.DefaultWgPort
		refresh = true
	}

	if input.WireguardPort != nil {
		config.WgPort = *input.WireguardPort
		refresh = true
	}

	if input.InterfaceName != nil {
		config.WgIface = *input.InterfaceName
		refresh = true
	}

	if input.NATExternalIPs != nil && len(config.NATExternalIPs) != len(input.NATExternalIPs) {
		config.NATExternalIPs = input.NATExternalIPs
		refresh = true
	}

	if input.CustomDNSAddress != nil {
		config.CustomDNSAddress = string(input.CustomDNSAddress)
		refresh = true
	}

	if input.RosenpassEnabled != nil {
		config.RosenpassEnabled = *input.RosenpassEnabled
		refresh = true
	}

	if input.RosenpassPermissive != nil {
		config.RosenpassPermissive = *input.RosenpassPermissive
		refresh = true
	}

	if input.DisableAutoConnect != nil {
		config.DisableAutoConnect = *input.DisableAutoConnect
		refresh = true
	}

	if input.ServerSSHAllowed != nil {
		config.ServerSSHAllowed = input.ServerSSHAllowed
		refresh = true
	}

	if config.ServerSSHAllowed == nil {
		config.ServerSSHAllowed = util.True()
		refresh = true
	}

	if len(input.ExtraIFaceBlackList) > 0 {
		for _, iFace := range util.SliceDiff(input.ExtraIFaceBlackList, config.IFaceBlackList) {
			config.IFaceBlackList = append(config.IFaceBlackList, iFace)
			refresh = true
		}
	}

	if refresh {
		// since we have new management URL, we need to update config file
		if err := util.WriteJson(input.ConfigPath, config); err != nil {
			return nil, err
		}
	}

	return config, nil
}

// parseURL parses and validates a service URL
func parseURL(serviceName, serviceURL string) (*url.URL, error) {
	parsedMgmtURL, err := url.ParseRequestURI(serviceURL)
	if err != nil {
		log.Errorf("failed parsing %s URL %s: [%s]", serviceName, serviceURL, err.Error())
		return nil, err
	}

	if parsedMgmtURL.Scheme != "https" && parsedMgmtURL.Scheme != "http" {
		return nil, fmt.Errorf(
			"invalid %s URL provided %s. Supported format [http|https]://[host]:[port]",
			serviceName, serviceURL)
	}

	if parsedMgmtURL.Port() == "" {
		switch parsedMgmtURL.Scheme {
		case "https":
			parsedMgmtURL.Host += ":443"
		case "http":
			parsedMgmtURL.Host += ":80"
		default:
			log.Infof("unable to determine a default port for schema %s in URL %s", parsedMgmtURL.Scheme, serviceURL)
		}
	}

	return parsedMgmtURL, err
}

// generateKey generates a new Wireguard private key
func generateKey() string {
	key, err := wgtypes.GeneratePrivateKey()
	if err != nil {
		panic(err)
	}
	return key.String()
}

// don't overwrite pre-shared key if we receive asterisks from UI
func isPreSharedKeyHidden(preSharedKey *string) bool {
	if preSharedKey != nil && *preSharedKey == "**********" {
		return true
	}
	return false
}

func configFileIsExists(path string) bool {
	_, err := os.Stat(path)
	return !os.IsNotExist(err)
}

// UpdateOldManagementURL checks whether client can switch to the new Management URL with port 443 and the management domain.
// If it can switch, then it updates the config and returns a new one. Otherwise, it returns the provided config.
// The check is performed only for the NetBird's managed version.
func UpdateOldManagementURL(ctx context.Context, config *Config, configPath string) (*Config, error) {
	defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
	if err != nil {
		return nil, err
	}

	parsedOldDefaultManagementURL, err := parseURL("Management URL", oldDefaultManagementURL)
	if err != nil {
		return nil, err
	}

	if config.ManagementURL.Hostname() != defaultManagementURL.Hostname() &&
		config.ManagementURL.Hostname() != parsedOldDefaultManagementURL.Hostname() {
		// only do the check for the NetBird's managed version
		return config, nil
	}

	var mgmTlsEnabled bool
	if config.ManagementURL.Scheme == "https" {
		mgmTlsEnabled = true
	}

	if !mgmTlsEnabled {
		// only do the check for HTTPs scheme (the hosted version of the Management service is always HTTPs)
		return config, nil
	}

	if config.ManagementURL.Port() != managementLegacyPortString &&
		config.ManagementURL.Hostname() == defaultManagementURL.Hostname() {
		return config, nil
	}

	newURL, err := parseURL("Management URL", fmt.Sprintf("%s://%s:%d",
		config.ManagementURL.Scheme, defaultManagementURL.Hostname(), 443))
	if err != nil {
		return nil, err
	}
	// here we check whether we could switch from the legacy 33073 port to the new 443
	log.Infof("attempting to switch from the legacy Management URL %s to the new one %s",
		config.ManagementURL.String(), newURL.String())
	key, err := wgtypes.ParseKey(config.PrivateKey)
	if err != nil {
		log.Infof("couldn't switch to the new Management %s", newURL.String())
		return config, err
	}

	client, err := mgm.NewClient(ctx, newURL.Host, key, mgmTlsEnabled)
	if err != nil {
		log.Infof("couldn't switch to the new Management %s", newURL.String())
		return config, err
	}
	defer func() {
		err = client.Close()
		if err != nil {
			log.Warnf("failed to close the Management service client %v", err)
		}
	}()

	// gRPC check
	_, err = client.GetServerPublicKey()
	if err != nil {
		log.Infof("couldn't switch to the new Management %s", newURL.String())
		return nil, err
	}

	// everything is alright => update the config
	newConfig, err := UpdateConfig(ConfigInput{
		ManagementURL: newURL.String(),
		ConfigPath:    configPath,
	})
	if err != nil {
		log.Infof("couldn't switch to the new Management %s", newURL.String())
		return config, fmt.Errorf("failed updating config file: %v", err)
	}
	log.Infof("successfully switched to the new Management URL: %s", newURL.String())

	return newConfig, nil
}
