// Package googlecas implements the issuer.Connector interface for
// Google Cloud Certificate Authority Service (CAS).
//
// Google CAS is a managed private CA service on GCP. This connector
// uses the CAS REST API (privateca.googleapis.com/v1) with OAuth2
// service account authentication. Certificates are issued synchronously.
//
// Authentication: OAuth2 service account via JWT → access token exchange.
// No Google SDK dependency — uses stdlib crypto/rsa + net/http.
//
// API endpoints used:
//
//	POST /v1/{parent}/certificates         - Issue certificate
//	POST /v1/{name}:revoke                 - Revoke certificate
//	POST /v1/{caPool}:fetchCaCerts         - Get CA certificate chain
package googlecas

import (
	"bytes"
	"context"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"log/slog"
	"math/big"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"
	"time"

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

// Config represents the Google CAS issuer connector configuration.
type Config struct {
	// Project is the GCP project ID.
	// Required. Set via CERTCTL_GOOGLE_CAS_PROJECT environment variable.
	Project string `json:"project"`

	// Location is the GCP region (e.g., "us-central1").
	// Required. Set via CERTCTL_GOOGLE_CAS_LOCATION environment variable.
	Location string `json:"location"`

	// CAPool is the Certificate Authority pool name.
	// Required. Set via CERTCTL_GOOGLE_CAS_CA_POOL environment variable.
	CAPool string `json:"ca_pool"`

	// Credentials is the path to the service account JSON credentials file.
	// Required. Set via CERTCTL_GOOGLE_CAS_CREDENTIALS environment variable.
	Credentials string `json:"credentials"`

	// TTL is the requested certificate TTL (e.g., "8760h" for 1 year).
	// Default: "8760h". Set via CERTCTL_GOOGLE_CAS_TTL environment variable.
	TTL string `json:"ttl"`

	// BaseURL overrides the Google CAS API base URL (for testing).
	// Default: "https://privateca.googleapis.com/v1".
	BaseURL string `json:"base_url,omitempty"`

	// TokenURL overrides the OAuth2 token endpoint (for testing).
	// Default: "https://oauth2.googleapis.com/token".
	TokenURL string `json:"token_url,omitempty"`
}

// serviceAccountKey represents the relevant fields from a Google service account JSON file.
type serviceAccountKey struct {
	Type        string `json:"type"`
	ProjectID   string `json:"project_id"`
	PrivateKey  string `json:"private_key"`
	ClientEmail string `json:"client_email"`
	TokenURI    string `json:"token_uri"`
}

// cachedToken holds an OAuth2 access token and its expiry.
type cachedToken struct {
	token     string
	expiresAt time.Time
}

// Connector implements the issuer.Connector interface for Google CAS.
type Connector struct {
	config     *Config
	logger     *slog.Logger
	httpClient *http.Client

	// OAuth2 token caching
	mu          sync.Mutex
	tokenCache  *cachedToken
	saKey       *serviceAccountKey
	rsaKey      *rsa.PrivateKey
}

// New creates a new Google CAS connector with the given configuration and logger.
func New(config *Config, logger *slog.Logger) *Connector {
	if config != nil {
		if config.TTL == "" {
			config.TTL = "8760h"
		}
		if config.BaseURL == "" {
			config.BaseURL = "https://privateca.googleapis.com/v1"
		}
		if config.TokenURL == "" {
			config.TokenURL = "https://oauth2.googleapis.com/token"
		}
	}

	return &Connector{
		config: config,
		logger: logger,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// parentPath returns the CAS resource parent path.
func (c *Connector) parentPath() string {
	return fmt.Sprintf("projects/%s/locations/%s/caPools/%s",
		c.config.Project, c.config.Location, c.config.CAPool)
}

// certificateCreateResponse represents the Google CAS create certificate response.
type certificateCreateResponse struct {
	Name                string   `json:"name"`
	PEMCertificate      string   `json:"pemCertificate"`
	PEMCertificateChain []string `json:"pemCertificateChain"`
}

// fetchCACertsResponse represents the Google CAS fetchCaCerts response.
type fetchCACertsResponse struct {
	CACerts []caCertChain `json:"caCerts"`
}

type caCertChain struct {
	Certificates []string `json:"certificates"`
}

// googleAPIError represents a Google API error response.
type googleAPIError struct {
	Error struct {
		Code    int    `json:"code"`
		Message string `json:"message"`
		Status  string `json:"status"`
	} `json:"error"`
}

// ValidateConfig checks that the Google CAS configuration is valid.
// Verifies required fields and that the credentials file is parseable.
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 Google CAS config: %w", err)
	}

	if cfg.Project == "" {
		return fmt.Errorf("Google CAS project is required")
	}
	if cfg.Location == "" {
		return fmt.Errorf("Google CAS location is required")
	}
	if cfg.CAPool == "" {
		return fmt.Errorf("Google CAS CA pool is required")
	}
	if cfg.Credentials == "" {
		return fmt.Errorf("Google CAS credentials path is required")
	}

	// Verify credentials file exists and is valid
	saKey, _, err := loadServiceAccountKey(cfg.Credentials)
	if err != nil {
		return fmt.Errorf("Google CAS credentials invalid: %w", err)
	}

	if saKey.ClientEmail == "" {
		return fmt.Errorf("Google CAS credentials missing client_email")
	}
	if saKey.PrivateKey == "" {
		return fmt.Errorf("Google CAS credentials missing private_key")
	}

	if cfg.TTL == "" {
		cfg.TTL = "8760h"
	}
	if cfg.BaseURL == "" {
		cfg.BaseURL = "https://privateca.googleapis.com/v1"
	}
	if cfg.TokenURL == "" {
		cfg.TokenURL = "https://oauth2.googleapis.com/token"
	}

	c.config = &cfg
	c.logger.Info("Google CAS configuration validated",
		"project", cfg.Project,
		"location", cfg.Location,
		"ca_pool", cfg.CAPool)

	return nil
}

// loadServiceAccountKey reads and parses a service account JSON file.
func loadServiceAccountKey(path string) (*serviceAccountKey, *rsa.PrivateKey, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, nil, fmt.Errorf("cannot read credentials file: %w", err)
	}

	var saKey serviceAccountKey
	if err := json.Unmarshal(data, &saKey); err != nil {
		return nil, nil, fmt.Errorf("cannot parse credentials JSON: %w", err)
	}

	if saKey.PrivateKey == "" {
		return &saKey, nil, nil
	}

	// Parse the RSA private key
	block, _ := pem.Decode([]byte(saKey.PrivateKey))
	if block == nil {
		return nil, nil, fmt.Errorf("cannot decode private key PEM")
	}

	// Try PKCS#8 first, then PKCS#1
	var rsaKey *rsa.PrivateKey
	if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
		var ok bool
		rsaKey, ok = key.(*rsa.PrivateKey)
		if !ok {
			return nil, nil, fmt.Errorf("private key is not RSA")
		}
	} else if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
		rsaKey = key
	} else {
		return nil, nil, fmt.Errorf("cannot parse private key: not PKCS#8 or PKCS#1")
	}

	return &saKey, rsaKey, nil
}

