package auth

import (
	"context"
	"fmt"
	"log"
	"strings"

	"github.com/gin-gonic/gin"

	"switchboard-core/models"
	"switchboard-core/store"
)

// MTLSConfig holds mTLS-specific configuration.
type MTLSConfig struct {
	HeaderDN          string // header carrying cert DN (default "X-SSL-Client-DN")
	HeaderVerify      string // header carrying verify status (default "X-SSL-Client-Verify")
	HeaderFingerprint string // header carrying cert fingerprint (default "X-SSL-Client-Fingerprint")
	AutoActivate      bool   // auto-activate new users (default true)
	DefaultTeam       string // team ID for auto-provisioned users (optional)
}

// MTLSProvider authenticates via client certificate headers injected
// by the TLS-terminating reverse proxy (nginx, Traefik, Istio).
//
// The backend never sees the actual TLS handshake — it trusts headers
// injected by the proxy after cert validation. After reading and parsing
// the DN, the provider resolves an existing user or auto-provisions a
// new one, then returns a Result. The auth handler issues an internal JWT.
//
// Header flow:
//
//	Client cert → nginx ssl_verify_client → injects X-SSL-Client-DN,
//	X-SSL-Client-Verify, X-SSL-Client-Fingerprint → backend reads headers.
type MTLSProvider struct {
	cfg MTLSConfig
}

func NewMTLSProvider(cfg MTLSConfig) *MTLSProvider {
	if cfg.HeaderDN == "" {
		cfg.HeaderDN = "X-SSL-Client-DN"
	}
	if cfg.HeaderVerify == "" {
		cfg.HeaderVerify = "X-SSL-Client-Verify"
	}
	if cfg.HeaderFingerprint == "" {
		cfg.HeaderFingerprint = "X-SSL-Client-Fingerprint"
	}
	return &MTLSProvider{cfg: cfg}
}

func (p *MTLSProvider) Mode() Mode { return ModeMTLS }

func (p *MTLSProvider) SupportsRegistration() bool { return false }

func (p *MTLSProvider) Register(_ *gin.Context, _ store.Stores) (*Result, error) {
	return nil, ErrNotSupported
}

// Authenticate reads the cert DN and verify status from proxy headers,
// parses the DN fields, and resolves or auto-provisions the user.
//
// Returns ErrInvalidCreds when headers are missing or verify fails.
// Returns ErrInactive when user exists but is deactivated.
func (p *MTLSProvider) Authenticate(c *gin.Context, stores store.Stores) (*Result, error) {
	// ── Validate headers ───────────────────────────────────────────
	verify := c.GetHeader(p.cfg.HeaderVerify)
	if verify == "" {
		return nil, fmt.Errorf("%w: missing %s header", ErrInvalidCreds, p.cfg.HeaderVerify)
	}
	// nginx: "SUCCESS", Traefik: "0" (both mean valid cert)
	if verify != "SUCCESS" && verify != "0" {
		return nil, fmt.Errorf("%w: cert verify=%s", ErrInvalidCreds, verify)
	}

	dn := c.GetHeader(p.cfg.HeaderDN)
	if dn == "" {
		return nil, fmt.Errorf("%w: missing %s header", ErrInvalidCreds, p.cfg.HeaderDN)
	}

	fields := ParseDN(dn)
	cn := fields["CN"]
	if cn == "" {
		return nil, fmt.Errorf("%w: cert DN has no CN field", ErrInvalidCreds)
	}

	// Stable external identity: fingerprint if available, else full DN
	fingerprint := c.GetHeader(p.cfg.HeaderFingerprint)
	if fingerprint == "" {
		fingerprint = dn
	}

	ctx := c.Request.Context()

	// ── Look up existing user ──────────────────────────────────────
	user, err := stores.Users.GetByExternalID(ctx, string(ModeMTLS), fingerprint)
	if err == nil && user != nil {
		if !user.IsActive {
			return nil, ErrInactive
		}
		log.Printf("[auth/mtls] existing user %s (%s)", user.Username, cn)
		return &Result{User: user, IsNewUser: false, VaultHint: ""}, nil
	}

	// ── Auto-provision ─────────────────────────────────────────────
	if !p.cfg.AutoActivate {
		return nil, fmt.Errorf("%w: auto-provision disabled", ErrInvalidCreds)
	}

	return p.autoProvision(ctx, fields, fingerprint, stores)
}

func (p *MTLSProvider) autoProvision(
	ctx context.Context,
	dn map[string]string,
	fingerprint string,
	stores store.Stores,
) (*Result, error) {
	cn := dn["CN"]

	email := dn["emailAddress"]
	if email == "" {
		email = models.HandleFromName(cn) + "@mtls.local"
	}

	handle := UniqueHandle(ctx, stores.Users, models.HandleFromName(cn))

	user := &models.User{
		Username:    handle,
		Email:       email,
		DisplayName: cn,
		IsActive:    true,
		AuthSource:  string(ModeMTLS),
		ExternalID:  &fingerprint,
		Handle:      handle,
	}

	if err := stores.Users.Create(ctx, user); err != nil {
		return nil, fmt.Errorf("auto-provision failed: %w", err)
	}

	log.Printf("[auth/mtls] auto-provisioned user %s from CN=%s", user.ID, cn)
	EnsureEveryoneGroup(ctx, stores, user.ID)

	// Auto-add to default team if configured
	if p.cfg.DefaultTeam != "" {
		if err := stores.Teams.AddMember(ctx, p.cfg.DefaultTeam, user.ID, "member"); err != nil {
			log.Printf("[auth/mtls] warn: could not add %s to default team: %v", user.ID, err)
		}
	}

	return &Result{User: user, IsNewUser: true, VaultHint: ""}, nil
}

// ParseDN parses an RFC 2253 / RFC 4514 distinguished name into key-value pairs.
//
// Examples:
//
//	"CN=Jeff Smith,O=Acme Corp,OU=Engineering"
//	"CN=Jane Doe,emailAddress=jane@acme.com,O=Acme Corp"
//
// Handles simple comma-separated key=value pairs. Does NOT handle
// escaped commas in values (\,) or multi-valued RDNs (+). Sufficient
// for typical X.509 client cert DNs.
func ParseDN(dn string) map[string]string {
	result := make(map[string]string)
	parts := strings.Split(dn, ",")
	for _, part := range parts {
		part = strings.TrimSpace(part)
		idx := strings.Index(part, "=")
		if idx < 1 {
			continue
		}
		key := strings.TrimSpace(part[:idx])
		val := strings.TrimSpace(part[idx+1:])
		result[key] = val
	}
	return result
}
