// Package openssl implements the issuer.Connector interface for custom CA integrations.
//
// This connector delegates certificate signing to user-provided scripts/commands.
// It allows operators to use their existing CA tooling (OpenSSL, cfssl, custom scripts, etc.)
// as the signing backend for certctl.
//
// Configuration:
//
//	SignScript: path to a script/command that signs CSRs.
//	  Called as: <sign_script> <csr_file> <cert_output_file>
//	  The script receives the CSR PEM as a temp file, and must write the signed cert PEM to the output file.
//	  Exit 0 = success, non-zero = failure (stderr captured as error message).
//
//	RevokeScript: path to a script/command that revokes certificates (optional).
//	  Called as: <revoke_script> <serial> <reason>
//	  Optional — if empty, revocation returns "not supported".
//
//	CRLScript: path to a script/command that generates a CRL (optional).
//	  Called as: <crl_script> <revoked_serials_json_file> <crl_output_file>
//	  Optional — if empty, CRL generation returns nil.
//
//	TimeoutSeconds: max time to wait for script execution (default 30).
package openssl

import (
	"context"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"time"

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

// Config represents the OpenSSL/Custom CA issuer connector configuration.
type Config struct {
	// SignScript is the path to a script/command that signs CSRs.
	// Called as: <sign_script> <csr_file> <cert_output_file>
	// The script receives the CSR PEM as a temp file, and must write the signed cert PEM to the output file.
	// Exit 0 = success, non-zero = failure (stderr captured as error message).
	SignScript string `json:"sign_script"`

	// RevokeScript is the path to a script/command that revokes certificates.
	// Called as: <revoke_script> <serial> <reason>
	// Optional — if empty, revocation returns "not supported".
	RevokeScript string `json:"revoke_script,omitempty"`

	// CRLScript is the path to a script/command that generates a CRL.
	// Called as: <crl_script> <revoked_serials_json_file> <crl_output_file>
	// Optional — if empty, CRL generation returns nil.
	CRLScript string `json:"crl_script,omitempty"`

	// TimeoutSeconds is the max time to wait for script execution.
	// Defaults to 30.
	TimeoutSeconds int `json:"timeout_seconds,omitempty"`
}

// Connector implements the issuer.Connector interface for custom CA signing via scripts.
type Connector struct {
	config  *Config
	logger  *slog.Logger
	timeout time.Duration
}

// New creates a new OpenSSL/Custom CA connector with the given configuration and logger.
func New(config *Config, logger *slog.Logger) *Connector {
	if config == nil {
		config = &Config{}
	}

	timeout := time.Duration(config.TimeoutSeconds) * time.Second
	if timeout == 0 {
		timeout = 30 * time.Second
	}

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

// ValidateConfig validates the OpenSSL/Custom CA configuration.
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 OpenSSL/Custom CA config: %w", err)
	}

	// SignScript is required
	if cfg.SignScript == "" {
		return fmt.Errorf("sign_script is required")
	}

	// Verify sign_script exists and is a regular file
	if info, err := os.Stat(cfg.SignScript); err != nil {
		return fmt.Errorf("sign_script not accessible: %w", err)
	} else if !info.Mode().IsRegular() {
		return fmt.Errorf("sign_script must be a regular file, got %s", info.Mode())
	}

	// Verify revoke_script exists and is a regular file if specified
	if cfg.RevokeScript != "" {
		if info, err := os.Stat(cfg.RevokeScript); err != nil {
			return fmt.Errorf("revoke_script not accessible: %w", err)
		} else if !info.Mode().IsRegular() {
			return fmt.Errorf("revoke_script must be a regular file, got %s", info.Mode())
		}
	}

	// Verify crl_script exists and is a regular file if specified
	if cfg.CRLScript != "" {
		if info, err := os.Stat(cfg.CRLScript); err != nil {
			return fmt.Errorf("crl_script not accessible: %w", err)
		} else if !info.Mode().IsRegular() {
			return fmt.Errorf("crl_script must be a regular file, got %s", info.Mode())
		}
	}

	// Update connector config
	c.config = &cfg
	timeout := time.Duration(cfg.TimeoutSeconds) * time.Second
	if timeout == 0 {
		timeout = 30 * time.Second
	}
	c.timeout = timeout

	c.logger.Info("OpenSSL/Custom CA configuration validated",
		"sign_script", cfg.SignScript,
		"has_revoke_script", cfg.RevokeScript != "",
		"has_crl_script", cfg.CRLScript != "",
		"timeout_seconds", c.timeout.Seconds())

	return nil
}

