// Package stepca implements the issuer.Connector interface for Smallstep step-ca
// private certificate authority.
//
// step-ca is a popular open-source private CA that provides both ACME and native
// provisioner-based certificate issuance. This connector uses the native /sign API
// with JWK provisioner authentication, which is simpler than ACME for internal PKI:
// no challenge solving, no domain validation — just CSR + auth token → signed cert.
//
// For teams already using step-ca, this connector integrates certctl's lifecycle
// management (renewal policies, deployment, audit) with step-ca's certificate signing.
//
// Authentication: JWK provisioner with a shared provisioner password.
// The connector generates a short-lived token for each signing request using the
// provisioner key (loaded from disk or provided inline).
//
// step-ca API used:
//
//	POST /sign — submit CSR with provisioner token, receive signed certificate
//	POST /revoke — revoke a certificate by serial
//	GET  /health — check CA availability
package stepca

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"time"

	"github.com/shankar0123/certctl/internal/connector/issuer"
)

// Config represents the step-ca issuer connector configuration.
type Config struct {
	// CAURL is the base URL of the step-ca server (e.g., "https://ca.internal:9000").
	CAURL string `json:"ca_url"`

	// RootCertPath is the path to the step-ca root certificate PEM (for TLS verification).
	// If empty, the system trust store is used.
	RootCertPath string `json:"root_cert_path,omitempty"`

	// ProvisionerName is the name of the JWK provisioner to use for signing.
	ProvisionerName string `json:"provisioner_name"`

	// ProvisionerKeyPath is the path to the provisioner's encrypted private key (JWK JSON).
	// This is the key file generated by `step ca provisioner add`.
	ProvisionerKeyPath string `json:"provisioner_key_path,omitempty"`

	// ProvisionerPassword is the password to decrypt the provisioner key.
	// Can also be set via CERTCTL_STEPCA_PROVISIONER_PASSWORD env var.
	ProvisionerPassword string `json:"provisioner_password,omitempty"`

	// ValidityDays is the requested certificate validity (step-ca may enforce a maximum).
	// Defaults to 90.
	ValidityDays int `json:"validity_days,omitempty"`
}

// Connector implements the issuer.Connector interface for step-ca.
type Connector struct {
	config     *Config
	logger     *slog.Logger
	httpClient *http.Client
}

// New creates a new step-ca connector with the given configuration and logger.
// If RootCertPath is set, the HTTP client will trust that CA certificate for TLS connections.
// Otherwise, the system trust store is used (which works if setup-trust.sh has run).
func New(config *Config, logger *slog.Logger) *Connector {
	// Don't default ValidityDays — let step-ca use its own default duration.
	// Operators can explicitly set ValidityDays if their step-ca is configured
	// with longer max durations. A zero value means "omit from sign request."

	httpClient := &http.Client{Timeout: 30 * time.Second}

	// Load custom root CA cert if provided
	if config != nil && config.RootCertPath != "" {
		rootPEM, err := os.ReadFile(config.RootCertPath)
		if err == nil {
			pool := x509.NewCertPool()
			if pool.AppendCertsFromPEM(rootPEM) {
				httpClient.Transport = &http.Transport{
					TLSClientConfig: &tls.Config{
						RootCAs: pool,
					},
				}
				logger.Info("step-ca custom root CA loaded", "path", config.RootCertPath)
			}
		} else {
			logger.Warn("failed to read step-ca root cert, using system trust store", "path", config.RootCertPath, "error", err)
		}
	}

	return &Connector{
		config:     config,
		logger:     logger,
		httpClient: httpClient,
	}
}

