package ssl

import (
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"log"
	"net"
	"strings"
	"sync"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/go-acme/lego/v5/certificate"
	"github.com/miekg/dns"
	"golang.org/x/crypto/acme"
)

type ManualClient struct {
	client  *acme.Client
	account *model.WebsiteAcmeAccount
	logger  *log.Logger
}

type RequestCertRequest struct {
	WebsiteSSL *model.WebsiteSSL
}

var manualOrders sync.Map

func loadManualOrder(sslID uint) (*acme.Order, bool) {
	value, ok := manualOrders.Load(sslID)
	if !ok {
		return nil, false
	}
	order, ok := value.(*acme.Order)
	return order, ok && order != nil
}

func storeManualOrder(sslID uint, order *acme.Order) {
	if order == nil {
		manualOrders.Delete(sslID)
		return
	}
	manualOrders.Store(sslID, order)
}

func deleteManualOrder(sslID uint, order *acme.Order) {
	if order == nil {
		manualOrders.Delete(sslID)
		return
	}
	manualOrders.CompareAndDelete(sslID, order)
}

func NewCustomAcmeClient(acmeAccount *model.WebsiteAcmeAccount, proxy *dto.SystemProxy, logger *log.Logger) (*ManualClient, error) {
	key, err := parsePrivateKeyPEM([]byte(acmeAccount.PrivateKey))
	if err != nil {
		return nil, err
	}
	if logger == nil {
		logger = log.Default()
	}
	var (
		proxyURL      string
		proxyUser     string
		proxyPassword string
	)
	if proxy != nil {
		proxyURL = fmt.Sprintf("%s://%s:%s", proxy.Type, proxy.URL, proxy.Port)
		proxyUser = proxy.User
		proxyPassword = proxy.Password
	}

	client := &acme.Client{
		Key:          key,
		HTTPClient:   createHTTPClientWithProxy(proxyURL, proxyUser, proxyPassword),
		DirectoryURL: getCaDirURL(acmeAccount.Type, acmeAccount.CaDirURL),
	}
	return &ManualClient{
		client:  client,
		account: acmeAccount,
		logger:  logger,
	}, nil
}

type Resolve struct {
	Key   string
	Value string
	Err   string
}

func (c *ManualClient) GetDNSResolve(ctx context.Context, websiteSSL *model.WebsiteSSL) (map[string]Resolve, error) {
	var order *acme.Order
	var err error

	// Check if we have an existing valid order for this SSL
	existingOrder, exists := loadManualOrder(websiteSSL.ID)
	if exists && existingOrder != nil {
		// Verify the order is still valid (not expired and still pending)
		// If Expires is zero, order is still valid (ACME doesn't always set expiry immediately)
		isNotExpired := existingOrder.Expires.IsZero() || existingOrder.Expires.After(time.Now())
		if isNotExpired && (existingOrder.Status == acme.StatusPending || existingOrder.Status == acme.StatusReady) {
			// Try to reuse the existing order
			records, err := c.extractDNSChallenges(ctx, existingOrder)
			if err == nil && len(records) > 0 {
				return records, nil
			}
			// If extraction failed, fall through to create a new order
		}
		// Existing order is expired or invalid, remove it
		deleteManualOrder(websiteSSL.ID, existingOrder)
	}

	// Create a new order
	order, err = c.client.AuthorizeOrder(ctx, acme.DomainIDs(getWebsiteSSLDomains(websiteSSL)...))
	if err != nil {
		return nil, err
	}
	storeManualOrder(websiteSSL.ID, order)

	return c.extractDNSChallenges(ctx, order)
}

// extractDNSChallenges extracts DNS-01 challenge values from an ACME order
func (c *ManualClient) extractDNSChallenges(ctx context.Context, order *acme.Order) (map[string]Resolve, error) {
	records := make(map[string]Resolve)

	for _, authzURL := range order.AuthzURLs {
		authz, err := c.client.GetAuthorization(ctx, authzURL)
		if err != nil {
			return nil, err
		}
		domain := authz.Identifier.Value

		var dnsChallenge *acme.Challenge
		for _, challenge := range authz.Challenges {
			if challenge.Type == "dns-01" {
				dnsChallenge = challenge
				break
			}
		}

		if dnsChallenge == nil {
			return nil, fmt.Errorf("no DNS-01 challenge found for domain %s", domain)
		}

		txtValue, err := c.client.DNS01ChallengeRecord(dnsChallenge.Token)
		if err != nil {
			return nil, err
		}

		// Use different map key for wildcard vs non-wildcard to avoid overwriting
		// Both use the same DNS record name (_acme-challenge.domain) but different values
		mapKey := domain
		if authz.Wildcard {
			mapKey = "*." + domain
		}

		records[mapKey] = Resolve{
			Key:   fmt.Sprintf("_acme-challenge.%s", domain),
			Value: txtValue,
		}
	}
	return records, nil
}