// IssueCertificate issues a new certificate by calling the sign script.
func (c *Connector) IssueCertificate(ctx context.Context, request issuer.IssuanceRequest) (*issuer.IssuanceResult, error) {
	c.logger.Info("processing custom CA issuance request",
		"common_name", request.CommonName,
		"san_count", len(request.SANs))

	// MaxTTLSeconds is advisory for script-based issuers — the sign script controls validity.
	// Log a warning so operators know the profile TTL cap isn't enforced server-side.
	if request.MaxTTLSeconds > 0 {
		c.logger.Warn("MaxTTLSeconds specified but OpenSSL/custom CA delegates signing to external script; TTL cap is advisory only",
			"max_ttl_seconds", request.MaxTTLSeconds,
			"common_name", request.CommonName)
	}

	// Write CSR to a temporary file
	csrFile, err := c.writeTempFile([]byte(request.CSRPEM), "csr-")
	if err != nil {
		c.logger.Error("failed to write CSR temp file", "error", err)
		return nil, fmt.Errorf("failed to write CSR temp file: %w", err)
	}
	defer os.Remove(csrFile)

	// Create temp file for cert output
	certFile := filepath.Join(filepath.Dir(csrFile), "cert-"+filepath.Base(csrFile))
	defer os.Remove(certFile)

	// Call sign script
	if err := c.callSignScript(ctx, csrFile, certFile); err != nil {
		c.logger.Error("sign script failed", "error", err)
		return nil, fmt.Errorf("sign script failed: %w", err)
	}

	// Read the signed certificate
	certPEM, err := os.ReadFile(certFile)
	if err != nil {
		c.logger.Error("failed to read signed certificate", "error", err)
		return nil, fmt.Errorf("failed to read signed certificate: %w", err)
	}

	// Parse the certificate to extract metadata
	cert, serial, err := c.parseCertificate(certPEM)
	if err != nil {
		c.logger.Error("failed to parse signed certificate", "error", err)
		return nil, fmt.Errorf("failed to parse signed certificate: %w", err)
	}

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

	result := &issuer.IssuanceResult{
		CertPEM:   string(certPEM),
		ChainPEM:  "", // Custom CA connectors typically don't provide chain; operators must configure separately
		Serial:    serial,
		NotBefore: cert.NotBefore,
		NotAfter:  cert.NotAfter,
		OrderID:   orderID,
	}

	c.logger.Info("certificate issued successfully",
		"serial", serial,
		"common_name", request.CommonName,
		"not_after", cert.NotAfter)

	return result, nil
}

// RenewCertificate renews a certificate by issuing a new one with the same identifiers.
// For custom CA connectors, this is functionally identical to IssueCertificate.
func (c *Connector) RenewCertificate(ctx context.Context, request issuer.RenewalRequest) (*issuer.IssuanceResult, error) {
	c.logger.Info("processing custom CA renewal request",
		"common_name", request.CommonName,
		"san_count", len(request.SANs))

	// Write CSR to a temporary file
	csrFile, err := c.writeTempFile([]byte(request.CSRPEM), "csr-")
	if err != nil {
		c.logger.Error("failed to write CSR temp file", "error", err)
		return nil, fmt.Errorf("failed to write CSR temp file: %w", err)
	}
	defer os.Remove(csrFile)

	// Create temp file for cert output
	certFile := filepath.Join(filepath.Dir(csrFile), "cert-"+filepath.Base(csrFile))
	defer os.Remove(certFile)

	// Call sign script
	if err := c.callSignScript(ctx, csrFile, certFile); err != nil {
		c.logger.Error("sign script failed", "error", err)
		return nil, fmt.Errorf("sign script failed: %w", err)
	}

	// Read the signed certificate
	certPEM, err := os.ReadFile(certFile)
	if err != nil {
		c.logger.Error("failed to read signed certificate", "error", err)
		return nil, fmt.Errorf("failed to read signed certificate: %w", err)
	}

	// Parse the certificate to extract metadata
	cert, serial, err := c.parseCertificate(certPEM)
	if err != nil {
		c.logger.Error("failed to parse signed certificate", "error", err)
		return nil, fmt.Errorf("failed to parse signed certificate: %w", err)
	}

	// Preserve order ID if provided
	orderID := fmt.Sprintf("openssl-%s", serial)
	if request.OrderID != nil {
		orderID = *request.OrderID
	}

	result := &issuer.IssuanceResult{
		CertPEM:   string(certPEM),
		ChainPEM:  "",
		Serial:    serial,
		NotBefore: cert.NotBefore,
		NotAfter:  cert.NotAfter,
		OrderID:   orderID,
	}

	c.logger.Info("certificate renewed successfully",
		"serial", serial,
		"common_name", request.CommonName,
		"not_after", cert.NotAfter)

	return result, nil
}

