package server

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"math/big"
	"net"
	"os"
	"path/filepath"
	"sync"
	"time"

	"golang.org/x/crypto/acme/autocert"
)

// certGenerationMutex protects concurrent certificate generation for the same host
var certGenerationMutex sync.Mutex

// generateSelfSignedCert generates a self-signed certificate for the given host.
// Returns the certificate and private key in PEM format.
func generateSelfSignedCert(host string) (certPEM, keyPEM []byte, err error) {
	// Generate private key
	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to generate private key: %w", err)
	}

	// Create certificate template
	notBefore := time.Now()
	notAfter := notBefore.Add(365 * 24 * time.Hour) // Valid for 1 year

	serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
	if err != nil {
		return nil, nil, fmt.Errorf("failed to generate serial number: %w", err)
	}

	template := x509.Certificate{
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			Organization: []string{"ResolveSpec Self-Signed"},
			CommonName:   host,
		},
		NotBefore:             notBefore,
		NotAfter:              notAfter,
		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
	}

	// Add host as DNS name or IP address
	if ip := net.ParseIP(host); ip != nil {
		template.IPAddresses = []net.IP{ip}
	} else {
		template.DNSNames = []string{host}
	}

	// Create certificate
	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
	}

	// Encode certificate to PEM
	certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})

	// Encode private key to PEM
	privBytes, err := x509.MarshalECPrivateKey(priv)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to marshal private key: %w", err)
	}
	keyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes})

	return certPEM, keyPEM, nil
}

// sanitizeHostname converts a hostname to a safe filename by replacing invalid characters.
func sanitizeHostname(host string) string {
	// Replace any character that's not alphanumeric, dot, or dash with underscore
	safe := ""
	for _, r := range host {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '-' {
			safe += string(r)
		} else {
			safe += "_"
		}
	}
	return safe
}

// getCertDirectory returns the directory path for storing self-signed certificates.
// Creates the directory if it doesn't exist.
func getCertDirectory() (string, error) {
	// Use a consistent directory in the user's cache directory
	cacheDir, err := os.UserCacheDir()
	if err != nil {
		// Fallback to current directory if cache dir is not available
		cacheDir = "."
	}

	certDir := filepath.Join(cacheDir, "resolvespec", "certs")

	// Create directory if it doesn't exist
	if err := os.MkdirAll(certDir, 0700); err != nil {
		return "", fmt.Errorf("failed to create certificate directory: %w", err)
	}

	return certDir, nil
}

// isCertificateValid checks if a certificate file exists and is not expired.
func isCertificateValid(certFile string) bool {
	// Check if file exists
	certData, err := os.ReadFile(certFile)
	if err != nil {
		return false
	}

	// Parse certificate
	block, _ := pem.Decode(certData)
	if block == nil {
		return false
	}

	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		return false
	}

	// Check if certificate is expired or will expire in the next 30 days
	now := time.Now()
	expiryThreshold := now.Add(30 * 24 * time.Hour)

	if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
		return false
	}

	// Renew if expiring soon
	if expiryThreshold.After(cert.NotAfter) {
		return false
	}

	return true
}

// saveCertToFiles saves certificate and key PEM data to persistent files.
// Returns the file paths for the certificate and key.
func saveCertToFiles(certPEM, keyPEM []byte, host string) (certFile, keyFile string, err error) {
	// Get certificate directory
	certDir, err := getCertDirectory()
	if err != nil {
		return "", "", err
	}

	// Sanitize hostname for safe file naming
	safeHost := sanitizeHostname(host)

	// Use consistent file names based on host
	certFile = filepath.Join(certDir, fmt.Sprintf("%s-cert.pem", safeHost))
	keyFile = filepath.Join(certDir, fmt.Sprintf("%s-key.pem", safeHost))

	// Write certificate
	if err := os.WriteFile(certFile, certPEM, 0600); err != nil {
		return "", "", fmt.Errorf("failed to write certificate: %w", err)
	}

	// Write key
	if err := os.WriteFile(keyFile, keyPEM, 0600); err != nil {
		return "", "", fmt.Errorf("failed to write private key: %w", err)
	}

	return certFile, keyFile, nil
}