func queryDNSRecords(domain string) (map[string][]string, error) {
	recordName := fmt.Sprintf("_acme-challenge.%s", domain)
	txts, err := net.LookupTXT(recordName)
	if err != nil {
		return nil, err
	}
	records := make(map[string][]string)
	if len(txts) > 0 {
		records[recordName] = txts
	}
	return records, nil
}

func (c *ManualClient) handleAuthorization(ctx context.Context, authzURL string, nameservers []string) error {
	authz, err := c.client.GetAuthorization(ctx, authzURL)
	if err != nil {
		return fmt.Errorf("failed to get authorization: %v", err)
	}

	domain := authz.Identifier.Value
	c.logger.Printf("[INFO] [%s] AuthURL: %s", domain, authzURL)

	if authz.Status == acme.StatusValid {
		return nil
	}

	var dnsChallenge *acme.Challenge
	for _, challenge := range authz.Challenges {
		if challenge.Type == "dns-01" {
			dnsChallenge = challenge
			break
		}
	}

	c.logger.Printf("[INFO] [%s] acme: use dns-01 solver", domain)
	if dnsChallenge == nil {
		return fmt.Errorf("no DNS-01 challenge found for domain %s", domain)
	}

	deadline := time.Now().Add(manualDnsTimeout)
	expectedRecord, err := c.client.DNS01ChallengeRecord(dnsChallenge.Token)
	if err != nil {
		return fmt.Errorf("failed to compute DNS challenge record: %v", err)
	}
	c.logger.Printf("[INFO] [%s] acme: Checking TXT record  %s", domain, expectedRecord)

	for {
		if err := ctx.Err(); err != nil {
			return err
		}
		c.logger.Printf("[INFO] [%s] acme: Checking DNS record propagation.", domain)
		var currentRecords map[string][]string
		var queryErr error
		if len(nameservers) == 0 {
			currentRecords, queryErr = queryDNSRecords(domain)
		} else {
			var errs []string
			for _, nameserver := range nameservers {
				currentRecords, queryErr = queryDNSRecordsWithResolver(ctx, c.logger, domain, nameserver)
				errs = append(errs, fmt.Sprintf("%s: %v", nameserver, queryErr))
			}
			if queryErr != nil && len(errs) > 0 {
				queryErr = fmt.Errorf("all nameservers failed: %s", strings.Join(errs, "; "))
			}
		}
		if currentRecords == nil && queryErr != nil {
			return fmt.Errorf("failed to query DNS records: %v", queryErr)
		}
		recordName := fmt.Sprintf("_acme-challenge.%s", domain)
		providedRecords, exists := currentRecords[recordName]
		// Check if expected record is in any of the TXT values
		found := false
		if exists {
			for _, record := range providedRecords {
				if record == expectedRecord {
					found = true
					break
				}
			}
		}
		if found {
			break
		}
		if time.Now().After(deadline) {
			if !exists || len(providedRecords) == 0 {
				return fmt.Errorf("TXT record not provided for domain %s after retrying", domain)
			}
			c.logger.Printf("[INFO] [%s] TXT record mismatch for %s: expected %s, got %v", domain, domain, expectedRecord, providedRecords)
			return fmt.Errorf("TXT record mismatch for %s: expected %s, got %v", domain, expectedRecord, providedRecords)
		}
		if err := waitForRetry(ctx, pollingInterval); err != nil {
			return err
		}
	}

	_, err = c.client.Accept(ctx, dnsChallenge)
	if err != nil {
		return fmt.Errorf("failed to accept challenge: %v", err)
	}
	for {
		if err := waitForRetry(ctx, pollingInterval); err != nil {
			return err
		}
		authz, err = c.client.GetAuthorization(ctx, authzURL)
		if err != nil {
			return fmt.Errorf("failed to get authorization while polling: %v", err)
		}
		if authz.Status == acme.StatusValid {
			break
		} else if authz.Status == acme.StatusInvalid {
			return fmt.Errorf("authorization failed for domain %s", domain)
		}
	}
	return nil
}

func (c *ManualClient) createCSR(keyType string, privateKey string, domains []string) ([]byte, crypto.PrivateKey, error) {
	var certKey crypto.PrivateKey
	var err error
	certKey, err = GetPrivateKeyByType(keyType, privateKey)
	if err != nil {
		return nil, nil, err
	}
	template := x509.CertificateRequest{
		Subject:  pkix.Name{CommonName: domains[0]},
		DNSNames: domains,
	}
	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, certKey)
	if err != nil {
		return nil, nil, err
	}
	return csrBytes, certKey, nil
}