// ValidateConfig checks that the step-ca configuration is valid and the CA is reachable.
func (c *Connector) ValidateConfig(ctx context.Context, rawConfig json.RawMessage) error {
	var cfg Config
	if err := json.Unmarshal(rawConfig, &cfg); err != nil {
		return fmt.Errorf("invalid step-ca config: %w", err)
	}

	if cfg.CAURL == "" {
		return fmt.Errorf("step-ca ca_url is required")
	}

	if cfg.ProvisionerName == "" {
		return fmt.Errorf("step-ca provisioner_name is required")
	}

	// Don't default ValidityDays — 0 means "let step-ca use its own default duration"

	// Check CA health
	healthURL := cfg.CAURL + "/health"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil)
	if err != nil {
		return fmt.Errorf("failed to create health check request: %w", err)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("step-ca not reachable at %s: %w", cfg.CAURL, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("step-ca health check returned status %d", resp.StatusCode)
	}

	// Validate provisioner key path exists if provided
	if cfg.ProvisionerKeyPath != "" {
		if _, err := os.Stat(cfg.ProvisionerKeyPath); err != nil {
			return fmt.Errorf("provisioner key not accessible: %w", err)
		}
	}

	c.config = &cfg
	c.logger.Info("step-ca configuration validated",
		"ca_url", cfg.CAURL,
		"provisioner", cfg.ProvisionerName)

	return nil
}

// signRequest is the JSON body for the step-ca /sign endpoint.
type signRequest struct {
	CsrPEM    string    `json:"csr"`
	OTT       string    `json:"ott"` // One-Time Token (provisioner JWT)
	NotBefore time.Time `json:"notBefore,omitempty"`
	NotAfter  time.Time `json:"notAfter,omitempty"`
}

// signResponse is the JSON response from the step-ca /sign endpoint.
type signResponse struct {
	ServerPEM certificateChain `json:"serverPEM,omitempty"`
	CaPEM     certificateChain `json:"caPEM,omitempty"`
	CertChainPEM []certBlock  `json:"certChainPEM,omitempty"`
}

type certificateChain struct {
	Certificate string `json:"certificate"`
}

type certBlock struct {
	Certificate string `json:"certificate"`
}

// IssueCertificate submits a CSR to step-ca for signing.
func (c *Connector) IssueCertificate(ctx context.Context, request issuer.IssuanceRequest) (*issuer.IssuanceResult, error) {
	c.logger.Info("processing step-ca issuance request",
		"common_name", request.CommonName,
		"san_count", len(request.SANs))

	// Generate a provisioner token (OTT) for this request
	ott, err := c.generateProvisionerToken(request.CommonName, request.SANs)
	if err != nil {
		return nil, fmt.Errorf("failed to generate provisioner token: %w", err)
	}

	// Build the sign request.
	// When ValidityDays is 0 (default), omit NotBefore/NotAfter so step-ca uses its
	// own default duration (typically 24h). The signRequest struct has omitempty on
	// both time fields, so zero-value time.Time{} gets stripped from the JSON.
	signReq := signRequest{
		CsrPEM: request.CSRPEM,
		OTT:    ott,
	}
	if c.config.ValidityDays > 0 || request.MaxTTLSeconds > 0 {
		now := time.Now()
		signReq.NotBefore = now
		if c.config.ValidityDays > 0 {
			signReq.NotAfter = now.AddDate(0, 0, c.config.ValidityDays)
		}
		// Cap validity to MaxTTLSeconds if profile specifies a maximum
		if request.MaxTTLSeconds > 0 {
			maxNotAfter := now.Add(time.Duration(request.MaxTTLSeconds) * time.Second)
			if signReq.NotAfter.IsZero() || maxNotAfter.Before(signReq.NotAfter) {
				signReq.NotAfter = maxNotAfter
			}
		}
	}

	body, err := json.Marshal(signReq)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal sign request: %w", err)
	}

	// POST /sign
	signURL := c.config.CAURL + "/sign"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, signURL, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("failed to create sign request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("step-ca sign request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read sign response: %w", err)
	}

	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("step-ca sign returned status %d: %s", resp.StatusCode, string(respBody))
	}

	// Parse response — step-ca returns the cert chain
	certPEM, chainPEM, serial, certNotBefore, certNotAfter, err := parseSignResponse(respBody)
	if err != nil {
		return nil, fmt.Errorf("failed to parse sign response: %w", err)
	}

	orderID := fmt.Sprintf("stepca-%s", serial)

	c.logger.Info("step-ca certificate issued",
		"common_name", request.CommonName,
		"serial", serial,
		"not_after", certNotAfter)

	return &issuer.IssuanceResult{
		CertPEM:   certPEM,
		ChainPEM:  chainPEM,
		Serial:    serial,
		NotBefore: certNotBefore,
		NotAfter:  certNotAfter,
		OrderID:   orderID,
	}, nil
}

