package security

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"os"
)

// allowedSuites is the AEAD cipher allowlist required by D-015. We only
// support TLS 1.3, so the Go cipher suite names below are TLS 1.3 cipher
// suites. In Go 1.22+, the CipherSuites field still works for TLS 1.2
// negotiation, but with MinVersion=tls.VersionTLS13 only the TLS 1.3
// suites apply.
//
// We pin the three NIST/CHACHA AEAD suites:
//   - TLS_AES_256_GCM_SHA384
//   - TLS_CHACHA20_POLY1305_SHA256
//   - TLS_AES_128_GCM_SHA256
//
// No TLS 1.2 fallback. No CBC modes. No NULL/integrity-only modes.
var allowedSuites = []uint16{
	tls.TLS_AES_256_GCM_SHA384,
	tls.TLS_CHACHA20_POLY1305_SHA256,
	tls.TLS_AES_128_GCM_SHA256,
}

// AllowedCipherSuites returns a copy of the cipher allowlist. Exposed for
// tests and for callers that want to construct their own tls.Config with
// the same policy.
func AllowedCipherSuites() []uint16 {
	out := make([]uint16, len(allowedSuites))
	copy(out, allowedSuites)
	return out
}

// loadKeyPair is a small helper: load cert + key from disk, return
// tls.Certificate. Errors are wrapped with the path that failed.
func loadKeyPair(certPath, keyPath string) (tls.Certificate, error) {
	if certPath == "" || keyPath == "" {
		return tls.Certificate{}, errors.New("loadKeyPair: certPath and keyPath are required")
	}
	cert, err := tls.LoadX509KeyPair(certPath, keyPath)
	if err != nil {
		return tls.Certificate{}, fmt.Errorf("load cert/key pair (%s, %s): %w", certPath, keyPath, err)
	}
	return cert, nil
}

// loadCAPool reads a PEM CA cert file and returns a CertPool containing
// that cert. We use the subject as the trust anchor — clients verify
// server certs against this single CA.
func loadCAPool(caPath string) (*x509.CertPool, error) {
	if caPath == "" {
		return nil, errors.New("loadCAPool: caPath is required")
	}
	caPEM, err := os.ReadFile(caPath)
	if err != nil {
		return nil, fmt.Errorf("read CA cert: %w", err)
	}
	pool := x509.NewCertPool()
	if !pool.AppendCertsFromPEM(caPEM) {
		return nil, fmt.Errorf("parse CA cert PEM from %s", caPath)
	}
	return pool, nil
}

// ServerTLSConfig returns a *tls.Config suitable for an mTLS server. The
// server presents certPath/keyPath and requires client certs signed by
// the CA at caPath. The cipher allowlist + MinVersion=1.3 are enforced.
//
// ClientCAs is the same pool as the trust store — peers present certs
// signed by the same CA, and we verify them. GetCertificate is left nil;
// callers (the daemon) populate it to enable hot-swap on cert renewal.
//
// Returns an error if any path is missing or any file cannot be read.
func ServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) {
	if _, err := loadKeyPair(certPath, keyPath); err != nil {
		return nil, err
	}
	pool, err := loadCAPool(caPath)
	if err != nil {
		return nil, err
	}
	return &tls.Config{
		MinVersion:   tls.VersionTLS13,
		MaxVersion:   tls.VersionTLS13,
		CipherSuites: AllowedCipherSuites(),
		Certificates: []tls.Certificate{{Certificate: nil}}, // placeholder; daemon fills via GetCertificate
		ClientCAs:    pool,
		ClientAuth:   tls.RequireAndVerifyClientCert,
		NextProtos:   []string{"h2", "http/1.1"},
	}, nil
}

// ClientTLSConfig returns a *tls.Config suitable for an mTLS client. The
// client verifies the server cert against the CA at caPath. If certPath
// and keyPath are both non-empty, the client also presents a cert (for
// mutual auth). If only one is set, the call fails — both-or-neither.
//
// serverName is the expected server identity (SNI / cert SAN match). It
// MUST match a SAN on the server cert; the standard tls.Config will then
// validate it during the handshake. For extra safety, callers should also
// use VerifyPeerCertificate to enforce a pinned peer identity.
func ClientTLSConfig(caPath, serverName string, certPath, keyPath string) (*tls.Config, error) {
	pool, err := loadCAPool(caPath)
	if err != nil {
		return nil, err
	}
	cfg := &tls.Config{
		MinVersion:   tls.VersionTLS13,
		MaxVersion:   tls.VersionTLS13,
		CipherSuites: AllowedCipherSuites(),
		RootCAs:      pool,
		ServerName:   serverName,
		NextProtos:   []string{"h2", "http/1.1"},
	}
	hasCert, hasKey := certPath != "", keyPath != ""
	if hasCert != hasKey {
		return nil, errors.New("ClientTLSConfig: certPath and keyPath must be both set or both empty")
	}
	if hasCert && hasKey {
		cert, err := loadKeyPair(certPath, keyPath)
		if err != nil {
			return nil, err
		}
		cfg.Certificates = []tls.Certificate{cert}
	}
	return cfg, nil
}