// getAccessToken returns a valid OAuth2 access token, refreshing if needed.
func (c *Connector) getAccessToken(ctx context.Context) (string, error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Return cached token if still valid (5 min buffer)
	if c.tokenCache != nil && time.Now().Add(5*time.Minute).Before(c.tokenCache.expiresAt) {
		return c.tokenCache.token, nil
	}

	// Load credentials if not cached
	if c.saKey == nil || c.rsaKey == nil {
		saKey, rsaKey, err := loadServiceAccountKey(c.config.Credentials)
		if err != nil {
			return "", fmt.Errorf("failed to load credentials: %w", err)
		}
		c.saKey = saKey
		c.rsaKey = rsaKey
	}

	// Build JWT
	now := time.Now()
	header := base64URLEncode([]byte(`{"alg":"RS256","typ":"JWT"}`))

	claims, err := json.Marshal(map[string]interface{}{
		"iss":   c.saKey.ClientEmail,
		"scope": "https://www.googleapis.com/auth/cloud-platform",
		"aud":   c.config.TokenURL,
		"iat":   now.Unix(),
		"exp":   now.Add(time.Hour).Unix(),
	})
	if err != nil {
		return "", fmt.Errorf("failed to marshal JWT claims: %w", err)
	}
	payload := base64URLEncode(claims)

	// Sign
	signingInput := header + "." + payload
	hash := sha256.Sum256([]byte(signingInput))
	sig, err := rsa.SignPKCS1v15(rand.Reader, c.rsaKey, crypto.SHA256, hash[:])
	if err != nil {
		return "", fmt.Errorf("failed to sign JWT: %w", err)
	}

	jwt := signingInput + "." + base64URLEncode(sig)

	// Exchange JWT for access token
	form := url.Values{
		"grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
		"assertion":  {jwt},
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.config.TokenURL,
		strings.NewReader(form.Encode()))
	if err != nil {
		return "", fmt.Errorf("failed to create token request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("token exchange failed: %w", err)
	}
	defer resp.Body.Close()

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

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("token exchange returned status %d: %s", resp.StatusCode, string(body))
	}

	var tokenResp struct {
		AccessToken string `json:"access_token"`
		ExpiresIn   int    `json:"expires_in"`
		TokenType   string `json:"token_type"`
	}
	if err := json.Unmarshal(body, &tokenResp); err != nil {
		return "", fmt.Errorf("failed to parse token response: %w", err)
	}

	if tokenResp.AccessToken == "" {
		return "", fmt.Errorf("empty access token in response")
	}

	// Cache token
	c.tokenCache = &cachedToken{
		token:     tokenResp.AccessToken,
		expiresAt: now.Add(time.Duration(tokenResp.ExpiresIn) * time.Second),
	}

	return tokenResp.AccessToken, nil
}