// RenewCertificate renews a certificate by creating a new signing request.
// For step-ca, renewal is functionally identical to issuance.
func (c *Connector) RenewCertificate(ctx context.Context, request issuer.RenewalRequest) (*issuer.IssuanceResult, error) {
	c.logger.Info("processing step-ca renewal request",
		"common_name", request.CommonName,
		"san_count", len(request.SANs))

	return c.IssueCertificate(ctx, issuer.IssuanceRequest{
		CommonName:    request.CommonName,
		SANs:          request.SANs,
		CSRPEM:        request.CSRPEM,
		MaxTTLSeconds: request.MaxTTLSeconds,
	})
}

// revokeRequest is the JSON body for the step-ca /revoke endpoint.
type revokeRequest struct {
	Serial     string `json:"serial"`
	ReasonCode int    `json:"reasonCode,omitempty"`
	Reason     string `json:"reason,omitempty"`
	OTT        string `json:"ott"`
	Passive    bool   `json:"passive"` // true = don't propagate to OCSP (just mark revoked)
}

// RevokeCertificate revokes a certificate at step-ca.
func (c *Connector) RevokeCertificate(ctx context.Context, request issuer.RevocationRequest) error {
	c.logger.Info("processing step-ca revocation request", "serial", request.Serial)

	ott, err := c.generateProvisionerToken(request.Serial, nil)
	if err != nil {
		return fmt.Errorf("failed to generate revocation token: %w", err)
	}

	reason := "unspecified"
	if request.Reason != nil {
		reason = *request.Reason
	}

	revokeReq := revokeRequest{
		Serial:  request.Serial,
		Reason:  reason,
		OTT:     ott,
		Passive: true,
	}

	body, err := json.Marshal(revokeReq)
	if err != nil {
		return fmt.Errorf("failed to marshal revoke request: %w", err)
	}

	revokeURL := c.config.CAURL + "/revoke"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, revokeURL, bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("failed to create revoke request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("step-ca revoke request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		respBody, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("step-ca revoke returned status %d: %s", resp.StatusCode, string(respBody))
	}

	c.logger.Info("step-ca certificate revoked", "serial", request.Serial, "reason", reason)
	return nil
}

// GetOrderStatus returns the status of a step-ca order.
// step-ca signs synchronously, so orders are always "completed" immediately.
func (c *Connector) GetOrderStatus(ctx context.Context, orderID string) (*issuer.OrderStatus, error) {
	return &issuer.OrderStatus{
		OrderID:   orderID,
		Status:    "completed",
		UpdatedAt: time.Now(),
	}, nil
}

