package controllers

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/pem"
	"fmt"
	"sync"
	"time"

	pca "github.com/alibabacloud-go/cas-20200630/client"
	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
	sts "github.com/alibabacloud-go/sts-20150401/v2/client"
	"github.com/aliyun/credentials-go/credentials"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"

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

const (
	certIdentifierAnnotation = "alicloud-pca-issuer/certificate-identifier"
	csrFingerprintAnnotation = "alicloud-pca-issuer/csr-fingerprint"

	roleSessionName = "alicloud-pca-issuer"

	// Pending annotation format: "pending:<timestamp>:<cr-uid>"
	pendingPrefix = "pending:"
	// Timeout for pending state - if a pending annotation is older than this,
	// we consider the previous operation failed and allow retry
	pendingTimeout = 5 * time.Minute
)

// calculateCSRFingerprint computes a SHA256 fingerprint of the CSR for deduplication.
// Returns a hex-encoded string prefixed with "sha256:" for clarity.
// Uses full 256 bits to eliminate any collision risk in certificate issuance scenarios.
func calculateCSRFingerprint(csr []byte) string {
	hash := sha256.Sum256(csr)
	return fmt.Sprintf("sha256:%x", hash[:]) // Full 32 bytes (256 bits) for zero collision risk
}

// collection stores provisioners keyed by issuer NamespacedName.
// Note: Provisioners are currently not automatically cleaned up when issuers are deleted.
// This is a known limitation. In practice, the memory footprint is small (one client per issuer),
// and issuers are typically long-lived resources. For explicit cleanup, use DeleteProvisioner.
var collection = new(sync.Map)

// GenericProvisioner abstracts over the Provisioner type for mocking purposes
type GenericProvisioner interface {
	// Create issues a certificate for the given CSR. If the CAS API returns
	// the certificate immediately, certPEM and chainPEM will be non-empty.
	// If issuance is asynchronous, identifier will be non-empty while certPEM
	// and chainPEM can be empty.
	Create(ctx context.Context, csr []byte) (identifier string, certPEM, chainPEM, rootCA []byte, err error)
	// Describe fetches the issued certificate by identifier. It returns the
	// leaf certificate (certPEM), the intermediate chain (chainPEM), and the
	// root CA (rootCA) split from the CA cert chain.
	Describe(ctx context.Context, identifier string) (certPEM, chainPEM, rootCA []byte, err error)
	GetCADetails() (ready bool, err error)
}

type pcaClient interface {
	CreateServerCertificateWithCsr(*pca.CreateServerCertificateWithCsrRequest) (*pca.CreateServerCertificateWithCsrResponse, error)

	DescribeClientCertificate(*pca.DescribeClientCertificateRequest) (*pca.DescribeClientCertificateResponse, error)
	DescribeCACertificate(*pca.DescribeCACertificateRequest) (*pca.DescribeCACertificateResponse, error)
}

// PCAProvisioner contains logic for issuing PCA certificates
type PCAProvisioner struct {
	pcaClient     pcaClient
	caIdentifier  string
	caCertificate *pca.DescribeCACertificateResponseBodyCertificate
	clock         func() time.Time
}

func getDefaultCredentialConfig(region string) (*openapi.Config, error) {
	provider, err := credentials.NewCredential(nil)
	if err != nil {
		return nil, err
	}
	cred, err := provider.GetCredential()
	if err != nil {
		return nil, err
	}
	config := new(openapi.Config).
		SetAccessKeyId(*cred.AccessKeyId).
		SetAccessKeySecret(*cred.AccessKeySecret).
		SetSecurityToken(*cred.SecurityToken).
		SetRegionId(region)
	return config, nil
}

