// Package dkim manages per-domain DKIM keys and signs outbound mail, mirroring
// email_server/dkim_manager.py.
package dkim

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"math/big"
	"strings"
	"time"

	msgdkim "github.com/emersion/go-msgauth/dkim"
	"mailgoserver/internal/db"
)

// FixedHeaders is the exact 8-header list DKIM signs over, in this fixed order,
// mirroring dkim_manager.sign_email's `headers` list.
var FixedHeaders = []string{
	"from", "to", "subject", "date", "message-id", "mime-version", "content-type", "content-transfer-encoding",
}

const selectorChars = "abcdefghijklmnopqrstuvwxyz0123456789"

// GenerateSelector mirrors DKIMManager._generate_random_selector(length=12).
func GenerateSelector() string {
	b := make([]byte, 12)
	max := big.NewInt(int64(len(selectorChars)))
	for i := range b {
		n, _ := rand.Int(rand.Reader, max)
		b[i] = selectorChars[n.Int64()]
	}
	return string(b)
}

// Manager mirrors DKIMManager, keyed to a DB handle.
type Manager struct {
	DB      *db.DB
	KeySize int
}

func New(database *db.DB, keySize int) *Manager {
	if keySize == 0 {
		keySize = 2048
	}
	return &Manager{DB: database, KeySize: keySize}
}

// GenerateDKIMKeypair mirrors DKIMManager.generate_dkim_keypair. Returns false if the
// domain doesn't exist (looked up by exact name, active or not — matching the Python
// query, which has no is_active filter here).
func (m *Manager) GenerateDKIMKeypair(domainName, selector string, forceNewKey bool) (bool, error) {
	dom, err := m.DB.GetDomainByNameExact(domainName)
	if err != nil {
		return false, err
	}
	if dom == nil {
		return false, nil
	}

	now := time.Now()
	if _, err := m.DB.Exec(`UPDATE esrv_dkim_keys SET is_active = 0, replaced_at = ? WHERE domain_id = ? AND is_active = 1`, now, dom.ID); err != nil {
		return false, err
	}

	if selector == "" {
		selector = GenerateSelector()
	}

	if !forceNewKey {
		existing, err := m.DB.GetDKIMKeyByDomainAndSelector(dom.ID, selector)
		if err != nil {
			return false, err
		}
		if existing != nil {
			if _, err := m.DB.Exec(`UPDATE esrv_dkim_keys SET is_active = 1, replaced_at = NULL WHERE id = ?`, existing.ID); err != nil {
				return false, err
			}
			return true, nil
		}
	}

	priv, err := rsa.GenerateKey(rand.Reader, m.KeySize)
	if err != nil {
		return false, err
	}
	privPEM, pubPEM, err := encodeKeyPair(priv)
	if err != nil {
		return false, err
	}

	if _, err := m.DB.Exec(`INSERT INTO esrv_dkim_keys (domain_id, selector, private_key, public_key, is_active, created_at)
		VALUES (?, ?, ?, ?, 1, ?)`, dom.ID, selector, privPEM, pubPEM, now); err != nil {
		return false, err
	}
	return true, nil
}

func encodeKeyPair(priv *rsa.PrivateKey) (privPEM, pubPEM string, err error) {
	privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
	if err != nil {
		return "", "", err
	}
	privPEM = string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}))

	pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
	if err != nil {
		return "", "", err
	}
	pubPEM = string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes}))
	return privPEM, pubPEM, nil
}

// GetActiveDKIMKey mirrors DKIMManager.get_active_dkim_key.
func (m *Manager) GetActiveDKIMKey(domainName string) (*db.DKIMKey, error) {
	dom, err := m.DB.GetDomainByName(domainName)
	if err != nil || dom == nil {
		return nil, err
	}
	return m.DB.GetActiveDKIMKeyByDomainID(dom.ID)
}

// DNSRecord is the DNS TXT record for a domain's active DKIM key, mirroring
// DKIMManager.get_dkim_public_key_record's return shape.
type DNSRecord struct {
	Name  string
	Type  string
	Value string
}

