// Package ca implements the Controller's internal Certificate Authority:
// the root-of-trust that signs both the Controller's own server cert and
// every Agent's client cert during the pairing flow.
//
// Files on disk under caDir:
//
//	ca.crt          — self-signed root CA certificate (PEM)
//	ca.key          — CA private key (PEM, mode 0600)
//	controller.crt  — Controller's server cert, signed by ca.key (PEM)
//	controller.key  — Controller's server key (PEM, mode 0600)
//
// Bootstrapping is idempotent: on first start we generate both pairs; on
// every subsequent start we load them. Rotate by deleting the specific
// file(s) you want regenerated and restarting.
package ca

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"errors"
	"fmt"
	"math/big"
	"net"
	"os"
	"path/filepath"
	"time"
)

const (
	rsaBits          = 2048
	caValidity       = 10 * 365 * 24 * time.Hour
	serverValidity   = 365 * 24 * time.Hour
	agentValidity    = 365 * 24 * time.Hour
	caCommonName     = "panel Internal CA"
	serverCommonName = "panel controller"
)

// CA wraps the loaded root cert/key plus the Controller's server cert/key.
type CA struct {
	Root     *x509.Certificate
	RootKey  *rsa.PrivateKey
	RootPEM  []byte // cached ca.crt bytes
	Server   *x509.Certificate
	ServerKey *rsa.PrivateKey
}

// Ensure loads (or initializes) the CA + server cert under dir. If
// hostnames is non-empty, it's added as SANs on the server cert (e.g.
// "mypanel.example.com"). localhost + 127.0.0.1 + the machine's
// hostname are always included.
func Ensure(dir string, extraHostnames []string) (*CA, error) {
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return nil, fmt.Errorf("mkdir %s: %w", dir, err)
	}
	rootCert, rootKey, rootPEM, err := ensureRoot(dir)
	if err != nil {
		return nil, err
	}
	serverCert, serverKey, err := ensureServer(dir, rootCert, rootKey, extraHostnames)
	if err != nil {
		return nil, err
	}
	return &CA{
		Root: rootCert, RootKey: rootKey, RootPEM: rootPEM,
		Server: serverCert, ServerKey: serverKey,
	}, nil
}

func ensureRoot(dir string) (*x509.Certificate, *rsa.PrivateKey, []byte, error) {
	certPath := filepath.Join(dir, "ca.crt")
	keyPath := filepath.Join(dir, "ca.key")
	if _, err := os.Stat(certPath); err == nil {
		cert, pemBytes, err := loadCert(certPath)
		if err != nil {
			return nil, nil, nil, fmt.Errorf("load ca.crt: %w", err)
		}
		key, err := loadKey(keyPath)
		if err != nil {
			return nil, nil, nil, fmt.Errorf("load ca.key: %w", err)
		}
		return cert, key, pemBytes, nil
	}

	priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("rsa key: %w", err)
	}
	tpl := &x509.Certificate{
		SerialNumber:          randomSerial(),
		Subject:               pkix.Name{CommonName: caCommonName, Organization: []string{"panel"}},
		NotBefore:             time.Now().Add(-1 * time.Minute),
		NotAfter:              time.Now().Add(caValidity),
		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
		BasicConstraintsValid: true,
		IsCA:                  true,
		MaxPathLenZero:        false,
	}
	der, err := x509.CreateCertificate(rand.Reader, tpl, tpl, &priv.PublicKey, priv)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("create ca cert: %w", err)
	}
	cert, err := x509.ParseCertificate(der)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("parse ca cert: %w", err)
	}
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
	if err := os.WriteFile(certPath, certPEM, 0o644); err != nil {
		return nil, nil, nil, err
	}
	if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
		return nil, nil, nil, err
	}
	return cert, priv, certPEM, nil
}