// generateProvisionerToken creates a short-lived JWT (One-Time Token) for step-ca API calls.
// The JWT is signed with the provisioner's private key (loaded from the encrypted JWE file
// at ProvisionerKeyPath and decrypted with ProvisionerPassword).
func (c *Connector) generateProvisionerToken(subject string, sans []string) (string, error) {
	var key *ecdsa.PrivateKey
	var kid string

	if c.config.ProvisionerKeyPath != "" {
		// Production: load and decrypt the real provisioner key from disk
		var err error
		key, kid, err = c.loadProvisionerKey()
		if err != nil {
			return "", fmt.Errorf("failed to load provisioner key: %w", err)
		}
	} else {
		// Fallback: generate an ephemeral key (for testing or when key path not configured).
		// This won't authenticate with a real step-ca server, but allows the connector
		// to function against mock servers in tests.
		c.logger.Warn("no provisioner key path configured, using ephemeral key (will not work with real step-ca)")
		var err error
		key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
		if err != nil {
			return "", fmt.Errorf("failed to generate ephemeral key: %w", err)
		}
		kid = "ephemeral"
	}

	now := time.Now()

	// step-ca expects: aud = <ca-url>/1.0/sign (the sign endpoint audience)
	claims := map[string]interface{}{
		"sub":  subject,
		"iss":  c.config.ProvisionerName,
		"aud":  c.config.CAURL + "/1.0/sign",
		"nbf":  now.Unix(),
		"iat":  now.Unix(),
		"exp":  now.Add(5 * time.Minute).Unix(),
		"jti":  generateJTI(),
		"sha":  kid, // step-ca uses this to look up the provisioner by key fingerprint
	}

	if len(sans) > 0 {
		claims["sans"] = sans
	}

	return signJWTWithKID(claims, key, kid)
}

// loadProvisionerKey loads and decrypts the step-ca provisioner key from disk.
// Returns the ECDSA private key and the key ID (JWK thumbprint).
func (c *Connector) loadProvisionerKey() (*ecdsa.PrivateKey, string, error) {
	if c.config.ProvisionerKeyPath == "" {
		return nil, "", fmt.Errorf("provisioner_key_path is required for step-ca JWK authentication")
	}

	jweData, err := os.ReadFile(c.config.ProvisionerKeyPath)
	if err != nil {
		return nil, "", fmt.Errorf("failed to read provisioner key file %s: %w", c.config.ProvisionerKeyPath, err)
	}

	password := c.config.ProvisionerPassword
	if password == "" {
		return nil, "", fmt.Errorf("provisioner_password is required to decrypt the provisioner key")
	}

	key, kid, err := decryptProvisionerKey(jweData, password)
	if err != nil {
		return nil, "", fmt.Errorf("failed to decrypt provisioner key: %w", err)
	}

	c.logger.Info("provisioner key loaded and decrypted",
		"key_path", c.config.ProvisionerKeyPath,
		"kid", kid)

	return key, kid, nil
}

// generateJTI creates a unique JWT ID.
func generateJTI() string {
	b := make([]byte, 16)
	_, _ = rand.Read(b)
	return base64.RawURLEncoding.EncodeToString(b)
}

// signJWTWithKID creates an ES256 JWT with a key ID in the header.
func signJWTWithKID(claims map[string]interface{}, key *ecdsa.PrivateKey, kid string) (string, error) {
	// Header with kid so step-ca can look up the provisioner
	header := map[string]string{
		"alg": "ES256",
		"typ": "JWT",
		"kid": kid,
	}

	return signJWTRaw(claims, key, header)
}

// signJWTRaw creates an ES256 JWT from the given claims and header.
func signJWTRaw(claims map[string]interface{}, key *ecdsa.PrivateKey, header map[string]string) (string, error) {

	headerJSON, err := json.Marshal(header)
	if err != nil {
		return "", err
	}

	claimsJSON, err := json.Marshal(claims)
	if err != nil {
		return "", err
	}

	headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
	claimsB64 := base64.RawURLEncoding.EncodeToString(claimsJSON)
	signingInput := headerB64 + "." + claimsB64

	// Sign with ES256
	hash := sha256.Sum256([]byte(signingInput))
	r, s, err := ecdsa.Sign(rand.Reader, key, hash[:])
	if err != nil {
		return "", fmt.Errorf("failed to sign JWT: %w", err)
	}

	// Encode signature as fixed-size concatenation (r || s, 32 bytes each for P-256)
	sig := make([]byte, 64)
	rBytes := r.Bytes()
	sBytes := s.Bytes()
	copy(sig[32-len(rBytes):32], rBytes)
	copy(sig[64-len(sBytes):64], sBytes)

	sigB64 := base64.RawURLEncoding.EncodeToString(sig)
	return signingInput + "." + sigB64, nil
}

