package daemon

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"sync"
	"time"

	"git.cloudinit.dev/coreci/orca/internal/security"
)

// MTLSState holds the runtime state for the mTLS server. The hot-swap
// mechanism works by reading cert/key from disk + (optionally) the cert
// repo on every TLS handshake, so `orca cert renew` can write a new
// server.crt / server.key and the daemon picks it up without a restart.
//
// The actual handshake callback (`GetCertificate`) is set on the tls.Config
// by StartMTLS.
type MTLSState struct {
	CertPath string
	KeyPath  string
	CAPath   string

	Log *slog.Logger

	// mu guards the timestamp / counter so concurrent reads of the
	// on-disk cert are well-defined and we can log rotation events.
	mu          sync.Mutex
	lastModTime time.Time
}

// NewMTLSState validates the on-disk cert/key/CA paths and returns a
// state struct. Fails fast if the CA cert is missing or unreadable — the
// daemon must not start in mTLS mode without a CA.
func NewMTLSState(certPath, keyPath, caPath string, log *slog.Logger) (*MTLSState, error) {
	if certPath == "" || keyPath == "" || caPath == "" {
		return nil, errors.New("NewMTLSState: certPath, keyPath, and caPath are all required")
	}
	for _, p := range []string{certPath, keyPath, caPath} {
		if _, err := os.Stat(p); err != nil {
			return nil, fmt.Errorf("NewMTLSState: stat %s: %w", p, err)
		}
	}
	// Enforce CA file modes (REQ-033) at daemon start so we fail fast.
	caDir := caPath[:max(0, lastSep(caPath))]
	if err := security.EnforceFileModes(caDir); err != nil {
		return nil, fmt.Errorf("NewMTLSState: %w", err)
	}
	if log == nil {
		log = slog.Default()
	}
	return &MTLSState{
		CertPath: certPath,
		KeyPath:  keyPath,
		CAPath:   caPath,
		Log:      log,
	}, nil
}

// GetCertificate returns the tls.Certificate to present for a given
// ClientHelloInfo. It reloads the cert from disk on every call so that
// `orca cert renew` (which writes a new server.crt / server.key) takes
// effect without a daemon restart. REQ-034's hot-swap requirement.
//
// The reload is cheap — PEM decode is microseconds for typical cert
// sizes. The callback runs once per handshake; concurrency is fine.
func (m *MTLSState) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
	cert, err := tls.LoadX509KeyPair(m.CertPath, m.KeyPath)
	if err != nil {
		m.Log.Warn("mtls cert load failed (will fail handshake)",
			slog.String("cert", m.CertPath),
			slog.String("key", m.KeyPath),
			slog.String("err", err.Error()))
		return nil, err
	}
	cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
	if err != nil {
		// Not fatal — stdlib falls back to the raw cert. Log a warning.
		m.Log.Warn("mtls leaf parse failed (non-fatal)",
			slog.String("err", err.Error()))
	}
	m.touch()
	return &cert, nil
}

// touch updates the last-modified timestamp; primarily for tests.
func (m *MTLSState) touch() {
	m.mu.Lock()
	m.lastModTime = time.Now()
	m.mu.Unlock()
}

// LastReload returns the timestamp of the most recent successful reload
// from disk. Exposed for tests / health endpoints.
func (m *MTLSState) LastReload() time.Time {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.lastModTime
}

// StartMTLS reconfigures the existing http.Server to serve over TLS using
// the given state. The Server's httpServer field is mutated in place;
// callers that already have a goroutine running s.httpServer.Serve should
// shut it down first and then call StartMTLS, then re-serve.
//
// We also flip a flag so health endpoints can introspect mTLS state.
func (s *Server) StartMTLS(state *MTLSState) error {
	if state == nil {
		return errors.New("StartMTLS: state is nil")
	}
	tlsCfg, err := security.ServerTLSConfig(state.CertPath, state.KeyPath, state.CAPath)
	if err != nil {
		return fmt.Errorf("StartMTLS: %w", err)
	}
	tlsCfg.GetCertificate = state.GetCertificate
	// We REQUIRE client certs, so the handshake will fail (and log a
	// structured mtls.handshake_failed record) for plaintext-only clients.
	tlsCfg.ClientAuth = tls.RequireAndVerifyClientCert
	s.httpServer.TLSConfig = tlsCfg
	s.mtls = state
	s.log.Info("mTLS enabled",
		slog.String("cert", state.CertPath),
		slog.String("ca", state.CAPath),
		slog.String("component", "daemon"))
	return nil
}

// MTLSActive reports whether the server is configured to require mTLS.
func (s *Server) MTLSActive() bool { return s.mtls != nil }

// lastSep returns the index of the final separator in path. Used to
// extract the dir from a file path. Returns -1 if no separator is found.
func lastSep(path string) int {
	for i := len(path) - 1; i >= 0; i-- {
		if path[i] == '/' || path[i] == '\\' {
			return i
		}
	}
	return -1
}