// doAuthenticatedRequest performs an HTTP request with OAuth2 bearer token.
func (c *Connector) doAuthenticatedRequest(ctx context.Context, method, urlStr string, body interface{}) ([]byte, int, error) {
	token, err := c.getAccessToken(ctx)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to get access token: %w", err)
	}

	var bodyReader io.Reader
	if body != nil {
		bodyBytes, err := json.Marshal(body)
		if err != nil {
			return nil, 0, fmt.Errorf("failed to marshal request body: %w", err)
		}
		bodyReader = bytes.NewReader(bodyBytes)
	}

	req, err := http.NewRequestWithContext(ctx, method, urlStr, bodyReader)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+token)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

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

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

	return respBody, resp.StatusCode, nil
}

// extractAPIError extracts an error message from a Google API error response.
func extractAPIError(body []byte) string {
	var apiErr googleAPIError
	if err := json.Unmarshal(body, &apiErr); err == nil && apiErr.Error.Message != "" {
		return fmt.Sprintf("%s (%s)", apiErr.Error.Message, apiErr.Error.Status)
	}
	return string(body)
}

// IssueCertificate issues a new certificate via Google CAS.
func (c *Connector) IssueCertificate(ctx context.Context, request issuer.IssuanceRequest) (*issuer.IssuanceResult, error) {
	c.logger.Info("processing Google CAS issuance request",
		"common_name", request.CommonName,
		"san_count", len(request.SANs))

	// Convert TTL to seconds string
	ttlDuration, err := time.ParseDuration(c.config.TTL)
	if err != nil {
		return nil, fmt.Errorf("invalid TTL %q: %w", c.config.TTL, err)
	}
	lifetimeSeconds := fmt.Sprintf("%ds", int(ttlDuration.Seconds()))

	// Generate unique certificate ID
	certID := fmt.Sprintf("certctl-%d-%s", time.Now().Unix(), randomHex(4))

	// Build request
	createURL := fmt.Sprintf("%s/%s/certificates?certificateId=%s",
		c.config.BaseURL, c.parentPath(), certID)

	createBody := map[string]interface{}{
		"lifetime": lifetimeSeconds,
		"pemCsr":   request.CSRPEM,
	}

	respBody, statusCode, err := c.doAuthenticatedRequest(ctx, http.MethodPost, createURL, createBody)
	if err != nil {
		return nil, fmt.Errorf("Google CAS create certificate failed: %w", err)
	}

	if statusCode != http.StatusOK {
		return nil, fmt.Errorf("Google CAS create certificate returned status %d: %s",
			statusCode, extractAPIError(respBody))
	}

	// Parse response
	var certResp certificateCreateResponse
	if err := json.Unmarshal(respBody, &certResp); err != nil {
		return nil, fmt.Errorf("failed to parse Google CAS response: %w", err)
	}

	if certResp.PEMCertificate == "" {
		return nil, fmt.Errorf("no certificate in Google CAS response")
	}

	// Parse leaf cert to extract metadata
	block, _ := pem.Decode([]byte(certResp.PEMCertificate))
	if block == nil {
		return nil, fmt.Errorf("failed to decode certificate PEM from Google CAS")
	}

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

	// Build chain PEM
	chainPEM := strings.Join(certResp.PEMCertificateChain, "\n")

	serial := formatSerial(cert.SerialNumber)

	// Store full resource name as OrderID for revocation lookup
	orderID := certResp.Name

	c.logger.Info("Google CAS certificate issued",
		"common_name", request.CommonName,
		"serial", serial,
		"name", certResp.Name,
		"not_after", cert.NotAfter)

	return &issuer.IssuanceResult{
		CertPEM:   certResp.PEMCertificate,
		ChainPEM:  chainPEM,
		Serial:    serial,
		NotBefore: cert.NotBefore,
		NotAfter:  cert.NotAfter,
		OrderID:   orderID,
	}, nil
}

// RenewCertificate renews a certificate by creating a new one.
// For Google CAS, renewal is functionally identical to issuance.
func (c *Connector) RenewCertificate(ctx context.Context, request issuer.RenewalRequest) (*issuer.IssuanceResult, error) {
	c.logger.Info("processing Google CAS 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,
		EKUs:       request.EKUs,
	})
}