// parseSignResponse extracts the certificate and chain from step-ca's /sign response.
func parseSignResponse(respBody []byte) (certPEM string, chainPEM string, serial string, notBefore time.Time, notAfter time.Time, err error) {
	// step-ca /sign response format:
	// { "crt": "-----BEGIN CERTIFICATE-----\n...", "ca": "-----BEGIN CERTIFICATE-----\n..." }
	// or
	// { "serverPEM": { "certificate": "..." }, "caPEM": { "certificate": "..." } }
	// or
	// { "certChainPEM": [ { "certificate": "..." }, ... ] }

	// Try the simple format first (crt/ca)
	var simpleResp struct {
		Crt string `json:"crt"`
		Ca  string `json:"ca"`
	}
	if err = json.Unmarshal(respBody, &simpleResp); err == nil && simpleResp.Crt != "" {
		certPEM = simpleResp.Crt
		chainPEM = simpleResp.Ca
	} else {
		// Try the structured format
		var structResp signResponse
		if err = json.Unmarshal(respBody, &structResp); err != nil {
			return "", "", "", time.Time{}, time.Time{}, fmt.Errorf("failed to parse sign response: %w", err)
		}

		if structResp.ServerPEM.Certificate != "" {
			certPEM = structResp.ServerPEM.Certificate
			chainPEM = structResp.CaPEM.Certificate
		} else if len(structResp.CertChainPEM) > 0 {
			certPEM = structResp.CertChainPEM[0].Certificate
			for i := 1; i < len(structResp.CertChainPEM); i++ {
				chainPEM += structResp.CertChainPEM[i].Certificate
			}
		}
	}

	if certPEM == "" {
		return "", "", "", time.Time{}, time.Time{}, fmt.Errorf("no certificate in sign response")
	}

	// Parse the leaf cert to extract metadata
	block, _ := pem.Decode([]byte(certPEM))
	if block == nil {
		return "", "", "", time.Time{}, time.Time{}, fmt.Errorf("failed to decode certificate PEM")
	}

	cert, parseErr := x509.ParseCertificate(block.Bytes)
	if parseErr != nil {
		return "", "", "", time.Time{}, time.Time{}, fmt.Errorf("failed to parse certificate: %w", parseErr)
	}

	serial = cert.SerialNumber.String()
	notBefore = cert.NotBefore
	notAfter = cert.NotAfter

	return certPEM, chainPEM, serial, notBefore, notAfter, nil
}

// GenerateCRL is not supported by step-ca as step-ca provides its own CRL endpoint.
func (c *Connector) GenerateCRL(ctx context.Context, revokedCerts []issuer.RevokedCertEntry) ([]byte, error) {
	return nil, fmt.Errorf("step-ca provides its own CRL endpoint; use step-ca's /crl directly")
}

// SignOCSPResponse is not supported by step-ca as step-ca provides its own OCSP responder.
func (c *Connector) SignOCSPResponse(ctx context.Context, req issuer.OCSPSignRequest) ([]byte, error) {
	return nil, fmt.Errorf("step-ca provides its own OCSP responder; use step-ca's /ocsp directly")
}

// GetCACertPEM is not directly supported; step-ca serves its own /root endpoint.
func (c *Connector) GetCACertPEM(ctx context.Context) (string, error) {
	return "", fmt.Errorf("step-ca serves its own CA certificate at /root; use step-ca's endpoint directly")
}

// GetRenewalInfo returns nil, nil as step-ca does not support ACME Renewal Information (ARI).
func (c *Connector) GetRenewalInfo(ctx context.Context, certPEM string) (*issuer.RenewalInfoResult, error) {
	return nil, nil
}

// Ensure Connector implements the issuer.Connector interface.
var _ issuer.Connector = (*Connector)(nil)
