package reconciler

import (
	"context"
	"encoding/json"
	"time"

	"git.jabali-panel.com/shukivaknin/jabali2/panel-api/internal/models"
	"git.jabali-panel.com/shukivaknin/jabali2/panel-api/internal/services"
)

// reconcilePanelCertificate is M32's reconciler hook. Post ADR-0105 it
// drives TWO independent rows — the panel hostname cert and the panel
// mail (mail.<hostname>) cert — through the same state machine. The
// mail row's routability preflight is on mail.<hostname>; when it
// fails the mail row parks in pending_acme_retry with a clear reason
// and the hostname row is processed completely independently (mail
// can never block the hostname cert). Both kinds share the single
// admin "Use Let's Encrypt" intent (the hostname row's use_le).
func (r *Reconciler) reconcilePanelCertificate(ctx context.Context) {
	if r.panelCerts == nil || r.panelCertRoutability == nil || r.serverSettings == nil {
		return
	}

	settings, err := r.serverSettings.Get(ctx)
	if err != nil {
		r.log.Debug("panel-cert reconcile skipped: server_settings unavailable", "error", err)
		return
	}

	if _, err := r.panelCerts.EnsureDefault(ctx, settings.Hostname); err != nil {
		r.log.Warn("panel-cert reconcile failed to ensure rows", "error", err)
		return
	}
	hostRow, err := r.panelCerts.GetByKind(ctx, models.PanelCertKindHostname)
	if err != nil {
		r.log.Warn("panel-cert reconcile failed to load hostname row", "error", err)
		return
	}
	// Single admin intent: the hostname row's use_le toggle governs
	// both certs.
	if !hostRow.UseLE {
		return
	}
	if settings.Hostname == "" || settings.AdminEmail == "" {
		r.log.Debug("panel-cert reconcile skipped: hostname or admin_email empty")
		return
	}

	// The mail kind (mail.<hostname>) is only pursued when the panel hostname
	// actually serves mail — i.e. email is enabled on the panel-primary domain,
	// the SAME gate the webmail vhost uses. Without this a no-mail panel (the
	// default: email_enabled=0) has no mail.<hostname> DNS, so the mail cert
	// parks in pending_acme_retry forever and shows as a perpetual "processing"
	// task (GH #295). Enabling email on the panel hostname turns it back on.
	panelMailEnabled := false
	if r.domains != nil {
		if d, derr := r.domains.FindByName(ctx, settings.Hostname); derr == nil && d != nil {
			panelMailEnabled = d.EmailEnabled
		}
	}

	for _, kind := range []string{models.PanelCertKindHostname, models.PanelCertKindMail} {
		row, gerr := r.panelCerts.GetByKind(ctx, kind)
		if gerr != nil {
			r.log.Warn("panel-cert reconcile load row", "kind", kind, "error", gerr)
			continue
		}
		if kind == models.PanelCertKindMail && !panelMailEnabled {
			// Don't attempt LE for mail when the panel isn't doing mail. Reset a
			// stale pending row to idle so the UI stops showing it as processing;
			// the self-signed mail cert on disk is harmless (no webmail vhost
			// references it while email is off).
			if row.Status == models.PanelCertStatusPendingACME || row.Status == models.PanelCertStatusPendingACMERetry {
				row.Status = models.PanelCertStatusSelfSigned
				row.LastError = "mail not enabled on the panel hostname — enable email there to request mail.<hostname> TLS"
				row.NextRetryAt = nil
				if uerr := r.panelCerts.Upsert(ctx, row); uerr != nil {
					r.log.Warn("panel-cert reconcile: failed to idle mail row", "error", uerr)
				}
			}
			continue
		}
		r.reconcileOnePanelCert(ctx, kind, row, settings.AdminEmail, settings.PublicIPv4)
	}
}