func (c *ManualClient) encodePrivateKey(key crypto.PrivateKey) (string, error) {
	var keyBytes []byte
	var keyType string
	var err error

	switch k := key.(type) {
	case *ecdsa.PrivateKey:
		keyBytes, err = x509.MarshalECPrivateKey(k)
		keyType = "EC PRIVATE KEY"
	case *rsa.PrivateKey:
		keyBytes = x509.MarshalPKCS1PrivateKey(k)
		keyType = "RSA PRIVATE KEY"
	default:
		return "", fmt.Errorf("unsupported key type")
	}

	if err != nil {
		return "", err
	}

	block := &pem.Block{
		Type:  keyType,
		Bytes: keyBytes,
	}

	return string(pem.EncodeToMemory(block)), nil
}

func (c *ManualClient) RequestCertificate(ctx context.Context, websiteSSL *model.WebsiteSSL) (certificate.Resource, error) {
	var res certificate.Resource
	domains := []string{websiteSSL.PrimaryDomain}
	if websiteSSL.Domains != "" {
		domains = append(domains, strings.Split(websiteSSL.Domains, ",")...)
	}

	c.logger.Printf("[INFO] Requesting certificate for domains: %v\n", domains)
	csr, certKey, err := c.createCSR(websiteSSL.KeyType, websiteSSL.PrivateKey, domains)
	if err != nil {
		return res, err
	}

	order, ok := loadManualOrder(websiteSSL.ID)
	if !ok {
		return res, fmt.Errorf("order not found")
	}
	defer deleteManualOrder(websiteSSL.ID, order)

	for _, authzURL := range order.AuthzURLs {
		if err := c.handleAuthorization(ctx, authzURL, getNameservers(*websiteSSL)); err != nil {
			return res, err
		}
	}

	c.logger.Printf("[INFO] acme: Validations succeeded; requesting certificates")
	order, err = c.client.WaitOrder(ctx, order.URI)
	if err != nil {
		return res, err
	}

	if order.Status != acme.StatusReady {
		return res, fmt.Errorf("order not ready: %s", order.Status)
	}

	certBytes, certURL, err := c.client.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
	if err != nil {
		return res, fmt.Errorf("failed to finalize order: %v", err)
	}

	privateKeyPEM, err := c.encodePrivateKey(certKey)
	if err != nil {
		return res, err
	}

	var certPEM []byte
	for _, cert := range certBytes {
		block := &pem.Block{
			Type:  "CERTIFICATE",
			Bytes: cert,
		}
		certPEM = append(certPEM, pem.EncodeToMemory(block)...)
	}
	c.logger.Printf("[INFO] acme: Server responded with a certificate.")
	resource := certificate.Resource{
		Domains:       domains,
		CertURL:       certURL,
		CertStableURL: certURL,
		PrivateKey:    []byte(privateKeyPEM),
		Certificate:   certPEM,
		CSR:           csr,
	}
	return resource, nil
}

func getNameservers(websiteSSL model.WebsiteSSL) []string {
	var nameservers []string
	if websiteSSL.Nameserver1 != "" {
		nameservers = append(nameservers, handleNameserver(websiteSSL.Nameserver1))
	}
	if websiteSSL.Nameserver2 != "" {
		nameservers = append(nameservers, handleNameserver(websiteSSL.Nameserver2))
	}
	return nameservers
}

func handleNameserver(nameserver string) string {
	if strings.Contains(nameserver, ":") {
		return nameserver
	}
	return fmt.Sprintf("%s:53", nameserver)
}

func queryDNSRecordsWithResolver(ctx context.Context, logger *log.Logger, domain string, dnsServer string) (map[string][]string, error) {
	recordName := fmt.Sprintf("_acme-challenge.%s", domain)
	c := new(dns.Client)
	c.Timeout = 10 * time.Second
	c.Net = "udp"

	m := new(dns.Msg)
	m.SetQuestion(dns.Fqdn(recordName), dns.TypeTXT)
	m.RecursionDesired = true

	r, _, err := c.ExchangeContext(ctx, m, dnsServer)
	if isNetworkError(err) {
		logger.Printf("[WARN] Network error occurred while querying DNS: %v", err)
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("DNS query failed: %w", err)
	}

	if r.Rcode == dns.RcodeNameError {
		logger.Printf("[INFO] DNS record does not exist yet (NXDOMAIN)")
		return nil, nil
	}

	if r.Rcode != dns.RcodeSuccess {
		return nil, fmt.Errorf("DNS query failed with code: %s", dns.RcodeToString[r.Rcode])
	}

	records := make(map[string][]string)
	var txtValues []string

	for _, answer := range r.Answer {
		if txt, ok := answer.(*dns.TXT); ok {
			if len(txt.Txt) > 0 {
				txtValues = append(txtValues, txt.Txt[0])
			}
		}
	}
	if len(txtValues) > 0 {
		records[recordName] = txtValues
	}

	return records, nil
}

func isNetworkError(err error) bool {
	if err == nil {
		return false
	}
	return strings.Contains(err.Error(), "i/o timeout") ||
		strings.Contains(err.Error(), "connection refused") ||
		strings.Contains(err.Error(), "network is unreachable") ||
		strings.Contains(err.Error(), "no route to host")
}