// RevokeCertificate revokes a certificate at Google CAS.
// The serial field should contain the full certificate resource name (set as OrderID at issuance).
func (c *Connector) RevokeCertificate(ctx context.Context, request issuer.RevocationRequest) error {
	c.logger.Info("processing Google CAS revocation request", "serial", request.Serial)

	// Determine the certificate resource name.
	// If serial starts with "projects/", it's a full resource name (from OrderID).
	// Otherwise, construct a best-effort path.
	var certName string
	if strings.HasPrefix(request.Serial, "projects/") {
		certName = request.Serial
	} else {
		certName = fmt.Sprintf("%s/certificates/%s", c.parentPath(), request.Serial)
	}

	reason := mapRevocationReason(request.Reason)

	revokeURL := fmt.Sprintf("%s/%s:revoke", c.config.BaseURL, certName)
	revokeBody := map[string]interface{}{
		"reason": reason,
	}

	respBody, statusCode, err := c.doAuthenticatedRequest(ctx, http.MethodPost, revokeURL, revokeBody)
	if err != nil {
		return fmt.Errorf("Google CAS revoke failed: %w", err)
	}

	if statusCode != http.StatusOK {
		return fmt.Errorf("Google CAS revoke returned status %d: %s",
			statusCode, extractAPIError(respBody))
	}

	c.logger.Info("Google CAS certificate revoked", "name", certName, "reason", reason)
	return nil
}

// GetOrderStatus returns the status of a Google CAS order.
// Google CAS 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
}

// GenerateCRL is not supported because Google CAS manages CRL directly.
func (c *Connector) GenerateCRL(ctx context.Context, revokedCerts []issuer.RevokedCertEntry) ([]byte, error) {
	return nil, fmt.Errorf("Google CAS manages CRL directly; not supported via certctl")
}

// SignOCSPResponse is not supported because Google CAS manages OCSP directly.
func (c *Connector) SignOCSPResponse(ctx context.Context, req issuer.OCSPSignRequest) ([]byte, error) {
	return nil, fmt.Errorf("Google CAS manages OCSP directly; not supported via certctl")
}

// GetCACertPEM retrieves the CA certificate chain from Google CAS.
func (c *Connector) GetCACertPEM(ctx context.Context) (string, error) {
	fetchURL := fmt.Sprintf("%s/%s:fetchCaCerts", c.config.BaseURL, c.parentPath())

	respBody, statusCode, err := c.doAuthenticatedRequest(ctx, http.MethodPost, fetchURL, map[string]interface{}{})
	if err != nil {
		return "", fmt.Errorf("Google CAS fetchCaCerts failed: %w", err)
	}

	if statusCode != http.StatusOK {
		return "", fmt.Errorf("Google CAS fetchCaCerts returned status %d: %s",
			statusCode, extractAPIError(respBody))
	}

	var resp fetchCACertsResponse
	if err := json.Unmarshal(respBody, &resp); err != nil {
		return "", fmt.Errorf("failed to parse fetchCaCerts response: %w", err)
	}

	if len(resp.CACerts) == 0 || len(resp.CACerts[0].Certificates) == 0 {
		return "", fmt.Errorf("no CA certificates in response")
	}

	// Join all certificates from the first CA cert chain
	return strings.Join(resp.CACerts[0].Certificates, "\n"), nil
}

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

// mapRevocationReason maps certctl RFC 5280 reason strings to Google CAS enum values.
func mapRevocationReason(reason *string) string {
	if reason == nil {
		return "REVOCATION_REASON_UNSPECIFIED"
	}

	switch strings.ToLower(*reason) {
	case "keycompromise":
		return "KEY_COMPROMISE"
	case "cacompromise":
		return "CERTIFICATE_AUTHORITY_COMPROMISE"
	case "affiliationchanged":
		return "AFFILIATION_CHANGED"
	case "superseded":
		return "SUPERSEDED"
	case "cessationofoperation":
		return "CESSATION_OF_OPERATION"
	case "certificatehold":
		return "CERTIFICATE_HOLD"
	case "privilegewithdrawn":
		return "PRIVILEGE_WITHDRAWN"
	default:
		return "REVOCATION_REASON_UNSPECIFIED"
	}
}

// formatSerial converts a *big.Int serial number to a hex string.
func formatSerial(serial *big.Int) string {
	return serial.Text(16)
}

// randomHex generates n random bytes and returns them as a hex string.
func randomHex(n int) string {
	b := make([]byte, n)
	_, _ = rand.Read(b)
	return fmt.Sprintf("%x", b)
}

// base64URLEncode encodes data using base64url without padding.
func base64URLEncode(data []byte) string {
	return base64.RawURLEncoding.EncodeToString(data)
}

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