// Copyright (c) 2026 Alibaba Group and its affiliates

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//     http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tdx

import (
	"bufio"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
)

// CollateralProviderInterface defines the interface for providing collateral data
type CollateralProviderInterface interface {
	GetCollateral(quote *TdxEcdsaQuote) (*Collateral, error)
}

// CollateralCacheEntry represents a cached collateral entry with metadata
type CollateralCacheEntry struct {
	Collateral     *Collateral
	RetrievedAt    time.Time
	ExpiresAt      time.Time
	LastAccessedAt time.Time // For LRU eviction
}

// CollateralProvider provides collateral data for quote verification
// Integrates DCAP client functionality with caching and retry mechanisms
type CollateralProvider struct {
	dcapServerURL string
	httpClient    *http.Client
	mutex         sync.RWMutex

	// Caching mechanism
	cache        map[string]*CollateralCacheEntry // Key: cache key hash
	cacheTTL     time.Duration                    // Default cache TTL
	maxCacheSize int                              // Maximum cache entries

	// Retry mechanism
	// Reference: Intel DCAP QVL QCNL qcnl_config.h retry_times/retry_delay
	retryTimes int           // Maximum retry attempts for HTTP requests
	retryDelay time.Duration // Delay between retries
}

// NewCollateralProvider creates a new CollateralProvider instance
func NewCollateralProvider(dcapServerURL string) *CollateralProvider {
	return &CollateralProvider{
		dcapServerURL: dcapServerURL,
		httpClient:    &http.Client{Timeout: 30 * time.Second},
		cache:         make(map[string]*CollateralCacheEntry),
		cacheTTL:      24 * time.Hour,  // Default 24 hours cache (TCB Info/QE Identity update frequency)
		maxCacheSize:  100,             // Maximum 100 cached entries (sufficient for most deployments)
		retryTimes:    3,               // Default 3 retry attempts
		retryDelay:    1 * time.Second, // Default 1 second between retries
	}
}

// SetRetryPolicy sets the retry policy for HTTP requests
func (cp *CollateralProvider) SetRetryPolicy(retryTimes int, retryDelay time.Duration) {
	cp.mutex.Lock()
	defer cp.mutex.Unlock()
	cp.retryTimes = retryTimes
	cp.retryDelay = retryDelay
}

// SetCacheTTL sets the cache TTL for collateral entries
func (cp *CollateralProvider) SetCacheTTL(ttl time.Duration) {
	cp.mutex.Lock()
	defer cp.mutex.Unlock()
	cp.cacheTTL = ttl
}

// SetMaxCacheSize sets the maximum number of cache entries
func (cp *CollateralProvider) SetMaxCacheSize(maxSize int) {
	cp.mutex.Lock()
	defer cp.mutex.Unlock()
	cp.maxCacheSize = maxSize
}

// httpGetWithRetry performs HTTP GET request with retry mechanism
// Retries on network errors and 5xx server errors
func (cp *CollateralProvider) httpGetWithRetry(url string) (*http.Response, error) {
	var lastErr error
	var lastResp *http.Response

	// Get retry configuration (read-lock)
	cp.mutex.RLock()
	retryTimes := cp.retryTimes
	retryDelay := cp.retryDelay
	cp.mutex.RUnlock()

	// Attempt request with retries
	for attempt := 0; attempt <= retryTimes; attempt++ {
		if attempt > 0 {
			// Wait before retry (except first attempt)
			time.Sleep(retryDelay)
		}

		resp, err := cp.httpClient.Get(url)
		if err != nil {
			// Network error - store and retry
			lastErr = err
			if attempt == retryTimes {
				break
			}
			continue
		}

		// Check if response is successful (2xx) or should be retried (5xx)
		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			// Success - return immediately
			return resp, nil
		}

		// Store response for potential return
		if lastResp != nil {
			lastResp.Body.Close()
		}
		lastResp = resp

		// Retry on 5xx server errors, don't retry on 4xx client errors
		if resp.StatusCode >= 500 && resp.StatusCode < 600 {
			lastErr = fmt.Errorf("server error: status %d", resp.StatusCode)
			if attempt == retryTimes {
				break
			}
			continue
		}

		// 4xx or other non-5xx error - don't retry, return immediately
		return resp, nil
	}

	// All retries failed
	if lastResp != nil {
		// Return last response if we have one
		return lastResp, nil
	}
	return nil, fmt.Errorf("HTTP request failed after %d retries: %w", retryTimes, lastErr)
}