// setupAutoTLS configures automatic TLS certificate management using Let's Encrypt.
// Returns a TLS config that can be used with http.Server.
func setupAutoTLS(domains []string, email, cacheDir string) (*tls.Config, error) {
	if len(domains) == 0 {
		return nil, fmt.Errorf("at least one domain must be specified for AutoTLS")
	}

	// Set default cache directory
	if cacheDir == "" {
		cacheDir = "./certs-cache"
	}

	// Create cache directory if it doesn't exist
	if err := os.MkdirAll(cacheDir, 0700); err != nil {
		return nil, fmt.Errorf("failed to create certificate cache directory: %w", err)
	}

	// Create autocert manager
	m := &autocert.Manager{
		Prompt:     autocert.AcceptTOS,
		Cache:      autocert.DirCache(cacheDir),
		HostPolicy: autocert.HostWhitelist(domains...),
		Email:      email,
	}

	// Create TLS config
	tlsConfig := m.TLSConfig()
	tlsConfig.MinVersion = tls.VersionTLS13

	return tlsConfig, nil
}

// configureTLS configures TLS for the server based on the provided configuration.
// Returns the TLS config and certificate/key file paths (if applicable).
func configureTLS(cfg Config) (tlsConfig *tls.Config, certFile string, keyFile string, err error) {
	// Option 1: Certificate files provided
	if cfg.SSLCert != "" && cfg.SSLKey != "" {
		// Validate that files exist
		if _, err := os.Stat(cfg.SSLCert); os.IsNotExist(err) {
			return nil, "", "", fmt.Errorf("SSL certificate file not found: %s", cfg.SSLCert)
		}
		if _, err := os.Stat(cfg.SSLKey); os.IsNotExist(err) {
			return nil, "", "", fmt.Errorf("SSL key file not found: %s", cfg.SSLKey)
		}

		// Return basic TLS config - cert/key will be loaded by ListenAndServeTLS
		tlsConfig := &tls.Config{
			MinVersion: tls.VersionTLS12,
		}
		return tlsConfig, cfg.SSLCert, cfg.SSLKey, nil
	}

	// Option 2: Auto TLS (Let's Encrypt)
	if cfg.AutoTLS {
		tlsConfig, err := setupAutoTLS(cfg.AutoTLSDomains, cfg.AutoTLSEmail, cfg.AutoTLSCacheDir)
		if err != nil {
			return nil, "", "", fmt.Errorf("failed to setup AutoTLS: %w", err)
		}
		return tlsConfig, "", "", nil
	}

	// Option 3: Self-signed certificate
	if cfg.SelfSignedSSL {
		host := cfg.Host
		if host == "" || host == "0.0.0.0" {
			host = "localhost"
		}

		// Sanitize hostname for safe file naming
		safeHost := sanitizeHostname(host)

		// Lock to prevent concurrent certificate generation for the same host
		certGenerationMutex.Lock()
		defer certGenerationMutex.Unlock()

		// Get certificate directory
		certDir, err := getCertDirectory()
		if err != nil {
			return nil, "", "", fmt.Errorf("failed to get certificate directory: %w", err)
		}

		// Check for existing valid certificates
		certFile := filepath.Join(certDir, fmt.Sprintf("%s-cert.pem", safeHost))
		keyFile := filepath.Join(certDir, fmt.Sprintf("%s-key.pem", safeHost))

		// If valid certificates exist, reuse them
		if isCertificateValid(certFile) {
			// Verify key file also exists
			if _, err := os.Stat(keyFile); err == nil {
				tlsConfig := &tls.Config{
					MinVersion: tls.VersionTLS12,
				}
				return tlsConfig, certFile, keyFile, nil
			}
		}

		// Generate new certificates
		certPEM, keyPEM, err := generateSelfSignedCert(host)
		if err != nil {
			return nil, "", "", fmt.Errorf("failed to generate self-signed certificate: %w", err)
		}

		certFile, keyFile, err = saveCertToFiles(certPEM, keyPEM, host)
		if err != nil {
			return nil, "", "", fmt.Errorf("failed to save self-signed certificate: %w", err)
		}

		tlsConfig := &tls.Config{
			MinVersion: tls.VersionTLS12,
		}
		return tlsConfig, certFile, keyFile, nil
	}

	return nil, "", "", nil
}
