package mtls

import (
	"context"
	"crypto/x509"
	"fmt"

	"github.com/redis/go-redis/v9"
)

// crlKey is the Redis SET that holds revoked node UUIDs.
const crlKey = "mtls:revoked"

// DB is the minimal persistence interface for revocation records.
// In production this is backed by Postgres; in tests a no-op is sufficient.
type DB interface {
	RecordRevocation(ctx context.Context, nodeUUID string) error
}

// CRL manages certificate revocation.
//
// Redis is the authoritative hot-path store (checked on every TLS handshake).
// DB is the durable backing store consulted on restart to re-populate Redis.
type CRL struct {
	redis redis.Cmdable
	db    DB // may be nil in tests
}

// NewCRL creates a CRL manager.
// db may be nil; if non-nil, Revoke also persists to it.
func NewCRL(r redis.Cmdable, db DB) *CRL {
	return &CRL{redis: r, db: db}
}

// Revoke marks nodeUUID as revoked.
// The UUID is written to the Redis revocation set immediately;
// if db is non-nil the record is also persisted there.
func (c *CRL) Revoke(ctx context.Context, nodeUUID string) error {
	if err := c.redis.SAdd(ctx, crlKey, nodeUUID).Err(); err != nil {
		return fmt.Errorf("mtls: revoke in redis: %w", err)
	}
	if c.db != nil {
		if err := c.db.RecordRevocation(ctx, nodeUUID); err != nil {
			return fmt.Errorf("mtls: revoke in db: %w", err)
		}
	}
	return nil
}

// IsRevoked returns true when nodeUUID is in the revocation set.
//
// On Redis error the function returns true (fail-safe: reject rather than
// silently allow a potentially revoked node to connect).
func (c *CRL) IsRevoked(nodeUUID string) bool {
	ctx := context.Background()
	revoked, err := c.redis.SIsMember(ctx, crlKey, nodeUUID).Result()
	if err != nil {
		// Fail-safe: treat transient Redis errors as revoked.
		return true
	}
	return revoked
}

// VerifyPeerCertificate is a tls.Config.VerifyPeerCertificate callback.
// It is invoked by the TLS stack after standard chain validation succeeds.
// If the leaf certificate's CN corresponds to a revoked node, the handshake
// is aborted.
//
// When no client certificate is presented (e.g. during Enroll) verifiedChains
// is empty and this function is a no-op; the identity interceptor handles
// the per-method enforcement.
func (c *CRL) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
	for _, chain := range verifiedChains {
		if len(chain) == 0 {
			continue
		}
		cn := chain[0].Subject.CommonName
		if cn != "" && c.IsRevoked(cn) {
			return fmt.Errorf("mtls: certificate revoked for node %q", cn)
		}
	}
	return nil
}