// generateCacheKey generates a cache key for collateral based on FMSPC and TEE type
// Unlike QVL QCNL which caches each HTTP request independently (sha256(URL)),
// we cache the entire collateral bundle per FMSPC+TEEType combination.
// This is more suitable for our use case where a complete collateral set is always needed together.
// Reference: QVL QCNL certification_provider.cpp:114,191 uses sha256(URL) per request
func (cp *CollateralProvider) generateCacheKey(quote *TdxEcdsaQuote) (string, error) {
	// Extract certificate chain to get PCK cert using utility function
	certChain, err := ParseCertificateChainFromQuote(quote)
	if err != nil {
		return "", fmt.Errorf("failed to parse certificate chain: %w", err)
	}

	// Extract FMSPC from PCK certificate
	fmspc, _, err := ExtractSgxExtensions(certChain.PCKCert)
	if err != nil {
		return "", fmt.Errorf("failed to extract FMSPC: %w", err)
	}

	fmspcHex := hex.EncodeToString(fmspc)

	// Generate cache key based on FMSPC and TEE type only
	// All quotes from the same platform (same FMSPC) share the same collateral (TCB Info, QE Identity, CRLs)
	// Individual PCK certificates differ per machine but reference the same platform collateral
	keyStr := fmt.Sprintf("fmspc:%s|tee:%d", fmspcHex, quote.Header.TeeType)

	return keyStr, nil
}

// getCachedCollateral retrieves collateral from cache if valid
// Updates LastAccessedAt for LRU tracking
func (cp *CollateralProvider) getCachedCollateral(cacheKey string) *Collateral {
	cp.mutex.Lock() // Upgrade to write lock to update LastAccessedAt
	defer cp.mutex.Unlock()

	entry, exists := cp.cache[cacheKey]
	if !exists {
		return nil
	}

	// Check if cache entry is still valid
	if time.Now().After(entry.ExpiresAt) {
		// Entry expired, will be cleaned up later
		return nil
	}

	// Update last accessed time for LRU
	entry.LastAccessedAt = time.Now()

	return entry.Collateral
}

// setCachedCollateral stores collateral in cache with configured TTL
func (cp *CollateralProvider) setCachedCollateral(cacheKey string, collateral *Collateral) {
	cp.mutex.Lock()
	defer cp.mutex.Unlock()

	// If cache is at max size, evict using LRU
	if len(cp.cache) >= cp.maxCacheSize {
		// First try to clean up expired entries
		cp.cleanupExpiredEntries()

		// If still at max after cleanup, evict LRU entry
		if len(cp.cache) >= cp.maxCacheSize {
			cp.evictLRU()
		}
	}

	// Set expiration time based on configured cache TTL
	now := time.Now()
	expiresAt := now.Add(cp.cacheTTL)

	// Store in cache with initial access time
	cp.cache[cacheKey] = &CollateralCacheEntry{
		Collateral:     collateral,
		RetrievedAt:    collateral.RetrievedAt,
		ExpiresAt:      expiresAt,
		LastAccessedAt: now,
	}
}

// cleanupExpiredEntries removes expired cache entries (caller must hold write lock)
func (cp *CollateralProvider) cleanupExpiredEntries() {
	now := time.Now()
	for key, entry := range cp.cache {
		if now.After(entry.ExpiresAt) {
			delete(cp.cache, key)
		}
	}
}

// evictLRU removes the least recently used cache entry (caller must hold write lock)
// Reference: Intel DCAP QVL QCNL local_cache.cpp LRU eviction strategy
func (cp *CollateralProvider) evictLRU() {
	if len(cp.cache) == 0 {
		return
	}

	// Find the entry with the oldest LastAccessedAt time
	var oldestKey string
	var oldestTime time.Time
	first := true

	for key, entry := range cp.cache {
		if first || entry.LastAccessedAt.Before(oldestTime) {
			oldestKey = key
			oldestTime = entry.LastAccessedAt
			first = false
		}
	}

	// Remove the LRU entry
	if oldestKey != "" {
		delete(cp.cache, oldestKey)
	}
}