func getStsCredentialConfig(config *openapi.Config, issuerSpec *api.AliCloudPCAIssuerSpec) (*openapi.Config, error) {
	if issuerSpec == nil || issuerSpec.AssumeRole == nil {
		return nil, fmt.Errorf("no assume role provided in issuer spec")
	}
	sessionName := roleSessionName
	if issuerSpec.AssumeRole.RoleSessionName != nil {
		sessionName = *issuerSpec.AssumeRole.RoleSessionName
	}

	// Use STS to assume role and retrieve assumed role credentials
	stsClient, err := sts.NewClient(config)
	if err != nil {
		return nil, fmt.Errorf("failed to create STS client: %w", err)
	}
	req := &sts.AssumeRoleRequest{
		RoleArn:         ptr.To(issuerSpec.AssumeRole.RoleARN),
		ExternalId:      issuerSpec.AssumeRole.ExternalID,
		RoleSessionName: ptr.To(sessionName),
	}
	resp, err := stsClient.AssumeRole(req)
	if err != nil {
		return nil, fmt.Errorf("failed to assume role: %w", err)
	}
	cfg := new(openapi.Config).
		SetAccessKeyId(*resp.Body.Credentials.AccessKeyId).
		SetAccessKeySecret(*resp.Body.Credentials.AccessKeySecret).
		SetSecurityToken(*resp.Body.Credentials.SecurityToken).
		SetRegionId(issuerSpec.Region)
	return cfg, nil
}

func GetConfig(spec *api.AliCloudPCAIssuerSpec) (*openapi.Config, error) {
	config, err := getDefaultCredentialConfig(spec.Region)
	if err != nil {
		return nil, fmt.Errorf("failed to get default credential config: %w", err)
	}
	if spec.AssumeRole == nil {
		return config, nil
	}
	return getStsCredentialConfig(config, spec)
}

func ClearProvisioners() {
	collection.Clear()
}

// DeleteProvisioner removes a provisioner from the cache to prevent memory leaks
func DeleteProvisioner(name types.NamespacedName) {
	collection.Delete(name)
}

// GetProvisioner gets a provisioner that has previously been stored or creates a new one
func GetProvisioner(name types.NamespacedName, spec *api.AliCloudPCAIssuerSpec) (GenericProvisioner, error) {
	value, _ := collection.Load(name)
	p, isProvisioner := value.(GenericProvisioner)
	if isProvisioner {
		return p, nil
	}

	config, err := GetConfig(spec)
	if err != nil {
		return nil, fmt.Errorf("failed to get AliCloud config: %w", err)
	}

	pcaCli, err := pca.NewClient(config)
	if err != nil {
		return nil, fmt.Errorf("failed to create AliCloud PCA client: %w", err)
	}

	provisioner := &PCAProvisioner{
		pcaClient:    pcaCli,
		caIdentifier: spec.CAIdentifier,
		clock:        time.Now,
	}
	collection.Store(name, provisioner)

	return provisioner, nil
}

// Create issues a certificate using the CAS API. It returns either the
// certificate material when available synchronously, or an identifier that
// can be used to poll for the certificate later via Describe.
func (p *PCAProvisioner) Create(ctx context.Context, csr []byte) (identifier string, certPEM, chainPEM, rootCA []byte, err error) {
	block, _ := pem.Decode(csr)
	if block == nil {
		return "", nil, nil, nil, fmt.Errorf("failed to decode CSR")
	}

	if _, err = p.GetCADetails(); err != nil {
		return "", nil, nil, nil, fmt.Errorf("failed to get CA details: %w", err)
	}

	// Note: Quota check is performed in GetCADetails() above.
	// We don't duplicate the check here because:
	// 1. Local checks cannot prevent concurrent quota exhaustion
	// 2. AliCloud API will return explicit error if quota is exhausted during actual call
	// 3. isPermanentAliCloudError will correctly classify quota errors

	req := &pca.CreateServerCertificateWithCsrRequest{
		Csr:              ptr.To(string(csr)),
		ParentIdentifier: ptr.To(p.caIdentifier),
	}
	issueOutput, err := p.pcaClient.CreateServerCertificateWithCsr(req)
	if err != nil {
		return "", nil, nil, nil, fmt.Errorf("failed to issue certificate: %w", err)
	}

	if issueOutput != nil && issueOutput.Body != nil {
		if issueOutput.Body.Identifier != nil {
			identifier = *issueOutput.Body.Identifier
		}
		if issueOutput.Body.X509Certificate != nil {
			certPEM = []byte(*issueOutput.Body.X509Certificate)
		}
		// Note: CertificateChain contains the full CA chain (intermediate + root),
		// NOT the leaf certificate. We return it as chainPEM to be combined with
		// the CA chain from the CA object for the full CAPEM.
		if issueOutput.Body.CertificateChain != nil {
			chainPEM = []byte(*issueOutput.Body.CertificateChain)
		}
	}

	// Get the full CA chain (intermediate + root) for CAPEM
	// The API may return it either in the response CertificateChain or on the CA object
	if len(chainPEM) > 0 {
		// Use the chain from the response if available
		chain, root, splitErr := splitRootCACertificate(chainPEM)
		if splitErr == nil {
			chainPEM = chain
			rootCA = root
		}
	} else if p.caCertificate != nil && p.caCertificate.CaCertChain != nil {
		// Fall back to CA object chain if not in response
		chain, root, splitErr := splitRootCACertificate([]byte(*p.caCertificate.CaCertChain))
		if splitErr == nil {
			chainPEM = chain
			rootCA = root
		}
	}

	return identifier, certPEM, chainPEM, rootCA, nil
}

