package security

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

// CAValidity is how long a CA cert is valid. Per D-013, the CA is long-lived
// (10 years) because manual rotation is expensive.
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). This constant is retained for the dual-write window and
// scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
const CAValidity = 10 * 365 * 24 * time.Hour

// ServerCertValidity is the default validity window for server certs. D-013
// says server certs are short-lived (90 days) to limit the compromise window.
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). This constant is retained for the dual-write window and
// scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
const ServerCertValidity = 90 * 24 * time.Hour

// CAKeySize is the RSA key size used for both CA and server certs. 3072 is
// the minimum we accept for v0.2 — matches REQ-033 spirit and Go's stdlib
// defaults for new RSA keys are typically 2048 or 4096. 3072 is the
// sweet spot for balance of safety and key-gen latency.
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). This constant is retained for the dual-write window and
// scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
const CAKeySize = 3072

// CAMode is the file mode used when persisting the CA private key. REQ-033
// requires 0600.
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). This constant is retained for the dual-write window and
// scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
const CAMode os.FileMode = 0o600

// CACPEMMode is the file mode used when persisting the CA public cert.
// REQ-033 requires 0644 (public, but still mode-pinned).
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). This constant is retained for the dual-write window and
// scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
const CACPEMMode os.FileMode = 0o644

// File names used inside the CA directory.
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). These constants are retained for the dual-write window and
// scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
const (
	CACertFile = "ca.crt"
	CAKeyFile  = "ca.key"
)

// CA wraps a loaded CA. Use CAInit to mint a new one, LoadCA to read an
// existing one from disk.
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). The CA type is retained for the dual-write window and
// scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
type CA struct {
	Cert      *x509.Certificate
	Key       *rsa.PrivateKey
	CertPEM   []byte
	Dir       string
	NotBefore time.Time
	NotAfter  time.Time
}

// CAInit creates a fresh self-signed CA and persists it to dir/ca.crt and
// dir/ca.key with the required file modes (REQ-033). If the CA files already
// exist with valid content, the existing CA is returned — idempotent.
//
// commonName is the CA's CommonName (typically an org/cluster identifier).
// Returns a *CA wrapping the loaded cert + key. The CA is valid for
// CAValidity from now.
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). CAInit is retained for the dual-write window and scheduled
// for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
func CAInit(dir, commonName string) (*CA, error) {
	if dir == "" {
		return nil, errors.New("CAInit: dir is required")
	}
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return nil, fmt.Errorf("CAInit: mkdir: %w", err)
	}
	certPath := filepath.Join(dir, CACertFile)
	keyPath := filepath.Join(dir, CAKeyFile)

	// Fast path: existing CA — load and return.
	if ok, err := bothExist(certPath, keyPath); err != nil {
		return nil, err
	} else if ok {
		// Verify file modes on the existing CA (REQ-033).
		if err := EnforceFileModes(dir); err != nil {
			return nil, err
		}
		return LoadCA(dir)
	}

	// Generate key.
	key, err := rsa.GenerateKey(rand.Reader, CAKeySize)
	if err != nil {
		return nil, fmt.Errorf("CAInit: generate key: %w", err)
	}

	// Self-signed cert. We use x509.Certificate directly to set the CA
	// extensions. Serial number is random 128 bits.
	serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
	if err != nil {
		return nil, fmt.Errorf("CAInit: serial: %w", err)
	}
	now := time.Now().UTC()
	tmpl := &x509.Certificate{
		SerialNumber: serial,
		Subject: pkix.Name{
			CommonName:   commonName,
			Organization: []string{"orca-internal-ca"},
		},
		NotBefore:             now.Add(-1 * time.Hour),
		NotAfter:              now.Add(CAValidity),
		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
		BasicConstraintsValid: true,
		IsCA:                  true,
		MaxPathLen:            1,
		MaxPathLenZero:        false,
	}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
	if err != nil {
		return nil, fmt.Errorf("CAInit: create cert: %w", err)
	}
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
	keyDER, err := x509.MarshalPKCS8PrivateKey(key)
	if err != nil {
		return nil, fmt.Errorf("CAInit: marshal key: %w", err)
	}
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})

	// Atomic write: temp file + rename. This avoids leaving a half-written
	// ca.key on disk if the process crashes mid-write.
	if err := WriteAtomic(certPath, CACPEMMode, certPEM); err != nil {
		return nil, err
	}
	if err := WriteAtomic(keyPath, CAMode, keyPEM); err != nil {
		return nil, err
	}

	return LoadCA(dir)
}