// GetCollateral retrieves verification collateral for the given quote
func (cp *CollateralProvider) GetCollateral(quote *TdxEcdsaQuote) (*Collateral, error) {
	// Generate cache key based on quote characteristics
	cacheKey, err := cp.generateCacheKey(quote)
	if err != nil {
		return nil, fmt.Errorf("failed to generate cache key: %w", err)
	}

	// Try to get from cache first
	if cached := cp.getCachedCollateral(cacheKey); cached != nil {
		return cached, nil
	}

	// Cache miss - fetch fresh collateral
	collateral, err := cp.fetchFreshCollateral(quote)
	if err != nil {
		return nil, err
	}

	// Store in cache for future use with configured TTL
	cp.setCachedCollateral(cacheKey, collateral)

	return collateral, nil
}

// fetchFreshCollateral retrieves fresh collateral from PCCS server
func (cp *CollateralProvider) fetchFreshCollateral(quote *TdxEcdsaQuote) (*Collateral, error) {
	// Extract FMSPC from PCK certificate in quote using utility function
	certChain, err := ParseCertificateChainFromQuote(quote)
	if err != nil {
		return nil, fmt.Errorf("failed to parse certificate chain from quote: %w", err)
	}

	fmspc, _, err := ExtractSgxExtensions(certChain.PCKCert)
	if err != nil {
		return nil, fmt.Errorf("failed to extract FMSPC from PCK certificate: %w", err)
	}

	// Get all collateral components directly
	collateral := &Collateral{
		TeeType:     quote.Header.TeeType,
		RetrievedAt: time.Now(),
	}

	// Get QCNL version
	version, err := cp.getQcnlVersion()
	if err != nil {
		return nil, fmt.Errorf("failed to get QCNL version: %w", err)
	}
	collateral.Version = uint32(version)

	// Get PCK CRL - extract CA from certificate chain
	// Based on Intel DCAP QVL: AttestationLibrary/src/AttestationParsers/src/ParserUtils.cpp
	var ca []byte
	if certChain.TopMostCert == nil {
		return nil, fmt.Errorf("no top-most certificate found in chain")
	}

	// Determine CA from top-most certificate issuer CommonName
	issuerCN := certChain.TopMostCert.Issuer.CommonName
	if strings.Contains(issuerCN, "Processor") {
		ca = []byte("processor")
	} else if strings.Contains(issuerCN, "Platform") {
		ca = []byte("platform")
	} else {
		return nil, fmt.Errorf("invalid ca name in certificate: %s", issuerCN)
	}

	pckCrl, pckCrlChain, err := cp.getPckCrl(ca)
	if err != nil {
		return nil, fmt.Errorf("failed to get PCK CRL: %w", err)
	}
	collateral.PckCrl = []byte(pckCrl)
	collateral.PckCrlIssuerChain = []byte(pckCrlChain)

	// Get Root CA CRL
	rootCaCrl, err := cp.getRootCaCrl()
	if err != nil {
		return nil, fmt.Errorf("failed to get root CA CRL: %w", err)
	}
	collateral.RootCaCrl = []byte(rootCaCrl)

	// Get TCB info
	tcbInfo, tcbInfoChain, err := cp.getTcbInfo(fmspc, quote.Header.TeeType)
	if err != nil {
		return nil, fmt.Errorf("failed to get TCB info: %w", err)
	}
	collateral.TcbInfo = []byte(tcbInfo)
	collateral.TcbInfoIssuerChain = []byte(tcbInfoChain)

	// Get QE identity
	qeIdentity, qeIdentityChain, err := cp.getQeIdentity(quote.Header.TeeType)
	if err != nil {
		return nil, fmt.Errorf("failed to get QE identity: %w", err)
	}
	collateral.QeIdentity = []byte(qeIdentity)
	collateral.QeIdentityIssuerChain = []byte(qeIdentityChain)

	return collateral, nil
}

// getPccsURL gets the PCCS URL from configuration or uses the provided URL
func (cp *CollateralProvider) getPccsURL() string {
	if cp.dcapServerURL != "" {
		return cp.dcapServerURL + "/sgx/certification/v4/"
	}

	// Try to read from SGX default QCNL config file
	qcnlConf, err := os.Open("/etc/sgx_default_qcnl.conf")
	if err == nil {
		defer qcnlConf.Close()
		scanner := bufio.NewScanner(qcnlConf)
		for scanner.Scan() {
			if strings.HasPrefix(scanner.Text(), "PCCS_URL=") {
				return strings.TrimPrefix(scanner.Text(), "PCCS_URL=")
			}
		}
	}

	// Default PCCS URL
	return "https://sgx-dcap-server.cn-hangzhou.aliyuncs.com/sgx/certification/v4/"
}