// GetDKIMPublicKeyRecord mirrors DKIMManager.get_dkim_public_key_record.
func (m *Manager) GetDKIMPublicKeyRecord(domainName string) (*DNSRecord, error) {
	key, err := m.GetActiveDKIMKey(domainName)
	if err != nil || key == nil {
		return nil, err
	}
	raw := rawBase64FromPEM(key.PublicKey)
	return &DNSRecord{
		Name:  fmt.Sprintf("%s._domainkey.%s", key.Selector, domainName),
		Type:  "TXT",
		Value: fmt.Sprintf(`"v=DKIM1; k=rsa; p=%s"`, raw),
	}, nil
}

func rawBase64FromPEM(pemStr string) string {
	block, _ := pem.Decode([]byte(pemStr))
	if block == nil {
		return ""
	}
	return base64.StdEncoding.EncodeToString(block.Bytes)
}

// Sign mirrors DKIMManager.sign_email: strips any existing DKIM-Signature header,
// signs over the fixed 8-header list with relaxed/relaxed canonicalization, and
// returns the original content unmodified on any failure (including "no active key").
func (m *Manager) Sign(content, domainName string) string {
	key, err := m.GetActiveDKIMKey(domainName)
	if err != nil || key == nil {
		return content
	}
	block, _ := pem.Decode([]byte(key.PrivateKey))
	if block == nil {
		return content
	}
	privAny, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return content
	}
	priv, ok := privAny.(*rsa.PrivateKey)
	if !ok {
		return content
	}

	stripped := stripExistingSignature(content)

	var out strings.Builder
	err = msgdkim.Sign(&out, strings.NewReader(stripped), &msgdkim.SignOptions{
		Domain:                 domainName,
		Selector:               key.Selector,
		Signer:                 priv,
		Hash:                   crypto.SHA256,
		HeaderCanonicalization: msgdkim.CanonicalizationRelaxed,
		BodyCanonicalization:   msgdkim.CanonicalizationRelaxed,
		HeaderKeys:             FixedHeaders,
	})
	if err != nil {
		return content
	}
	return out.String()
}

// stripExistingSignature removes a pre-existing DKIM-Signature header (including any
// folded continuation lines), mirroring the regex in dkim_manager.sign_email.
func stripExistingSignature(content string) string {
	lines := strings.Split(content, "\n")
	var out []string
	skipping := false
	for _, line := range lines {
		lower := strings.ToLower(line)
		if !skipping && strings.HasPrefix(lower, "dkim-signature:") {
			skipping = true
			continue
		}
		if skipping {
			if len(line) > 0 && (line[0] == ' ' || line[0] == '\t') {
				continue // folded continuation line
			}
			skipping = false
		}
		out = append(out, line)
	}
	return strings.Join(out, "\n")
}

// VerifyInbound checks whether content carries at least one valid DKIM signature
// aligned to domainName (typically the From-header domain), mirroring Sign's
// error-swallowing-to-false style. Used only as one signal feeding the inbound spam
// heuristic (internal/mailstore) — never blocks delivery by itself.
func VerifyInbound(content, domainName string) bool {
	verifications, err := msgdkim.Verify(strings.NewReader(content))
	if err != nil {
		return false
	}
	for _, v := range verifications {
		if v.Err == nil && strings.EqualFold(v.Domain, domainName) {
			return true
		}
	}
	return false
}

// GetActiveCustomHeaders mirrors DKIMManager.get_active_custom_headers.
func (m *Manager) GetActiveCustomHeaders(domainName string) ([][2]string, error) {
	dom, err := m.DB.GetDomainByName(domainName)
	if err != nil || dom == nil {
		return nil, err
	}
	rows, err := m.DB.Query(`SELECT header_name, header_value FROM esrv_custom_headers WHERE domain_id = ? AND is_active = 1`, dom.ID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out [][2]string
	for rows.Next() {
		var name, value string
		if err := rows.Scan(&name, &value); err != nil {
			return nil, err
		}
		out = append(out, [2]string{name, value})
	}
	return out, rows.Err()
}