// hexSerialRegex validates that a serial number contains only hexadecimal characters.
// Certificate serial numbers are integers represented in hex (RFC 5280).
var hexSerialRegex = regexp.MustCompile(`^[0-9a-fA-F]+$`)

// validateSerial validates a certificate serial number for safe use in shell commands.
// Serial numbers must be non-empty, hex-only strings with no shell metacharacters.
func validateSerial(serial string) error {
	if serial == "" {
		return fmt.Errorf("serial number cannot be empty")
	}
	if !hexSerialRegex.MatchString(serial) {
		return fmt.Errorf("serial number %q contains non-hex characters (expected ^[0-9a-fA-F]+$)", serial)
	}
	if err := validation.ValidateShellCommand(serial); err != nil {
		return fmt.Errorf("serial number failed shell safety validation: %w", err)
	}
	return nil
}

// validateRevocationReason validates a revocation reason against RFC 5280 reason codes.
func validateRevocationReason(reason string) error {
	if !domain.IsValidRevocationReason(reason) {
		return fmt.Errorf("invalid revocation reason %q (must be a valid RFC 5280 reason code)", reason)
	}
	if err := validation.ValidateShellCommand(reason); err != nil {
		return fmt.Errorf("revocation reason failed shell safety validation: %w", err)
	}
	return nil
}

// RevokeCertificate revokes a certificate by calling the revoke script if configured.
func (c *Connector) RevokeCertificate(ctx context.Context, request issuer.RevocationRequest) error {
	if c.config.RevokeScript == "" {
		c.logger.Warn("revocation not supported (revoke_script not configured)", "serial", request.Serial)
		return nil // No-op if revoke script not configured
	}

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

	// Validate serial number (hex-only) and reason code (RFC 5280) before shell execution
	if err := validateSerial(request.Serial); err != nil {
		return fmt.Errorf("revocation input validation failed: %w", err)
	}
	if err := validateRevocationReason(reason); err != nil {
		return fmt.Errorf("revocation input validation failed: %w", err)
	}

	c.logger.Info("revoking certificate via revoke script",
		"serial", request.Serial,
		"reason", reason)

	// Call revoke script: <revoke_script> <serial> <reason>
	cmd := exec.CommandContext(ctx, c.config.RevokeScript, request.Serial, reason)
	cmd.Env = os.Environ() // Inherit environment

	if err := cmd.Run(); err != nil {
		// Log but don't fail — revocation is best-effort
		c.logger.Warn("revoke script completed with error",
			"serial", request.Serial,
			"error", err)
		// Return nil to indicate best-effort success
	}

	c.logger.Info("certificate revoked",
		"serial", request.Serial,
		"reason", reason)

	return nil
}

// GetOrderStatus returns the status of an issuance or renewal order.
// For custom CA connectors, orders complete immediately, so this always returns "completed" status.
func (c *Connector) GetOrderStatus(ctx context.Context, orderID string) (*issuer.OrderStatus, error) {
	c.logger.Info("fetching custom CA order status", "order_id", orderID)

	// Custom CA orders complete immediately
	status := &issuer.OrderStatus{
		OrderID:   orderID,
		Status:    "completed",
		UpdatedAt: time.Now(),
	}

	return status, nil
}

