// Package auth provides mTLS client-certificate verification for the
// opt-in source-side mTLS feature (M14 W2, SPEC §15). HMAC + API key
// are still the default; mTLS layers on top with an AND relationship
// when source.mtls_required=true.
//
// The verifier wraps a *x509.CertPool (the intermediate CA that
// issues source certs) and a callback for CN/SAN → source_id
// resolution. It is consumed by ingestd's HTTP listener when the
// source is configured for mTLS.
//
// Threading: Verifier is safe for concurrent use. The cert pool is
// read-only after construction; rotation creates a new Verifier and
// atomically swaps the pointer (see internal/auth/mtls_rotation.go,
// not in this file).
package auth

import (
	"crypto/x509"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"
)

// timeNow is overridable in tests.
var timeNow = time.Now

// contains is a tiny helper to keep imports clean.
func contains(s, substr string) bool {
	return strings.Contains(s, substr)
}

// SourceIDResolver maps a verified client cert to the source_id it
// represents. Implementations typically look up the CN or SAN in
// Postgres. The Verifier calls this after chain + expiry checks
// succeed.
//
// Returning a non-nil error rejects the connection. The error is
// logged by the caller and surfaced to the client as 401.
type SourceIDResolver interface {
	// ResolveSourceID returns the source_id for a verified cert.
	// cert.Subject.CommonName is the CN, cert.DNSNames / cert.URIs
	// are the SANs.
	ResolveSourceID(cert *x509.Certificate) (sourceID string, err error)
}

// StaticSourceIDResolver is the simplest implementation: it accepts
// any cert where the CN matches "source:<id>.<company_slug>" and
// returns the <id> as the source_id. Suitable for testing and for
// environments without a Postgres lookup. Production should use a
// resolver that verifies the source_id is active in the DB.
type StaticSourceIDResolver struct{}

func (StaticSourceIDResolver) ResolveSourceID(cert *x509.Certificate) (string, error) {
	const prefix = "source:"
	if !strings.HasPrefix(cert.Subject.CommonName, prefix) {
		return "", fmt.Errorf("mtls: CN %q does not start with %q", cert.Subject.CommonName, prefix)
	}
	rest := cert.Subject.CommonName[len(prefix):]
	dot := strings.Index(rest, ".")
	if dot <= 0 {
		return "", fmt.Errorf("mtls: CN %q missing company_slug suffix", cert.Subject.CommonName)
	}
	return rest[:dot], nil
}

// Verifier checks a client cert against the intermediate CA pool and
// resolves it to a source_id. Construct once at startup, then reuse
// across all requests.
type Verifier struct {
	// Intermediates is the pool of CA certs that sign source certs.
	// In production this is the intermediate from scripts/cert-manager/ca-init.sh.
	Intermediates *x509.CertPool

	// Resolver maps a verified cert to a source_id.
	Resolver SourceIDResolver

	// Optional: a list of revoked cert serials (in-memory CRL). For
	// production, use cert-manager's CRL or OCSP responder.
	revokedMu sync.RWMutex
	revoked   map[string]struct{} // serial hex → {}
}

// NewVerifier constructs a Verifier with the given CA pool.
// resolver must be non-nil.
func NewVerifier(intermediates *x509.CertPool, resolver SourceIDResolver) *Verifier {
	if resolver == nil {
		resolver = StaticSourceIDResolver{}
	}
	return &Verifier{
		Intermediates: intermediates,
		Resolver:      resolver,
		revoked:       make(map[string]struct{}),
	}
}

// ErrCertExpired is returned when the client cert is past its
// notAfter or before its notBefore.
var ErrCertExpired = errors.New("mtls: client certificate expired or not yet valid")

// ErrUntrustedIssuer is returned when the cert chain doesn't lead to
// any cert in the Intermediates pool.
var ErrUntrustedIssuer = errors.New("mtls: client certificate signed by untrusted CA")

// ErrMissingClientUsage is returned when the cert doesn't have
// ExtKeyUsageClientAuth set.
var ErrMissingClientUsage = errors.New("mtls: client certificate missing ExtKeyUsageClientAuth")

// ErrRevoked is returned when the cert serial is in the revoked set.
var ErrRevoked = errors.New("mtls: client certificate revoked")

