package controllers

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	issuerapi "github.com/cert-manager/issuer-lib/api/v1alpha1"
	"github.com/cert-manager/issuer-lib/controllers"
	libsigner "github.com/cert-manager/issuer-lib/controllers/signer"
	"github.com/go-logr/logr"
	k8stypes "k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	api "github.com/tidbcloud/alicloud-pca-issuer/pkg/api/v1alpha1"
)

type Issuer struct {
	ClusterResourceNamespace string

	Log    logr.Logger
	client client.Client
}

// +kubebuilder:rbac:groups=alicloudpca.cert-manager.io,resources=alicloudpcaclusterissuers;alicloudpcaissuers,verbs=create;delete;get;list;watch;update;patch
// +kubebuilder:rbac:groups=alicloudpca.cert-manager.io,resources=alicloudpcaclusterissuers/status;alicloudpcaissuers/status,verbs=get;patch;update
// +kubebuilder:rbac:groups="coordination.k8s.io",resources=leases,verbs=get;list;watch;create;patch;update;delete
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=cert-manager.io,resources=signers,verbs=approve,resourceNames=alicloudpcaissuers.alicloudpca.cert-manager.io/*;alicloudpcaclusterissuers.alicloudpca.cert-manager.io/*
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificaterequests,verbs=get;list;update;watch;patch
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificaterequests/status,verbs=get;patch;update
// +kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests,verbs=get;list;watch
// +kubebuilder:rbac:groups=certificates.k8s.io,resources=certificatesigningrequests/status,verbs=patch

func (issuer *Issuer) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
	issuer.client = mgr.GetClient()

	return (&controllers.CombinedController{
		IssuerTypes:        []issuerapi.Issuer{&api.AliCloudPCAIssuer{}},
		ClusterIssuerTypes: []issuerapi.Issuer{&api.AliCloudPCAClusterIssuer{}},
		FieldOwner:         "alicloud-pca-issuer.cert-manager.io",
		MaxRetryDuration:   2 * time.Minute,
		Sign:               issuer.Sign,
		Check:              issuer.Check,
		EventRecorder:      mgr.GetEventRecorderFor("alicloud-pca-issuer.cert-manager.io"),
	}).SetupWithManager(ctx, mgr)
}

func (issuer *Issuer) getIssuerDetails(issuerObject issuerapi.Issuer) (*api.AliCloudPCAIssuerSpec, string, error) {
	switch t := issuerObject.(type) {
	case *api.AliCloudPCAIssuer:
		return &t.Spec, issuerObject.GetNamespace(), nil
	case *api.AliCloudPCAClusterIssuer:
		return &t.Spec, issuer.ClusterResourceNamespace, nil
	default:
		return nil, "", libsigner.PermanentError{Err: fmt.Errorf("unexpected issuer type: %T", issuerObject)}
	}
}

// Check verifies the AliCloud PCA is reachable and in a healthy state.
func (issuer *Issuer) Check(ctx context.Context, issuerObject issuerapi.Issuer) error {
	log := issuer.Log.WithValues(
		"issuer", issuerObject.GetName(),
		"namespace", issuerObject.GetNamespace(),
		"kind", fmt.Sprintf("%T", issuerObject),
	)
	log.V(1).Info("Starting health check for issuer")

	spec, namespace, err := issuer.getIssuerDetails(issuerObject)
	if err != nil {
		log.Error(err, "Failed to get issuer details")
		return err
	}

	// Build/reuse provisioner and check CA status
	key := client.ObjectKey{Namespace: namespace, Name: issuerObject.GetName()}
	log.V(2).Info("Initializing provisioner", "identifier", spec.CAIdentifier, "region", spec.Region)

	p, err := GetProvisioner(key, spec)
	if err != nil {
		// Provisioner init failure is likely a transient credential/config issue
		log.Error(err, "Provisioner initialization failed. Please verify your AliCloud credentials and region configuration")
		return fmt.Errorf("provisioner init failed: please check your AliCloud credentials and region configuration: %w", err)
	}

	log.V(2).Info("Checking CA details")
	ready, err := p.GetCADetails()
	if err != nil {
		if isPermanentAliCloudError(err) {
			log.Error(err, "Permanent error during health check. Please verify CA identifier and permissions")
			return libsigner.PermanentError{Err: err}
		}
		log.Error(err, "Health check failed, will retry")
		return fmt.Errorf("healthcheck failed: %w", err)
	}
	if !ready {
		log.Info("CA is not ready yet, will retry")
		return fmt.Errorf("issuer CA not ready yet")
	}

	log.Info("Health check passed successfully")
	return nil
}

// isPermanentAliCloudError classifies non-recoverable errors such as
// permission/parameter/not-found into PermanentError.
func isPermanentAliCloudError(err error) bool {
	if err == nil {
		return false
	}
	msg := strings.ToLower(err.Error())
	// Common non-recoverable error keywords
	permanent := []string{
		"forbidden", "accessdenied", "access denied", "invalidparameter", "parameterinvalid",
		"nosuchentity", "notfound", "invalidaccesskeyid", "authfailure", "signaturedoesnotmatch",
		"unsupportedregion", "invalidregion", "invalid arn", "malformed", "permission", "unauthorized",
		"quota exhausted", "no remaining certificates", // Quota errors are permanent - require manual intervention
	}
	for _, p := range permanent {
		if strings.Contains(msg, p) {
			return true
		}
	}
	return false
}

// Sign performs AliCloud PCA signing with async support via identifier annotation.
// Flow (idempotent):
//  1. If annotation has a real identifier, poll for certificate.
//  2. If annotation has "pending" marker, check timeout and wait or retry.
//  3. If no annotation, write "pending" marker first, then create certificate.
//     This ensures we don't create duplicate certificates even if patching fails.
func (issuer *Issuer) Sign(ctx context.Context, cr libsigner.CertificateRequestObject, issuerObject issuerapi.Issuer) (libsigner.PEMBundle, error) {
	log := issuer.Log.WithValues(
		"certificaterequest", cr.GetName(),
		"namespace", cr.GetNamespace(),
		"issuer", issuerObject.GetName(),
		"issuerKind", fmt.Sprintf("%T", issuerObject),
	)
	log.Info("Starting certificate signing process")

	spec, namespace, err := issuer.getIssuerDetails(issuerObject)
	if err != nil {
		log.Error(err, "Failed to get issuer details")
		return libsigner.PEMBundle{}, libsigner.IssuerError{Err: err}
	}

	key := client.ObjectKey{Namespace: namespace, Name: issuerObject.GetName()}
	log.V(2).Info("Initializing provisioner for signing")

	p, err := GetProvisioner(key, spec)
	if err != nil {
		log.Error(err, "Provisioner initialization failed during signing. Please verify your AliCloud credentials and region configuration")
		return libsigner.PEMBundle{}, libsigner.IssuerError{Err: fmt.Errorf("provisioner init failed: please check your AliCloud credentials and region configuration: %w", err)}
	}

	// Calculate CSR fingerprint for deduplication
	// This must be done early to check if we've already processed this exact CSR
	details, err := cr.GetCertificateDetails()
	if err != nil {
		log.Error(err, "Failed to get certificate details from CertificateRequest")
		return libsigner.PEMBundle{}, err
	}
	currentFingerprint := calculateCSRFingerprint(details.CSR)
	log.V(2).Info("Computed CSR fingerprint", "fingerprint", currentFingerprint)

	// Step 1: Check if annotation exists
	annotationValue, hasAnnotation := cr.GetAnnotations()[certIdentifierAnnotation]
	storedFingerprint := cr.GetAnnotations()[csrFingerprintAnnotation]

	// CSR deduplication check - prevents issuing duplicate certificates for the same CSR
	if hasAnnotation && storedFingerprint != "" {
		if storedFingerprint != currentFingerprint {
			// CSR has changed - this is a new certificate request
			log.Info("CSR has changed, clearing old annotations and starting fresh",
				"oldFingerprint", storedFingerprint,
				"newFingerprint", currentFingerprint)

			// Explicitly clear old annotations from the API server
			clearAnnotations := map[string]string{
				certIdentifierAnnotation: "",
				csrFingerprintAnnotation: "",
			}
			if err := issuer.patchCertificateRequestAnnotation(ctx, cr.GetNamespace(), cr.GetName(), clearAnnotations); err != nil {
				log.Error(err, "Failed to clear stale annotations")
				return libsigner.PEMBundle{}, fmt.Errorf("failed to clear stale annotations: %w", err)
			}

			// Clear local variables to trigger fresh processing
			annotationValue = ""
			hasAnnotation = false
		} else {
			// CSR matches - this is a duplicate/retry request
			log.V(1).Info("CSR fingerprint matches stored value, checking existing certificate status",
				"fingerprint", currentFingerprint)
		}
	}

	if hasAnnotation && annotationValue != "" {
		// Step 1a: Check if it's a "pending" marker
		if strings.HasPrefix(annotationValue, pendingPrefix) {
			log.V(1).Info("Found pending marker, checking timeout", "annotation", annotationValue)
			if isPendingTimeout(annotationValue) {
				log.Info("Pending marker has timed out, clearing state for retry")

				// Clear timed-out annotations to allow retry without duplicate prevention
				// This is safe because the timeout indicates the previous attempt likely failed
				clearAnnotations := map[string]string{
					certIdentifierAnnotation: "",
					csrFingerprintAnnotation: "",
				}
				if err := issuer.patchCertificateRequestAnnotation(ctx, cr.GetNamespace(), cr.GetName(), clearAnnotations); err != nil {
					log.Error(err, "Failed to clear timed-out annotations, will retry")
				}

				return libsigner.PEMBundle{}, libsigner.PendingError{
					Err:          fmt.Errorf("previous pending request timed out, cleared state for retry"),
					RequeueAfter: 2 * time.Second,
				}
			}
			log.V(2).Info("Pending marker is still valid, waiting")
			return libsigner.PEMBundle{}, libsigner.PendingError{
				Err:          fmt.Errorf("certificate creation in progress"),
				RequeueAfter: 5 * time.Second,
			}
		}

		// Step 1b: It's a real identifier, query the certificate
		log.V(1).Info("Checking status of previously created certificate", "identifier", annotationValue)
		cert, chain, root, derr := p.Describe(ctx, annotationValue)
		if derr != nil || len(cert) == 0 {
			log.V(2).Info("Certificate not ready yet, will retry", "identifier", annotationValue, "error", derr)
			return libsigner.PEMBundle{}, libsigner.PendingError{
				Err:          fmt.Errorf("waiting for AliCloud certificate: %v", derr),
				RequeueAfter: 5 * time.Second,
			}
		}
		log.Info("Certificate retrieved successfully", "identifier", annotationValue, "certSize", len(cert), "chainSize", len(chain), "rootSize", len(root))
		// Build the complete certificate chain for TLS usage:
		// - ChainPEM must contain leaf certificate + intermediate chain (without root)
		// - CAPEM contains only the root CA certificate
		// This matches cert-manager's Secret format where:
		//   tls.crt = leaf + intermediates
		//   ca.crt = root CA
		chainWithCert := append(cert, chain...)
		return libsigner.PEMBundle{
			ChainPEM: chainWithCert, // leaf + intermediate chain
			CAPEM:    root,          // root CA only
		}, nil
	}

	// Step 2: No annotation exists, need to create certificate
	log.V(2).Info("Checking CA details")
	ready, err := p.GetCADetails()
	if err != nil {
		if isPermanentAliCloudError(err) {
			log.Error(err, "Permanent error during health check. Please verify CA identifier and permissions")
			return libsigner.PEMBundle{}, libsigner.PermanentError{Err: err}
		}
		log.Error(err, "Health check failed, will retry")
		return libsigner.PEMBundle{}, fmt.Errorf("healthcheck failed: %w", err)
	}
	if !ready {
		log.Info("CA is not ready, will retry")
		return libsigner.PEMBundle{}, fmt.Errorf("issuer CA not ready")
	}

	// CRITICAL: Write "pending" marker AND CSR fingerprint BEFORE calling Create to ensure idempotency
	log.V(1).Info("No annotation found, creating pending marker and CSR fingerprint before certificate creation")
	crUID := string(cr.GetUID())
	pendingValue := createPendingAnnotation(crUID)

	// Patch both annotations atomically to establish creation lock and track CSR
	annotations := map[string]string{
		certIdentifierAnnotation: pendingValue,
		csrFingerprintAnnotation: currentFingerprint,
	}
	if err := issuer.patchCertificateRequestAnnotation(ctx, cr.GetNamespace(), cr.GetName(), annotations); err != nil {
		log.Error(err, "Failed to write pending marker and CSR fingerprint, will retry")
		// Return regular error to trigger retry - this is safe as we haven't called Create yet
		return libsigner.PEMBundle{}, fmt.Errorf("failed to acquire creation lock: %w", err)
	}
	log.V(1).Info("Successfully wrote pending marker and CSR fingerprint, proceeding with certificate creation",
		"fingerprint", currentFingerprint)

	// Step 3: Now safe to create certificate - pending marker with fingerprint is persisted
	log.V(2).Info("Sending certificate creation request to AliCloud PCA", "csrSize", len(details.CSR))
	id, cert, chain, root, cerr := p.Create(ctx, details.CSR)
	if cerr != nil {
		if isPermanentAliCloudError(cerr) {
			log.Error(cerr, "Permanent error during certificate creation. Please verify CA configuration and permissions")
			return libsigner.PEMBundle{}, libsigner.PermanentError{Err: cerr}
		}
		log.Error(cerr, "Certificate creation failed, pending marker remains for retry")
		// Don't clear pending marker - timeout mechanism will handle cleanup
		return libsigner.PEMBundle{}, fmt.Errorf("create certificate failed: %w", cerr)
	}

	// Step 4: Update annotation with real identifier (preserve CSR fingerprint)
	if id != "" {
		log.V(1).Info("Certificate created, updating annotation with identifier", "identifier", id)
		annos := map[string]string{
			certIdentifierAnnotation: id,
			csrFingerprintAnnotation: currentFingerprint,
		}
		if err := issuer.patchCertificateRequestAnnotation(ctx, cr.GetNamespace(), cr.GetName(), annos); err != nil {
			log.Error(err, "Failed to update identifier annotation")
			// For synchronous issuance (cert already available), annotation update failure means state inconsistency.
			// We must return PendingError to retry and maintain state consistency.
			// For asynchronous issuance, the code below will return PendingError anyway.
			if len(cert) > 0 {
				return libsigner.PEMBundle{}, libsigner.PendingError{
					Err:          fmt.Errorf("certificate issued but failed to update tracking annotation: %w", err),
					RequeueAfter: 2 * time.Second,
				}
			}
			// Asynchronous case: continue to return PendingError below
		} else {
			log.V(2).Info("Identifier annotation updated successfully")
		}
	}

	// Step 5: Handle synchronous vs asynchronous certificate issuance
	if len(cert) > 0 {
		log.Info("Certificate issued successfully", "identifier", id, "certSize", len(cert), "chainSize", len(chain), "rootSize", len(root))
		// Build the complete certificate chain for TLS usage:
		// - ChainPEM must contain leaf certificate + intermediate chain (without root)
		// - CAPEM contains only the root CA certificate
		// This matches cert-manager's Secret format where:
		//   tls.crt = leaf + intermediates
		//   ca.crt = root CA
		chainWithCert := append(cert, chain...)
		return libsigner.PEMBundle{
			ChainPEM: chainWithCert, // leaf + intermediate chain
			CAPEM:    root,          // root CA only
		}, nil
	}

	log.V(1).Info("Waiting for asynchronous certificate issuance", "identifier", id)
	return libsigner.PEMBundle{}, libsigner.PendingError{
		Err:          fmt.Errorf("waiting for AliCloud certificate issuance"),
		RequeueAfter: 5 * time.Second,
	}
}

// patchCertificateRequestAnnotation patches annotations on CertificateRequest using a MergePatch.
func (issuer *Issuer) patchCertificateRequestAnnotation(ctx context.Context, namespace, name string, annotations map[string]string) error {
	cr := &cmapi.CertificateRequest{}
	cr.Namespace = namespace
	cr.Name = name
	body := map[string]any{"metadata": map[string]any{"annotations": annotations}}
	b, _ := json.Marshal(body)
	return issuer.client.Patch(ctx, cr, client.RawPatch(k8stypes.MergePatchType, b))
}

// parsePendingAnnotation parses a pending annotation value.
// Format: "pending|<rfc3339-timestamp>|<cr-uid>" (using | as separator to avoid conflict with : in RFC3339)
// Returns timestamp, uid, and ok=true if parsing succeeds.
func parsePendingAnnotation(value string) (timestamp int64, uid string, ok bool) {
	if !strings.HasPrefix(value, pendingPrefix) {
		return 0, "", false
	}
	// Remove the "pending" prefix and split by |
	remaining := strings.TrimPrefix(value, pendingPrefix)
	parts := strings.Split(remaining, "|")
	if len(parts) != 2 {
		return 0, "", false
	}
	ts, err := time.Parse(time.RFC3339, parts[0])
	if err != nil {
		return 0, "", false
	}
	return ts.Unix(), parts[1], true
}

// isPendingTimeout checks if a pending annotation has exceeded the timeout duration.
func isPendingTimeout(value string) bool {
	ts, _, ok := parsePendingAnnotation(value)
	if !ok {
		return false
	}
	return time.Since(time.Unix(ts, 0)) > pendingTimeout
}

// createPendingAnnotation creates a pending annotation value with current timestamp.
func createPendingAnnotation(crUID string) string {
	return fmt.Sprintf("%s%s|%s", pendingPrefix, time.Now().Format(time.RFC3339), crUID)
}