// getPckCrl fetches PCK CRL from PCCS server
func (cp *CollateralProvider) getPckCrl(ca []byte) (string, string, error) {

	requestURL := cp.getPccsURL() + fmt.Sprintf("pckcrl?ca=%s", strings.TrimRight(string(ca), "\x00"))

	resp, err := cp.httpGetWithRetry(requestURL)
	if err != nil {
		return "", "", fmt.Errorf("failed to fetch PCK CRL: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", "", fmt.Errorf("PCCS server returned status %d for PCK CRL", resp.StatusCode)
	}

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

	// Get issuer chain from header
	chain, err := url.QueryUnescape(resp.Header.Get("SGX-PCK-CRL-Issuer-Chain"))
	if err != nil {
		return "", "", fmt.Errorf("failed to decode issuer chain: %w", err)
	}

	if len(chain) == 0 {
		return "", "", fmt.Errorf("not found SGX-PCK-CRL-Issuer-Chain")
	}

	return string(body), chain, nil
}

// getRootCaCrl fetches Root CA CRL from PCCS server

func (cp *CollateralProvider) getRootCaCrl() (string, error) {
	requestURL := cp.getPccsURL() + "rootcacrl"
	resp, err := cp.httpGetWithRetry(requestURL)
	if err != nil {
		return "", fmt.Errorf("failed to fetch root CA CRL: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("PCCS server returned status %d for root CA CRL", resp.StatusCode)
	}

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

	return string(body), nil
}

// getTcbInfo fetches TCB info from PCCS server
func (cp *CollateralProvider) getTcbInfo(fmspc []byte, teeType uint32) (string, string, error) {
	baseURL := cp.getPccsURL()

	// Determine sub URL and cert chain header name based on TEE type
	var subURL, certChainName string
	switch teeType {
	case TEE_TYPE_SGX:
		subURL = "/sgx/"
		certChainName = "SGX-TCB-Info-Issuer-Chain"
	case TEE_TYPE_TDX:
		subURL = "/tdx/"
		certChainName = "TCB-Info-Issuer-Chain"
	default:
		return "", "", fmt.Errorf("unknown TEE type: %d", teeType)
	}

	requestURL := strings.ReplaceAll(baseURL, "/sgx/", subURL) + fmt.Sprintf("tcb?fmspc=%s", hex.EncodeToString(fmspc))

	resp, err := cp.httpGetWithRetry(requestURL)
	if err != nil {
		return "", "", fmt.Errorf("failed to fetch TCB info: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", "", fmt.Errorf("PCCS server returned status %d for TCB info", resp.StatusCode)
	}

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

	chain, err := url.QueryUnescape(resp.Header.Get(certChainName))
	if err != nil {
		return "", "", fmt.Errorf("failed to decode TCB info issuer chain: %w", err)
	}

	if len(chain) == 0 {
		return "", "", fmt.Errorf("missing %s header", certChainName)
	}

	return string(body), chain, nil
}

// getQeIdentity fetches QE identity from PCCS server
func (cp *CollateralProvider) getQeIdentity(teeType uint32) (string, string, error) {
	baseURL := cp.getPccsURL()

	// Determine sub URL based on TEE type
	var subURL string
	switch teeType {
	case TEE_TYPE_SGX:
		subURL = "/sgx/"
	case TEE_TYPE_TDX:
		subURL = "/tdx/"
	default:
		return "", "", fmt.Errorf("unknown TEE type: %d", teeType)
	}

	requestURL := strings.ReplaceAll(baseURL, "/sgx/", subURL) + "qe/identity"

	resp, err := cp.httpGetWithRetry(requestURL)
	if err != nil {
		return "", "", fmt.Errorf("failed to fetch QE identity: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", "", fmt.Errorf("PCCS server returned status %d for QE identity", resp.StatusCode)
	}

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

	chain, err := url.QueryUnescape(resp.Header.Get("SGX-Enclave-Identity-Issuer-Chain"))
	if err != nil {
		return "", "", fmt.Errorf("failed to decode QE identity issuer chain: %w", err)
	}

	if len(chain) == 0 {
		return "", "", fmt.Errorf("not found SGX-Enclave-Identity-Issuer-Chain")
	}

	return string(body), chain, nil
}

// getQcnlVersion extracts the QCNL version from PCCS server URL
func (cp *CollateralProvider) getQcnlVersion() (uint, error) {
	url := cp.getPccsURL()
	r := regexp.MustCompile("/v[0-9]+/")
	ver, err := strconv.Atoi(strings.Trim(r.FindString(url), "/")[1:])
	return uint(ver), err
}