// VerifyChain checks the cert against the intermediate pool. It does
// NOT resolve to a source_id — call ResolveSourceID separately.
// Returns nil on success, or one of the sentinel errors above.
func (v *Verifier) VerifyChain(cert *x509.Certificate) error {
	if cert == nil {
		return errors.New("mtls: nil certificate")
	}

	// Check expiry first — cheap, no pool needed.
	now := timeNow()
	if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
		return fmt.Errorf("%w (notBefore=%s, notAfter=%s, now=%s)",
			ErrCertExpired, cert.NotBefore, cert.NotAfter, now)
	}

	// Build a chain of certs to verify. The peer cert + any
	// intermediates the caller passes (we don't have those here in
	// the basic Verifier — the caller wires the chain from the TLS
	// handshake).
	intermediates := x509.NewCertPool()
	// The trust anchor is the intermediate CA pool.
	opts := x509.VerifyOptions{
		Roots:         v.Intermediates,
		Intermediates: intermediates,
		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
		CurrentTime:   now,
	}
	if _, err := cert.Verify(opts); err != nil {
		// x509 verification failures are not all the same. The Go
		// stdlib returns stringified errors (no sentinel), so we
		// match on the error message. For better matching, the
		// caller should use VerifyHostname + their own checks.
		msg := err.Error()
		if contains(msg, "expired") || contains(msg, "not yet valid") {
			return fmt.Errorf("%w: %s", ErrCertExpired, err)
		}
		return fmt.Errorf("%w: %s", ErrUntrustedIssuer, err)
	}

	// Check key usage explicitly. Verify() with KeyUsages set does
	// check this, but only if the cert has ExtKeyUsage at all — a
	// cert without ExtKeyUsage passes. We want to be strict for
	// client auth, so we double-check.
	hasClientAuth := false
	for _, usage := range cert.ExtKeyUsage {
		if usage == x509.ExtKeyUsageClientAuth {
			hasClientAuth = true
			break
		}
	}
	if !hasClientAuth {
		return ErrMissingClientUsage
	}
	// If ExtKeyUsage is empty AND no ExtKeyUsageServerAuth either, some
	// old certs rely on KeyUsage only. We accept KeyUsage=DigitalSignature
	// as a fallback.
	if len(cert.ExtKeyUsage) == 0 {
		if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
			return ErrMissingClientUsage
		}
	}

	// Check revocation list.
	v.revokedMu.RLock()
	_, isRevoked := v.revoked[cert.SerialNumber.Text(16)]
	v.revokedMu.RUnlock()
	if isRevoked {
		return ErrRevoked
	}

	return nil
}

// VerifyResult is the outcome of a successful Verify call.
type VerifyResult struct {
	SourceID    string
	CompanySlug string
	SerialHex   string
	NotAfter    time.Time
}

// Verify is the high-level entry point: chain check + resolve to
// source_id. Returns a VerifyResult on success.
func (v *Verifier) Verify(cert *x509.Certificate) (*VerifyResult, error) {
	if err := v.VerifyChain(cert); err != nil {
		return nil, err
	}
	sourceID, err := v.Resolver.ResolveSourceID(cert)
	if err != nil {
		return nil, err
	}
	// Company slug from CN: source:<id>.<company_slug>
	companySlug := ""
	if rest, ok := strings.CutPrefix(cert.Subject.CommonName, "source:"); ok {
		if dot := strings.Index(rest, "."); dot > 0 {
			companySlug = rest[dot+1:]
		}
	}
	return &VerifyResult{
		SourceID:    sourceID,
		CompanySlug: companySlug,
		SerialHex:   cert.SerialNumber.Text(16),
		NotAfter:    cert.NotAfter,
	}, nil
}

// Revoke marks a cert serial as revoked. The next Verify call for a
// cert with this serial returns ErrRevoked. Idempotent.
func (v *Verifier) Revoke(serialHex string) {
	v.revokedMu.Lock()
	v.revoked[serialHex] = struct{}{}
	v.revokedMu.Unlock()
}

// Unrevoke removes a serial from the revoked set. Useful for
// recovering from a misissued revoke.
func (v *Verifier) Unrevoke(serialHex string) {
	v.revokedMu.Lock()
	delete(v.revoked, serialHex)
	v.revokedMu.Unlock()
}

// IsRevoked returns true if the serial is in the revoked set.
func (v *Verifier) IsRevoked(serialHex string) bool {
	v.revokedMu.RLock()
	defer v.revokedMu.RUnlock()
	_, ok := v.revoked[serialHex]
	return ok
}