func ensureServer(dir string, root *x509.Certificate, rootKey *rsa.PrivateKey, extraHostnames []string) (*x509.Certificate, *rsa.PrivateKey, error) {
	certPath := filepath.Join(dir, "controller.crt")
	keyPath := filepath.Join(dir, "controller.key")
	if _, err := os.Stat(certPath); err == nil {
		cert, _, err := loadCert(certPath)
		if err != nil {
			return nil, nil, fmt.Errorf("load controller.crt: %w", err)
		}
		key, err := loadKey(keyPath)
		if err != nil {
			return nil, nil, fmt.Errorf("load controller.key: %w", err)
		}
		return cert, key, nil
	}
	priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
	if err != nil {
		return nil, nil, err
	}
	host, _ := os.Hostname()
	ips := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
	dns := []string{"localhost", host}
	// Split extraHostnames into DNS vs IP — TLS validates IP dials against
	// IPAddresses, not DNSNames, so any LAN IP an agent will dial must
	// land in the IP set.
	for _, h := range extraHostnames {
		if ip := net.ParseIP(h); ip != nil {
			ips = append(ips, ip)
		} else {
			dns = append(dns, h)
		}
	}
	dns = uniq(dns)

	tpl := &x509.Certificate{
		SerialNumber: randomSerial(),
		Subject:      pkix.Name{CommonName: serverCommonName, Organization: []string{"panel"}},
		NotBefore:    time.Now().Add(-1 * time.Minute),
		NotAfter:     time.Now().Add(serverValidity),
		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		DNSNames:     dns,
		IPAddresses:  ips,
	}
	der, err := x509.CreateCertificate(rand.Reader, tpl, root, &priv.PublicKey, rootKey)
	if err != nil {
		return nil, nil, err
	}
	cert, err := x509.ParseCertificate(der)
	if err != nil {
		return nil, nil, err
	}
	if err := os.WriteFile(certPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o644); err != nil {
		return nil, nil, err
	}
	if err := os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}), 0o600); err != nil {
		return nil, nil, err
	}
	return cert, priv, nil
}

// SignAgentCSR validates a CSR from a pairing agent and returns the
// signed cert as PEM. The agent's CN becomes its identity. The cert
// is marked ExtKeyUsageClientAuth (mTLS client only).
func (c *CA) SignAgentCSR(csrPEM []byte, agentID string) (certPEM []byte, err error) {
	block, _ := pem.Decode(csrPEM)
	if block == nil || block.Type != "CERTIFICATE REQUEST" {
		return nil, errors.New("bad CSR PEM")
	}
	csr, err := x509.ParseCertificateRequest(block.Bytes)
	if err != nil {
		return nil, fmt.Errorf("parse CSR: %w", err)
	}
	if err := csr.CheckSignature(); err != nil {
		return nil, fmt.Errorf("CSR signature: %w", err)
	}
	// Force our agent_id into CN + DNS SANs regardless of what the CSR asked for.
	// (This is the panel's canonical identity for the cert; the agent can't
	// claim to be someone else.)
	tpl := &x509.Certificate{
		SerialNumber: randomSerial(),
		Subject:      pkix.Name{CommonName: agentID, Organization: []string{"panel-agent"}},
		NotBefore:    time.Now().Add(-1 * time.Minute),
		NotAfter:     time.Now().Add(agentValidity),
		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
		DNSNames:     []string{agentID},
	}
	der, err := x509.CreateCertificate(rand.Reader, tpl, c.Root, csr.PublicKey, c.RootKey)
	if err != nil {
		return nil, fmt.Errorf("sign: %w", err)
	}
	return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), nil
}

// ---- helpers ----

func randomSerial() *big.Int {
	max := new(big.Int).Lsh(big.NewInt(1), 128)
	n, _ := rand.Int(rand.Reader, max)
	return n
}

func loadCert(path string) (*x509.Certificate, []byte, error) {
	body, err := os.ReadFile(path)
	if err != nil {
		return nil, nil, err
	}
	block, _ := pem.Decode(body)
	if block == nil {
		return nil, nil, errors.New("no PEM block")
	}
	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		return nil, nil, err
	}
	return cert, body, nil
}

func loadKey(path string) (*rsa.PrivateKey, error) {
	body, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	block, _ := pem.Decode(body)
	if block == nil {
		return nil, errors.New("no PEM block")
	}
	return x509.ParsePKCS1PrivateKey(block.Bytes)
}

func uniq(in []string) []string {
	seen := map[string]bool{}
	out := make([]string, 0, len(in))
	for _, s := range in {
		if s == "" || seen[s] {
			continue
		}
		seen[s] = true
		out = append(out, s)
	}
	return out
}
