// Package acmecert obtains and renews Let's Encrypt certificates via the DNS-01
// challenge, as an admin-configurable alternative to the self-signed certificate
// tlsutil generates by default. Obtained certificates are written to the same
// cert/key file paths the self-signed generator already uses, so the SMTP/IMAP TLS
// listeners (via tlsutil.CertReloader) never need to know which produced the active
// certificate.
package acmecert

import (
	"context"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/go-acme/lego/v4/certcrypto"
	"github.com/go-acme/lego/v4/certificate"
	"github.com/go-acme/lego/v4/lego"
	"github.com/go-acme/lego/v4/registration"
	"gopkg.in/ini.v1"

	"mailgoserver/internal/tlsutil"
	"mailgoserver/internal/toolbox"
)

// renewalThreshold mirrors the standard ACME-client convention (certbot/lego CLI):
// renew once a certificate is within 30 days of its (90-day, for Let's Encrypt) expiry.
const renewalThreshold = 30 * 24 * time.Hour

// Status is a read-only snapshot of the current Let's Encrypt configuration and the
// last renewal attempt, for the admin settings page.
type Status struct {
	Enabled     bool
	Staging     bool
	Domains     []string
	Provider    string
	NotAfter    time.Time // parsed live from CertFile each call — never cached
	LastAttempt time.Time // zero value = no attempt yet this process run
	LastError   string    // empty if the last attempt succeeded, or none has run yet
}

// Manager obtains and renews certificates for one configured domain set.
type Manager struct {
	Cfg               *ini.File
	CertFile, KeyFile string
	DataDir           string
	Reloader          *tlsutil.CertReloader
	Logger            *toolbox.Logger

	mu          sync.Mutex
	lastAttempt time.Time
	lastError   string
}

func New(cfg *ini.File, certFile, keyFile, dataDir string, reloader *tlsutil.CertReloader, logger *toolbox.Logger) *Manager {
	return &Manager{Cfg: cfg, CertFile: certFile, KeyFile: keyFile, DataDir: dataDir, Reloader: reloader, Logger: logger}
}

func (m *Manager) section() *ini.Section { return m.Cfg.Section("LetsEncrypt") }

func (m *Manager) domains() []string {
	raw := m.section().Key("domains").String()
	if raw == "" {
		return nil
	}
	parts := strings.Split(raw, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		if p = strings.TrimSpace(p); p != "" {
			out = append(out, p)
		}
	}
	return out
}

// Status returns a snapshot of the current configuration plus the last obtain/renew
// attempt's outcome. In-memory only (no DB/file persistence) — this is informational
// status for the settings page, not an audit log; it resets on restart, which is an
// accepted simplification.
func (m *Manager) Status() Status {
	m.mu.Lock()
	s := Status{
		Enabled:     m.section().Key("enabled").MustBool(false),
		Staging:     m.section().Key("staging").MustBool(false),
		Domains:     m.domains(),
		Provider:    m.section().Key("dns_provider").String(),
		LastAttempt: m.lastAttempt,
		LastError:   m.lastError,
	}
	m.mu.Unlock()

	if cert, err := readLeafCertificate(m.CertFile); err == nil {
		s.NotAfter = cert.NotAfter
	}
	return s
}

// NeedsRenewal reports whether the certificate currently at CertFile is within 30 days
// of expiry (or unreadable/unparseable, which is treated as "yes" — nothing usable is
// there to keep). This is a pure expiry check; it makes no attempt to distinguish a
// self-signed cert from an ACME-obtained one (see the caller in main.go's renewal
// ticker for how the very-first-check case is handled instead).
func (m *Manager) NeedsRenewal() (bool, error) {
	cert, err := readLeafCertificate(m.CertFile)
	if err != nil {
		return true, nil
	}
	return time.Until(cert.NotAfter) < renewalThreshold, nil
}

func readLeafCertificate(certFile string) (*x509.Certificate, error) {
	raw, err := os.ReadFile(certFile)
	if err != nil {
		return nil, err
	}
	block, _ := pem.Decode(raw)
	if block == nil {
		return nil, fmt.Errorf("acmecert: no PEM block found in %s", certFile)
	}
	return x509.ParseCertificate(block.Bytes)
}

// ObtainOrRenew requests a certificate for the configured domains and, on success,
// writes it to CertFile/KeyFile and hot-reloads the live TLS listeners. Used for both
// first issuance and renewal — lego's Obtain covers both identically, so there is no
// separate renewal code path. A no-op (nil error) if Let's Encrypt isn't enabled. On
// any failure, the cert/key files on disk are left untouched — whatever was already
// serving (self-signed or a previous ACME cert) keeps working.
func (m *Manager) ObtainOrRenew(ctx context.Context) error {
	if !m.section().Key("enabled").MustBool(false) {
		return nil
	}

	err := m.obtain(ctx)

	m.mu.Lock()
	m.lastAttempt = time.Now()
	if err != nil {
		m.lastError = err.Error()
	} else {
		m.lastError = ""
	}
	m.mu.Unlock()

	return err
}

func (m *Manager) obtain(ctx context.Context) error {
	domains := m.domains()
	if len(domains) == 0 {
		return fmt.Errorf("acmecert: no domains configured")
	}

	email := m.section().Key("contact_email").String()
	user, err := loadOrCreateAccount(m.DataDir, email)
	if err != nil {
		return fmt.Errorf("load ACME account: %w", err)
	}

	config := lego.NewConfig(user)
	config.Certificate.KeyType = certcrypto.EC256
	if m.section().Key("staging").MustBool(false) {
		config.CADirURL = lego.LEDirectoryStaging
	}

	client, err := lego.NewClient(config)
	if err != nil {
		return fmt.Errorf("create ACME client: %w", err)
	}

	provider, err := buildDNSProvider(m.Cfg)
	if err != nil {
		return fmt.Errorf("configure DNS provider: %w", err)
	}
	if err := client.Challenge.SetDNS01Provider(provider); err != nil {
		return fmt.Errorf("set DNS-01 provider: %w", err)
	}

	if user.Registration == nil {
		reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
		if err != nil {
			return fmt.Errorf("register ACME account: %w", err)
		}
		user.Registration = reg
		if err := saveRegistration(m.DataDir, reg); err != nil {
			m.Logger.Error("save ACME account registration: %v", err)
		}
	}

	cert, err := client.Certificate.Obtain(certificate.ObtainRequest{
		Domains: domains,
		Bundle:  true,
	})
	if err != nil {
		return fmt.Errorf("obtain certificate: %w", err)
	}

	if err := os.WriteFile(m.CertFile, cert.Certificate, 0o644); err != nil {
		return fmt.Errorf("write certificate: %w", err)
	}
	if err := os.WriteFile(m.KeyFile, cert.PrivateKey, 0o600); err != nil {
		return fmt.Errorf("write private key: %w", err)
	}
	if err := m.Reloader.Reload(); err != nil {
		return fmt.Errorf("reload TLS certificate: %w", err)
	}

	m.Logger.Info("Let's Encrypt certificate obtained for %s", strings.Join(domains, ", "))
	return nil
}