// LoadCA reads a previously-initialized CA from disk. Returns a *CA or an
// error. Verifies file modes (REQ-033).
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). LoadCA is retained for the dual-write window and scheduled
// for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
func LoadCA(dir string) (*CA, error) {
	if dir == "" {
		return nil, errors.New("LoadCA: dir is required")
	}
	certPath := filepath.Join(dir, CACertFile)
	keyPath := filepath.Join(dir, CAKeyFile)

	if err := EnforceFileModes(dir); err != nil {
		return nil, err
	}

	certPEM, err := os.ReadFile(certPath)
	if err != nil {
		return nil, fmt.Errorf("LoadCA: read cert: %w", err)
	}
	keyPEM, err := os.ReadFile(keyPath)
	if err != nil {
		return nil, fmt.Errorf("LoadCA: read key: %w", err)
	}

	certBlock, _ := pem.Decode(certPEM)
	if certBlock == nil {
		return nil, fmt.Errorf("LoadCA: cert PEM decode failed")
	}
	cert, err := x509.ParseCertificate(certBlock.Bytes)
	if err != nil {
		return nil, fmt.Errorf("LoadCA: parse cert: %w", err)
	}
	keyBlock, _ := pem.Decode(keyPEM)
	if keyBlock == nil {
		return nil, fmt.Errorf("LoadCA: key PEM decode failed")
	}
	keyAny, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes)
	if err != nil {
		return nil, fmt.Errorf("LoadCA: parse key: %w", err)
	}
	key, ok := keyAny.(*rsa.PrivateKey)
	if !ok {
		return nil, fmt.Errorf("LoadCA: key is %T, not *rsa.PrivateKey", keyAny)
	}

	return &CA{
		Cert:      cert,
		Key:       key,
		CertPEM:   certPEM,
		Dir:       dir,
		NotBefore: cert.NotBefore,
		NotAfter:  cert.NotAfter,
	}, nil
}

// EnforceFileModes refuses to operate if ca.crt / ca.key do not have the
// required modes (REQ-033). Returns nil on success. Callers (daemon start,
// CA loaders) MUST call this and abort on error.
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). EnforceFileModes is retained for the dual-write window
// and scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
// EnforceFileModes checks that all security-sensitive files in dir have
// the correct permissions (REQ-033 + REQ-130, F13). Checks: ca.crt
// (0644), ca.key (0600), orca_ssh_key (0600), orca_ssh_key.pub (0644),
// known_hosts (0600), master.key (0600), master.key.sealed (0600),
// server.crt (0644), server.key (0600). Missing files are skipped (they
// may not exist yet — e.g. before init or after migration to step-ca).
func EnforceFileModes(dir string) error {
	// Files that must be 0600 (secrets/keys).
	secretFiles := []string{
		CAKeyFile,
		"orca_ssh_key",
		"known_hosts",
		"master.key",
		"master.key.sealed",
		"server.key",
	}
	// Files that must be 0644 (certs/public keys).
	publicFiles := []string{
		CACertFile,
		"orca_ssh_key.pub",
		"server.crt",
	}
	for _, name := range secretFiles {
		path := filepath.Join(dir, name)
		info, err := os.Stat(path)
		if err != nil {
			continue // skip missing
		}
		if info.Mode().Perm() != 0o600 {
			return fmt.Errorf(
				"REQ-033/130 violation: %s has mode %04o, want 0600 — fix with `chmod 0600 %s`",
				path, info.Mode().Perm(), path,
			)
		}
	}
	for _, name := range publicFiles {
		path := filepath.Join(dir, name)
		info, err := os.Stat(path)
		if err != nil {
			continue // skip missing
		}
		if info.Mode().Perm() != 0o644 {
			return fmt.Errorf(
				"REQ-033/130 violation: %s has mode %04o, want 0644 — fix with `chmod 0644 %s`",
				path, info.Mode().Perm(), path,
			)
		}
	}
	return nil
}