// reconcileOnePanelCert runs the ADR-0066 state machine for one kind.
// name is the cert subject for this kind (row.Hostname — the panel
// hostname for kind=hostname, mail.<hostname> for kind=mail). All row
// transitions use the per-kind repo variants so the two rows never
// clobber each other.
func (r *Reconciler) reconcileOnePanelCert(ctx context.Context, kind string, row *models.PanelCertificate, adminEmail, publicIPv4 string) {
	name := row.Hostname
	if name == "" {
		return
	}

	switch row.Status {
	case models.PanelCertStatusSelfSigned:
		// First attempt — fall through to dispatch.
	case models.PanelCertStatusPendingACMERetry:
		if row.NextRetryAt == nil || row.NextRetryAt.After(time.Now()) {
			return
		}
	case models.PanelCertStatusPendingACME:
		if time.Since(row.UpdatedAt) < 10*time.Minute {
			return
		}
		r.log.Warn("panel-cert: stale pending_acme lock, retrying", "kind", kind, "name", name, "stuck_for", time.Since(row.UpdatedAt))
	case models.PanelCertStatusIssued:
		return
	case models.PanelCertStatusFailed:
		return
	}

	gate, err := r.panelCertRoutability.Check(ctx, name, publicIPv4, kind == models.PanelCertKindMail)
	if err != nil {
		r.log.Warn("panel-cert routability check errored", "kind", kind, "name", name, "error", err)
		return
	}
	if !gate.Routable {
		// Mail kind not pointed at this server (e.g. mail.<hostname>
		// is Cloudflare-fronted) — park the MAIL row with a clear
		// reason on its own retry schedule. The hostname row is
		// untouched: mail never blocks the panel hostname cert.
		reason := "not routable: " + gate.Reason
		if kind == models.PanelCertKindMail {
			reason = "mail DNS not pointed at this server (" + name + " -> " + gate.Reason + ")"
		}
		r.log.Debug("panel-cert reconcile parked: not routable", "kind", kind, "name", name, "reason", gate.Reason)
		_ = r.panelCerts.MarkPendingRetryKind(ctx, kind, reason, 3*time.Hour)
		return
	}

	// Mark in-flight (per kind) before the agent call so a concurrent
	// tick / REST issue doesn't double-dispatch this kind.
	row.Status = models.PanelCertStatusPendingACME
	if err := r.panelCerts.Upsert(ctx, row); err != nil {
		r.log.Warn("panel-cert reconcile pre-dispatch upsert failed", "kind", kind, "error", err)
		return
	}

	dispatchCtx, cancel := context.WithTimeout(ctx, 3*time.Minute)
	defer cancel()
	// kind + cert_pem_path are forward-compat for the per-kind deploy
	// target (Wave 3 agent wiring honours them; older agents ignore
	// unknown fields). extra_hostnames stays empty — each kind is a
	// single-name cert, independence is the whole point of ADR-0105.
	raw, agentErr := r.agent.Call(dispatchCtx, "ssl.panel.issue", map[string]any{
		"hostname":        name,
		"extra_hostnames": []string{},
		"email":           adminEmail,
		"staging":         row.Staging,
		"kind":            kind,
		"cert_pem_path":   row.CertPEMPath,
	})
	if agentErr != nil {
		r.log.Warn("panel-cert ssl.panel.issue failed", "kind", kind, "name", name, "error", agentErr)
		failReason := agentErr.Error()
		if kind == models.PanelCertKindMail {
			failReason = services.HumanizePanelCertError(name, failReason)
		}
		_ = r.panelCerts.MarkPendingRetryKind(ctx, kind, failReason, 3*time.Hour)
		return
	}

	var resp struct {
		IssuedAt  string `json:"issued_at"`
		ExpiresAt string `json:"expires_at"`
	}
	if err := json.Unmarshal(raw, &resp); err != nil {
		_ = r.panelCerts.MarkPendingRetryKind(ctx, kind, "agent response unmarshal: "+err.Error(), 3*time.Hour)
		return
	}
	issuedAt, err1 := time.Parse(time.RFC3339, resp.IssuedAt)
	expiresAt, err2 := time.Parse(time.RFC3339, resp.ExpiresAt)
	if err1 != nil || err2 != nil {
		_ = r.panelCerts.MarkPendingRetryKind(ctx, kind, "agent response timestamp parse failed", 3*time.Hour)
		return
	}
	if err := r.panelCerts.MarkIssuedKind(ctx, kind, issuedAt, expiresAt); err != nil {
		r.log.Warn("panel-cert MarkIssued failed", "kind", kind, "error", err)
		return
	}
	r.log.Info("panel-cert issued", "kind", kind, "name", name, "expires_at", expiresAt)
}