// GenerateCRL generates a DER-encoded X.509 CRL by calling the CRL script if configured.
// Returns nil if the CRL script is not configured.
func (c *Connector) GenerateCRL(ctx context.Context, revokedCerts []issuer.RevokedCertEntry) ([]byte, error) {
	if c.config.CRLScript == "" {
		c.logger.Debug("CRL generation not supported (crl_script not configured)")
		return nil, nil
	}

	c.logger.Info("generating CRL via crl script", "revoked_count", len(revokedCerts))

	// Write revoked serials to a temporary JSON file
	serialsJSON, err := c.marshalRevokedSerials(revokedCerts)
	if err != nil {
		c.logger.Error("failed to marshal revoked serials", "error", err)
		return nil, fmt.Errorf("failed to marshal revoked serials: %w", err)
	}

	serialsFile, err := c.writeTempFile(serialsJSON, "serials-")
	if err != nil {
		c.logger.Error("failed to write revoked serials temp file", "error", err)
		return nil, fmt.Errorf("failed to write revoked serials temp file: %w", err)
	}
	defer os.Remove(serialsFile)

	// Create temp file for CRL output
	crlFile := filepath.Join(filepath.Dir(serialsFile), "crl-"+filepath.Base(serialsFile))
	defer os.Remove(crlFile)

	// Call CRL script: <crl_script> <revoked_serials_json_file> <crl_output_file>
	cmd := exec.CommandContext(ctx, c.config.CRLScript, serialsFile, crlFile)
	cmd.Env = os.Environ()

	if err := cmd.Run(); err != nil {
		c.logger.Error("crl script failed", "error", err)
		return nil, fmt.Errorf("crl script failed: %w", err)
	}

	// Read the generated CRL
	crlDER, err := os.ReadFile(crlFile)
	if err != nil {
		c.logger.Error("failed to read generated CRL", "error", err)
		return nil, fmt.Errorf("failed to read generated CRL: %w", err)
	}

	c.logger.Info("CRL generated successfully", "crl_size", len(crlDER))

	return crlDER, nil
}

// SignOCSPResponse signs an OCSP response.
// Custom CA connectors don't support OCSP, so this returns nil.
func (c *Connector) SignOCSPResponse(ctx context.Context, req issuer.OCSPSignRequest) ([]byte, error) {
	c.logger.Debug("OCSP signing not supported by custom CA connector")
	return nil, nil
}

// GetCACertPEM is not supported by the custom CA connector (no CA cert access).
func (c *Connector) GetCACertPEM(ctx context.Context) (string, error) {
	return "", fmt.Errorf("custom CA connector does not provide CA certificate access")
}

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

// --- Helper Methods ---

// writeTempFile writes data to a temporary file and returns its path.
func (c *Connector) writeTempFile(data []byte, prefix string) (string, error) {
	f, err := os.CreateTemp("", prefix+"*.pem")
	if err != nil {
		return "", err
	}
	defer f.Close()

	if _, err := f.Write(data); err != nil {
		os.Remove(f.Name())
		return "", err
	}

	return f.Name(), nil
}

// callSignScript calls the sign script with CSR and cert output file paths.
// Returns the script's error message if execution fails.
func (c *Connector) callSignScript(ctx context.Context, csrFile, certFile string) error {
	ctx, cancel := context.WithTimeout(ctx, c.timeout)
	defer cancel()

	// Call sign script: <sign_script> <csr_file> <cert_output_file>
	cmd := exec.CommandContext(ctx, c.config.SignScript, csrFile, certFile)
	cmd.Env = os.Environ() // Inherit environment

	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("script exited with error: %w (output: %s)", err, string(output))
	}

	return nil
}

// parseCertificate parses a PEM-encoded certificate and extracts serial and X.509 cert.
func (c *Connector) parseCertificate(certPEM []byte) (*x509.Certificate, string, error) {
	block, _ := pem.Decode(certPEM)
	if block == nil || block.Type != "CERTIFICATE" {
		return nil, "", fmt.Errorf("invalid certificate PEM format")
	}

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

	serial := cert.SerialNumber.String()
	return cert, serial, nil
}

// marshalRevokedSerials converts revoked certs to JSON format for the CRL script.
// Format: [{"serial": "...", "revoked_at": "...", "reason_code": ...}, ...]
func (c *Connector) marshalRevokedSerials(revokedCerts []issuer.RevokedCertEntry) ([]byte, error) {
	type RevokedEntry struct {
		Serial      string `json:"serial"`
		RevokedAt   string `json:"revoked_at"`
		ReasonCode  int    `json:"reason_code"`
	}

	entries := make([]RevokedEntry, len(revokedCerts))
	for i, rc := range revokedCerts {
		entries[i] = RevokedEntry{
			Serial:     rc.SerialNumber.String(),
			RevokedAt:  rc.RevokedAt.Format(time.RFC3339),
			ReasonCode: rc.ReasonCode,
		}
	}

	return json.MarshalIndent(entries, "", "  ")
}