// SignCSR signs a PEM-encoded CSR with the CA and returns the issued cert
// in PEM form. The resulting cert is valid for ServerCertValidity and
// inherits the SANs from the CSR (DNS, IP). If the CSR has no SANs, the
// call fails — REQ-036 requires server certs to have identifying SANs.
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). SignCSR is retained for the dual-write window and
// scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
func (c *CA) SignCSR(csrPEM []byte) ([]byte, error) {
	if c == nil || c.Cert == nil || c.Key == nil {
		return nil, errors.New("SignCSR: nil CA")
	}
	block, _ := pem.Decode(csrPEM)
	if block == nil {
		return nil, errors.New("SignCSR: CSR PEM decode failed")
	}
	if block.Type != "CERTIFICATE REQUEST" && block.Type != "NEW CERTIFICATE REQUEST" {
		return nil, fmt.Errorf("SignCSR: unexpected PEM type %q", block.Type)
	}
	csr, err := x509.ParseCertificateRequest(block.Bytes)
	if err != nil {
		return nil, fmt.Errorf("SignCSR: parse CSR: %w", err)
	}
	if err := csr.CheckSignature(); err != nil {
		return nil, fmt.Errorf("SignCSR: CSR signature invalid: %w", err)
	}
	// REQ-036: refuse CSRs without SANs. A server cert needs at least
	// one DNS or IP SAN so the peer can verify it against a pinned identity.
	if len(csr.DNSNames) == 0 && len(csr.IPAddresses) == 0 {
		return nil, errors.New("SignCSR: CSR has no DNS or IP SANs (REQ-036) — must include at least one")
	}

	serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
	if err != nil {
		return nil, fmt.Errorf("SignCSR: serial: %w", err)
	}
	now := time.Now().UTC()
	tmpl := &x509.Certificate{
		SerialNumber: serial,
		Subject:      csr.Subject,
		NotBefore:    now.Add(-1 * time.Hour),
		NotAfter:     now.Add(ServerCertValidity),
		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
		DNSNames:     csr.DNSNames,
		IPAddresses:  csr.IPAddresses,
	}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, c.Cert, csr.PublicKey, c.Key)
	if err != nil {
		return nil, fmt.Errorf("SignCSR: create cert: %w", err)
	}
	return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), nil
}

// Fingerprint returns the SHA-256 hex fingerprint of the CA cert. Useful
// for the operator to communicate to peers out-of-band; peers then pin
// this value at `orca node join --ca-fingerprint <sha>`.
//
// Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
// (D-101/REQ-076). CA.Fingerprint is retained for the dual-write window and
// scheduled for deletion in v0.10-P14. See .ciagent/PRD_v0.9.md.
func (c *CA) Fingerprint() string {
	return FingerprintOf(c.Cert.Raw)
}

// bothExist returns true if both paths exist (regular files).
func bothExist(paths ...string) (bool, error) {
	for _, p := range paths {
		info, err := os.Stat(p)
		if err != nil {
			if os.IsNotExist(err) {
				return false, nil
			}
			return false, err
		}
		if !info.Mode().IsRegular() {
			return false, fmt.Errorf("not a regular file: %s", p)
		}
	}
	return true, nil
}

// WriteCert writes a cert PEM blob to path with mode 0644 atomically.
// REQ-033 requires cert files to be 0644; this helper enforces that.
func WriteCert(path string, pemBytes []byte) error {
	return WriteAtomic(path, CACPEMMode, pemBytes)
}

// WriteKey writes a private-key PEM blob to path with mode 0600
// atomically. REQ-033 requires key files to be 0600; this helper
// enforces that.
func WriteKey(path string, pemBytes []byte) error {
	return WriteAtomic(path, CAMode, pemBytes)
}

// WriteAtomic writes data to a temp file in dir and renames. Sets the
// requested perm before the rename so the file lands at the right mode.
// Exported (AD-029) so the key-reset / known_hosts atomic rewrite path
// in proxmox (T02.6/T02.7) can reuse it instead of duplicating the
// ~20-LOC pattern (RESEARCH §5 pitfall #10).
func WriteAtomic(path string, mode os.FileMode, data []byte) error {
	dir := filepath.Dir(path)
	tmp, err := os.CreateTemp(dir, ".tmp-*")
	if err != nil {
		return fmt.Errorf("WriteAtomic: create temp: %w", err)
	}
	tmpName := tmp.Name()
	// Best-effort cleanup if we fail before rename.
	defer func() {
		_ = os.Remove(tmpName)
	}()
	if _, err := tmp.Write(data); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("WriteAtomic: write: %w", err)
	}
	if err := tmp.Chmod(mode); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("WriteAtomic: chmod: %w", err)
	}
	if err := tmp.Sync(); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("WriteAtomic: sync: %w", err)
	}
	if err := tmp.Close(); err != nil {
		return fmt.Errorf("WriteAtomic: close: %w", err)
	}
	if err := os.Rename(tmpName, path); err != nil {
		return fmt.Errorf("WriteAtomic: rename: %w", err)
	}
	return nil
}
