package cql

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"fmt"
	"math/big"
	"net"
	"os"
	"strings"
	"time"
)

// TLSConfig configures TLS for the CQL listener.
type TLSConfig struct {
	Mode         string
	CertFile     string
	KeyFile      string
	ClientCAFile string
	ClientAuth   string
	MinVersion   string
	MaxVersion   string
}

const (
	tlsModeDisabled = "disabled"
	tlsModeManual   = "manual"
	tlsModeAuto     = "auto"
)

// BuildTLSConfig builds a *tls.Config or returns nil when TLS is disabled.
func (c TLSConfig) BuildTLSConfig() (*tls.Config, error) {
	return c.buildTLSConfig("")
}

func (c TLSConfig) buildTLSConfig(hostname string) (*tls.Config, error) {
	mode, err := c.resolveMode()
	if err != nil {
		return nil, err
	}
	if mode == tlsModeDisabled {
		return nil, nil
	}

	var cert tls.Certificate
	switch mode {
	case tlsModeAuto:
		if hostname == "" {
			hostname, err = os.Hostname()
			if err != nil {
				return nil, fmt.Errorf("auto-tls hostname lookup failed: %w", err)
			}
		}
		hostname = strings.TrimSpace(hostname)
		if hostname == "" {
			return nil, fmt.Errorf("auto-tls hostname is empty")
		}
		cert, err = generateSelfSignedCertificate(hostname)
		if err != nil {
			return nil, fmt.Errorf("generate auto-tls certificate: %w", err)
		}
	case tlsModeManual:
		if c.CertFile == "" || c.KeyFile == "" {
			return nil, fmt.Errorf("tls cert_file and key_file must be set")
		}
		cert, err = tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
		if err != nil {
			return nil, fmt.Errorf("load tls cert/key: %w", err)
		}
	default:
		return nil, fmt.Errorf("unsupported tls mode %q", mode)
	}

	config := &tls.Config{
		Certificates: []tls.Certificate{cert},
		MinVersion:   tls.VersionTLS12,
	}

	if c.MinVersion != "" {
		minVersion, err := parseTLSVersion(c.MinVersion)
		if err != nil {
			return nil, err
		}
		config.MinVersion = minVersion
	}
	if c.MaxVersion != "" {
		maxVersion, err := parseTLSVersion(c.MaxVersion)
		if err != nil {
			return nil, err
		}
		config.MaxVersion = maxVersion
	}
	if config.MaxVersion != 0 && config.MaxVersion < config.MinVersion {
		return nil, fmt.Errorf("tls max_version %q is less than min_version %q", c.MaxVersion, c.MinVersion)
	}

	if err := applyClientAuth(c, config); err != nil {
		return nil, err
	}

	return config, nil
}

func (c TLSConfig) resolveMode() (string, error) {
	if strings.TrimSpace(c.Mode) != "" {
		mode := strings.ToLower(strings.TrimSpace(c.Mode))
		switch mode {
		case tlsModeDisabled, "off", "false":
			return tlsModeDisabled, nil
		case tlsModeManual, "on", "enabled", "true":
			return tlsModeManual, nil
		case tlsModeAuto, "self_signed", "self-signed":
			return tlsModeAuto, nil
		default:
			return "", fmt.Errorf("unsupported tls mode %q", c.Mode)
		}
	}

	if c.CertFile != "" || c.KeyFile != "" || c.ClientCAFile != "" || c.ClientAuth != "" || c.MinVersion != "" || c.MaxVersion != "" {
		return "", fmt.Errorf("tls.mode must be set to enable TLS")
	}
	return tlsModeDisabled, nil
}

func applyClientAuth(cfg TLSConfig, config *tls.Config) error {
	clientAuth, err := parseClientAuth(cfg.ClientAuth)
	if err != nil {
		return err
	}

	if cfg.ClientCAFile != "" {
		caBytes, err := os.ReadFile(cfg.ClientCAFile)
		if err != nil {
			return fmt.Errorf("read tls client ca file: %w", err)
		}
		pool := x509.NewCertPool()
		if !pool.AppendCertsFromPEM(caBytes) {
			return fmt.Errorf("failed to parse tls client ca file %q", cfg.ClientCAFile)
		}
		config.ClientCAs = pool
		if clientAuth == tls.NoClientCert {
			clientAuth = tls.VerifyClientCertIfGiven
		}
	} else if requiresClientCA(clientAuth) {
		return fmt.Errorf("client_ca_file is required when client_auth=%q", cfg.ClientAuth)
	}

	config.ClientAuth = clientAuth
	return nil
}

func generateSelfSignedCertificate(hostname string) (tls.Certificate, error) {
	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return tls.Certificate{}, fmt.Errorf("generate private key: %w", err)
	}

	serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
	if err != nil {
		return tls.Certificate{}, fmt.Errorf("generate serial: %w", err)
	}

	now := time.Now()
	template := x509.Certificate{
		SerialNumber: serial,
		Subject: pkix.Name{
			CommonName: hostname,
		},
		NotBefore:   now.Add(-time.Hour),
		NotAfter:    now.Add(365 * 24 * time.Hour),
		KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		DNSNames:    []string{hostname},
	}

	if ip := net.ParseIP(hostname); ip != nil {
		template.IPAddresses = []net.IP{ip}
	}

	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
	if err != nil {
		return tls.Certificate{}, fmt.Errorf("create certificate: %w", err)
	}

	cert := tls.Certificate{
		Certificate: [][]byte{derBytes},
		PrivateKey:  privateKey,
	}
	leaf, err := x509.ParseCertificate(derBytes)
	if err == nil {
		cert.Leaf = leaf
	}
	return cert, nil
}

func parseTLSVersion(value string) (uint16, error) {
	clean := strings.ToLower(strings.TrimSpace(value))
	clean = strings.TrimPrefix(clean, "tls")
	clean = strings.TrimPrefix(clean, "v")
	switch clean {
	case "1.0", "1":
		return tls.VersionTLS10, nil
	case "1.1":
		return tls.VersionTLS11, nil
	case "1.2":
		return tls.VersionTLS12, nil
	case "1.3":
		return tls.VersionTLS13, nil
	default:
		return 0, fmt.Errorf("unsupported tls version %q", value)
	}
}

func parseClientAuth(value string) (tls.ClientAuthType, error) {
	clean := strings.ToLower(strings.TrimSpace(value))
	switch clean {
	case "", "none", "no":
		return tls.NoClientCert, nil
	case "request", "request_client_cert":
		return tls.RequestClientCert, nil
	case "require", "require_any", "require_any_client_cert":
		return tls.RequireAnyClientCert, nil
	case "verify", "verify_if_given", "verify_client_cert":
		return tls.VerifyClientCertIfGiven, nil
	case "require_and_verify", "require-verify", "require_and_verify_client_cert":
		return tls.RequireAndVerifyClientCert, nil
	default:
		return 0, fmt.Errorf("unsupported client_auth %q", value)
	}
}

func requiresClientCA(auth tls.ClientAuthType) bool {
	return auth == tls.VerifyClientCertIfGiven || auth == tls.RequireAndVerifyClientCert
}