func (p *PCAProvisioner) Describe(ctx context.Context, certIdentifier string) (certPem, chainIntCAs, rootCA []byte, err error) {
	req := &pca.DescribeClientCertificateRequest{ // API surface may also expose server variant; keep client for compatibility.
		Identifier: ptr.To(certIdentifier),
	}
	output, err := p.pcaClient.DescribeClientCertificate(req)
	if err != nil {
		return nil, nil, nil, err
	}
	if output.Body != nil && output.Body.Certificate != nil && output.Body.Certificate.X509Certificate != nil {
		certPem = []byte(*output.Body.Certificate.X509Certificate + "\n")
	}

	if _, err = p.GetCADetails(); err != nil {
		return nil, nil, nil, fmt.Errorf("failed to get CA details: %w", err)
	}
	if p.caCertificate != nil && p.caCertificate.CaCertChain != nil {
		chainPem := []byte(*p.caCertificate.CaCertChain)
		chainIntCAs, rootCA, err = splitRootCACertificate(chainPem)
		if err != nil {
			return nil, nil, nil, err
		}
	}
	return certPem, chainIntCAs, rootCA, nil
}

func (p *PCAProvisioner) GetCADetails() (ready bool, err error) {
	if p.caCertificate == nil {
		req := &pca.DescribeCACertificateRequest{
			Identifier: ptr.To(p.caIdentifier),
		}
		output, err := p.pcaClient.DescribeCACertificate(req)
		if err != nil {
			return false, fmt.Errorf("failed to describe CA certificate: %w", err)
		}
		if output.Body != nil && output.Body.Certificate != nil {
			p.caCertificate = output.Body.Certificate
		}
	}

	// Validate CA certificate data
	if p.caCertificate == nil || p.caCertificate.Status == nil {
		return false, fmt.Errorf("CA certificate information not available")
	}

	// Check CA status
	if *p.caCertificate.Status != "ISSUE" {
		return false, fmt.Errorf("CA is not in ISSUE status (current: %s)", *p.caCertificate.Status)
	}

	// Check quota - treat as permanent error to prevent infinite retries
	remainingCount := ptr.Deref(p.caCertificate.CertRemainingCount, 0)
	if remainingCount < 1 {
		return false, fmt.Errorf("quota exhausted: no remaining certificates available (current: %d). Please increase quota in AliCloud console", remainingCount)
	}

	return true, nil
}

func splitRootCACertificate(caCertChainPem []byte) ([]byte, []byte, error) {
	var caChainCerts []byte
	var rootCACert []byte
	for {
		block, rest := pem.Decode(caCertChainPem)
		if block == nil || block.Type != "CERTIFICATE" {
			return nil, nil, fmt.Errorf("failed to read certificate")
		}
		var encBuf bytes.Buffer
		if err := pem.Encode(&encBuf, block); err != nil {
			return nil, nil, err
		}
		if len(rest) > 0 {
			caChainCerts = append(caChainCerts, encBuf.Bytes()...)
			caCertChainPem = rest
		} else {
			rootCACert = append(rootCACert, encBuf.Bytes()...)
			break
		}
	}
	return caChainCerts, rootCACert, nil
}
