package api

import (
	"context"
	"log/slog"
	"net/http"
	"path/filepath"
	"strings"

	"github.com/alexei/tinyforge/internal/crypto"
	"github.com/alexei/tinyforge/internal/dns"
	"github.com/alexei/tinyforge/internal/docker"
	"github.com/alexei/tinyforge/internal/npm"
	"github.com/alexei/tinyforge/internal/proxy"
	"github.com/alexei/tinyforge/internal/store"
	"github.com/alexei/tinyforge/internal/volume"
)

// settingsRequest is the expected JSON body for updating settings.
type settingsRequest struct {
	Domain           string `json:"domain"`
	ServerIP         string `json:"server_ip"`
	PublicIP         string `json:"public_ip"`
	Network          string `json:"network"`
	SubdomainPattern string `json:"subdomain_pattern"`
	NotificationURL  string `json:"notification_url"`
	NpmURL           string `json:"npm_url"`
	NpmEmail         string `json:"npm_email"`
	NpmPassword      string `json:"npm_password"`
	PollingInterval  string `json:"polling_interval"`
	SSLCertificateID   *int    `json:"ssl_certificate_id,omitempty"`
	StaleThresholdDays *int    `json:"stale_threshold_days,omitempty"`
	AllowedVolumePaths *string `json:"allowed_volume_paths,omitempty"`
	WildcardDNS          *bool   `json:"wildcard_dns,omitempty"`
	DNSProvider          *string `json:"dns_provider,omitempty"`
	CloudflareAPIToken   string  `json:"cloudflare_api_token"`
	CloudflareZoneID     *string `json:"cloudflare_zone_id,omitempty"`
	NpmAccessListID      *int    `json:"npm_access_list_id,omitempty"`
	ImagePruneThresholdMB *int   `json:"image_prune_threshold_mb,omitempty"`
	NpmRemote            *bool   `json:"npm_remote,omitempty"`
	ProxyProvider        *string `json:"proxy_provider,omitempty"`
	TraefikEntrypoint    *string `json:"traefik_entrypoint,omitempty"`
	TraefikCertResolver  *string `json:"traefik_cert_resolver,omitempty"`
	TraefikNetwork       *string `json:"traefik_network,omitempty"`
	TraefikAPIURL        *string `json:"traefik_api_url,omitempty"`
	BackupEnabled          *bool `json:"backup_enabled,omitempty"`
	BackupIntervalHours    *int  `json:"backup_interval_hours,omitempty"`
	BackupRetentionCount   *int  `json:"backup_retention_count,omitempty"`
	AutoBackupBeforeDeploy *bool `json:"auto_backup_before_deploy,omitempty"`
	StatsIntervalSeconds   *int  `json:"stats_interval_seconds,omitempty"`
	StatsRetentionHours    *int  `json:"stats_retention_hours,omitempty"`
}

// getSettings handles GET /api/settings.
func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) {
	settings, err := s.store.GetSettings()
	if err != nil {
		respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
		return
	}

	// Return settings without sensitive fields.
	respondJSON(w, http.StatusOK, map[string]any{
		"domain":                    settings.Domain,
		"server_ip":                 settings.ServerIP,
		"public_ip":                 settings.PublicIP,
		"network":                   settings.Network,
		"subdomain_pattern":         settings.SubdomainPattern,
		"notification_url":          settings.NotificationURL,
		"has_notification_secret":   settings.NotificationSecret != "",
		"npm_url":                   settings.NpmURL,
		"npm_email":                 settings.NpmEmail,
		"has_npm_password":          settings.NpmPassword != "",
		"npm_remote":               settings.NpmRemote,
		"image_prune_threshold_mb": settings.ImagePruneThresholdMB,
		"npm_access_list_id":       settings.NpmAccessListID,
		"polling_interval":          settings.PollingInterval,
		"ssl_certificate_id":        settings.SSLCertificateID,
		"stale_threshold_days":      settings.StaleThresholdDays,
		"allowed_volume_paths":      settings.AllowedVolumePaths,
		"wildcard_dns":              settings.WildcardDNS,
		"dns_provider":              settings.DNSProvider,
		"has_cloudflare_api_token":  settings.CloudflareAPIToken != "",
		"cloudflare_zone_id":        settings.CloudflareZoneID,
		"proxy_provider":            settings.ProxyProvider,
		"traefik_entrypoint":        settings.TraefikEntrypoint,
		"traefik_cert_resolver":     settings.TraefikCertResolver,
		"traefik_network":           settings.TraefikNetwork,
		"traefik_api_url":           settings.TraefikAPIURL,
		"backup_enabled":             settings.BackupEnabled,
		"backup_interval_hours":      settings.BackupIntervalHours,
		"backup_retention_count":     settings.BackupRetentionCount,
		"auto_backup_before_deploy":  settings.AutoBackupBeforeDeploy,
		"stats_interval_seconds":    settings.StatsIntervalSeconds,
		"stats_retention_hours":     settings.StatsRetentionHours,
		"updated_at":                settings.UpdatedAt,
	})
}

// updateSettings handles PUT /api/settings.
func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
	var req settingsRequest
	if !decodeJSON(w, r, &req) {
		return
	}

	existing, err := s.store.GetSettings()
	if err != nil {
		respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
		return
	}

	updated := existing
	if req.Domain != "" {
		updated.Domain = req.Domain
	}
	if req.ServerIP != "" {
		updated.ServerIP = req.ServerIP
	}
	if req.PublicIP != "" {
		updated.PublicIP = req.PublicIP
	}
	if req.Network != "" {
		updated.Network = req.Network
	}
	if req.SubdomainPattern != "" {
		updated.SubdomainPattern = req.SubdomainPattern
	}
	// Allow clearing notification URL.
	updated.NotificationURL = req.NotificationURL
	if req.NpmURL != "" {
		updated.NpmURL = req.NpmURL
	}
	if req.NpmEmail != "" {
		updated.NpmEmail = req.NpmEmail
	}
	if req.NpmPassword != "" {
		encPassword, err := crypto.Encrypt(s.encKey, req.NpmPassword)
		if err != nil {
			respondError(w, http.StatusInternalServerError, "failed to encrypt npm password: "+err.Error())
			return
		}
		updated.NpmPassword = encPassword
	}
	if req.PollingInterval != "" {
		updated.PollingInterval = req.PollingInterval
	}
	sslChanged := false
	if req.SSLCertificateID != nil && *req.SSLCertificateID != updated.SSLCertificateID {
		updated.SSLCertificateID = *req.SSLCertificateID
		sslChanged = true
	}
	if req.StaleThresholdDays != nil {
		if *req.StaleThresholdDays < 1 {
			respondError(w, http.StatusBadRequest, "stale_threshold_days must be at least 1")
			return
		}
		updated.StaleThresholdDays = *req.StaleThresholdDays
	}
	if req.AllowedVolumePaths != nil {
		// Validate it's valid JSON array of strings.
		paths, err := volume.ParseAllowedPaths(*req.AllowedVolumePaths)
		if err != nil {
			respondError(w, http.StatusBadRequest, "allowed_volume_paths must be a JSON array of strings")
			return
		}
		// Validate each path is absolute.
		for _, p := range paths {
			if !filepath.IsAbs(p) {
				respondError(w, http.StatusBadRequest, "each allowed volume path must be absolute")
				return
			}
		}
		updated.AllowedVolumePaths = *req.AllowedVolumePaths
		_ = paths // validated
	}

	// DNS settings.
	if req.WildcardDNS != nil {
		updated.WildcardDNS = *req.WildcardDNS
	}
	if req.DNSProvider != nil {
		updated.DNSProvider = *req.DNSProvider
	}
	if req.CloudflareAPIToken != "" {
		encToken, err := crypto.Encrypt(s.encKey, req.CloudflareAPIToken)
		if err != nil {
			respondError(w, http.StatusInternalServerError, "failed to encrypt cloudflare api token: "+err.Error())
			return
		}
		updated.CloudflareAPIToken = encToken
	}
	if req.CloudflareZoneID != nil {
		updated.CloudflareZoneID = *req.CloudflareZoneID
	}

	// Proxy provider setting.
	if req.ProxyProvider != nil {
		prov := *req.ProxyProvider
		if prov != "" && prov != "none" && prov != "npm" && prov != "traefik" {
			respondError(w, http.StatusBadRequest, "proxy_provider must be 'none', 'npm', or 'traefik'")
			return
		}
		updated.ProxyProvider = prov
	}
	if req.ImagePruneThresholdMB != nil {
		updated.ImagePruneThresholdMB = *req.ImagePruneThresholdMB
	}
	if req.NpmRemote != nil {
		updated.NpmRemote = *req.NpmRemote
	}
	if req.NpmAccessListID != nil {
		updated.NpmAccessListID = *req.NpmAccessListID
	}

	// Traefik provider settings.
	if req.TraefikEntrypoint != nil {
		updated.TraefikEntrypoint = *req.TraefikEntrypoint
	}
	if req.TraefikCertResolver != nil {
		updated.TraefikCertResolver = *req.TraefikCertResolver
	}
	if req.TraefikNetwork != nil {
		updated.TraefikNetwork = *req.TraefikNetwork
	}
	if req.TraefikAPIURL != nil {
		updated.TraefikAPIURL = *req.TraefikAPIURL
	}

	// Backup settings.
	if req.BackupEnabled != nil {
		updated.BackupEnabled = *req.BackupEnabled
	}
	if req.BackupIntervalHours != nil {
		if *req.BackupIntervalHours < 1 {
			respondError(w, http.StatusBadRequest, "backup_interval_hours must be at least 1")
			return
		}
		updated.BackupIntervalHours = *req.BackupIntervalHours
	}
	if req.BackupRetentionCount != nil {
		if *req.BackupRetentionCount < 1 {
			respondError(w, http.StatusBadRequest, "backup_retention_count must be at least 1")
			return
		}
		updated.BackupRetentionCount = *req.BackupRetentionCount
	}
	if req.AutoBackupBeforeDeploy != nil {
		updated.AutoBackupBeforeDeploy = *req.AutoBackupBeforeDeploy
	}
	if req.StatsIntervalSeconds != nil {
		v := *req.StatsIntervalSeconds
		if v != 0 && (v < 5 || v > 300) {
			respondError(w, http.StatusBadRequest, "stats_interval_seconds must be 0 (disabled) or between 5 and 300")
			return
		}
		updated.StatsIntervalSeconds = v
	}
	if req.StatsRetentionHours != nil {
		v := *req.StatsRetentionHours
		if v < 0 || v > 24 {
			respondError(w, http.StatusBadRequest, "stats_retention_hours must be between 0 and 24")
			return
		}
		updated.StatsRetentionHours = v
	}

	if err := s.store.UpdateSettings(updated); err != nil {
		respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error())
		return
	}

	// If proxy-affecting settings changed, re-sync all proxy routes in the background.
	proxyChanged := existing.Domain != updated.Domain ||
		existing.ProxyProvider != updated.ProxyProvider ||
		existing.NpmRemote != updated.NpmRemote ||
		existing.NpmAccessListID != updated.NpmAccessListID ||
		sslChanged
	if proxyChanged {
		go s.resyncAllProxies(existing, updated)
	}

	// Handle DNS provider changes.
	dnsChanged := existing.WildcardDNS != updated.WildcardDNS ||
		existing.DNSProvider != updated.DNSProvider ||
		existing.CloudflareZoneID != updated.CloudflareZoneID ||
		(req.CloudflareAPIToken != "" && req.CloudflareAPIToken != "unchanged")
	if dnsChanged {
		oldProvider := s.getDNSProviderLocked()
		go s.handleDNSSettingsChange(oldProvider, existing, updated)
	}

	// Handle backup settings changes.
	backupChanged := existing.BackupEnabled != updated.BackupEnabled ||
		existing.BackupIntervalHours != updated.BackupIntervalHours
	if backupChanged && s.onBackupSettingsChanged != nil {
		s.onBackupSettingsChanged(updated.BackupEnabled, updated.BackupIntervalHours)
	}

	respondJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}

// listNpmCertificates handles GET /api/settings/npm-certificates.
// It authenticates to NPM using the stored credentials and returns only wildcard certificates.
func (s *Server) listNpmCertificates(w http.ResponseWriter, r *http.Request) {
	settings, err := s.store.GetSettings()
	if err != nil {
		respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
		return
	}

	if settings.NpmURL == "" || settings.NpmEmail == "" || settings.NpmPassword == "" {
		respondError(w, http.StatusBadRequest, "NPM credentials not configured")
		return
	}

	npmPassword, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "failed to decrypt npm password: "+err.Error())
		return
	}

	client := npm.New(settings.NpmURL)
	if err := client.Authenticate(r.Context(), settings.NpmEmail, npmPassword); err != nil {
		respondError(w, http.StatusBadGateway, "failed to authenticate to NPM: "+err.Error())
		return
	}

	certs, err := client.ListCertificates(r.Context())
	if err != nil {
		respondError(w, http.StatusBadGateway, "failed to list certificates: "+err.Error())
		return
	}

	// Filter to wildcard certificates only.
	var wildcards []npm.Certificate
	for _, cert := range certs {
		if isWildcardCert(cert) {
			wildcards = append(wildcards, cert)
		}
	}

	if wildcards == nil {
		wildcards = []npm.Certificate{}
	}

	respondJSON(w, http.StatusOK, wildcards)
}

// listNpmAccessLists handles GET /api/settings/npm-access-lists.
// It authenticates to NPM using the stored credentials and returns all access lists.
func (s *Server) listNpmAccessLists(w http.ResponseWriter, r *http.Request) {
	settings, err := s.store.GetSettings()
	if err != nil {
		respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
		return
	}

	if settings.NpmURL == "" || settings.NpmEmail == "" || settings.NpmPassword == "" {
		respondError(w, http.StatusBadRequest, "NPM credentials not configured")
		return
	}

	npmPassword, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "failed to decrypt npm password: "+err.Error())
		return
	}

	client := npm.New(settings.NpmURL)
	if err := client.Authenticate(r.Context(), settings.NpmEmail, npmPassword); err != nil {
		respondError(w, http.StatusBadGateway, "failed to authenticate to NPM: "+err.Error())
		return
	}

	lists, err := client.ListAccessLists(r.Context())
	if err != nil {
		respondError(w, http.StatusBadGateway, "failed to list access lists: "+err.Error())
		return
	}

	if lists == nil {
		lists = []npm.AccessList{}
	}

	respondJSON(w, http.StatusOK, lists)
}

// isWildcardCert returns true if any of the certificate's domain names contains "*".
func isWildcardCert(cert npm.Certificate) bool {
	for _, d := range cert.DomainNames {
		if strings.Contains(d, "*") {
			return true
		}
	}
	return false
}

// createProxyProvider builds a proxy.Provider from the given settings.
func (s *Server) createProxyProvider(settings store.Settings) proxy.Provider {
	switch settings.ProxyProvider {
	case "npm":
		if settings.NpmURL == "" || settings.NpmEmail == "" || settings.NpmPassword == "" {
			slog.Warn("proxy resync: NPM credentials incomplete, falling back to none")
			return proxy.NewNoneProvider()
		}
		npmPassword, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
		if err != nil {
			slog.Error("proxy resync: decrypt npm password", "error", err)
			return proxy.NewNoneProvider()
		}
		return proxy.NewNpmProvider(npm.New(settings.NpmURL), settings.NpmEmail, npmPassword)
	case "traefik":
		return proxy.NewTraefikProvider(
			settings.TraefikEntrypoint,
			settings.TraefikCertResolver,
			settings.TraefikNetwork,
			settings.TraefikAPIURL,
		)
	default:
		return proxy.NewNoneProvider()
	}
}

// resyncAllProxies re-configures or removes proxy routes for all running instances
// when proxy-affecting settings change (domain, SSL cert, proxy provider).
// Runs in the background after settings save.
func (s *Server) resyncAllProxies(oldSettings, newSettings store.Settings) {
	ctx := context.Background()

	// Collect all proxy-enabled instances.
	routes, err := s.store.ListProxyRoutes(oldSettings.Domain)
	if err != nil {
		slog.Error("proxy resync: list routes", "error", err)
		return
	}

	if len(routes) == 0 {
		slog.Info("proxy resync: no proxy routes to update")
		return
	}

	providerChanged := oldSettings.ProxyProvider != newSettings.ProxyProvider
	domainChanged := oldSettings.Domain != newSettings.Domain

	// Step 1: If provider changed, delete old routes from the OLD provider, then switch.
	if providerChanged {
		slog.Info("proxy resync: provider changed", "old", oldSettings.ProxyProvider, "new", newSettings.ProxyProvider)
		oldProvider := s.proxyProvider
		for _, route := range routes {
			if route.ProxyRouteID != "" {
				if err := oldProvider.DeleteRoute(ctx, route.ProxyRouteID); err != nil {
					slog.Warn("proxy resync: delete old route", "route_id", route.ProxyRouteID, "error", err)
				}
			}
		}

		// Create and install the new provider.
		newProvider := s.createProxyProvider(newSettings)
		s.SetProxyProvider(newProvider)
		if s.onProxyProviderChanged != nil {
			s.onProxyProviderChanged(newProvider)
		}
	}

	// Step 2: If new provider is "none", clear all proxy route IDs and we're done.
	if newSettings.ProxyProvider == "none" {
		for _, route := range routes {
			c, err := s.store.GetContainerByID(route.InstanceID)
			if err != nil {
				continue
			}
			c.ProxyRouteID = ""
			c.NpmProxyID = 0
			if err := s.store.UpdateContainer(c); err != nil {
				slog.Warn("proxy resync: clear route ID", "container", route.InstanceID, "error", err)
			}
		}
		slog.Info("proxy resync: cleared all proxy routes (provider set to none)", "count", len(routes))
		return
	}

	// Step 3: Re-create/update routes with the current provider and new settings.
	updated := 0
	for _, route := range routes {
		if route.Subdomain == "" {
			continue
		}

		fqdn := route.Subdomain + "." + newSettings.Domain

		// Reconstruct the container name (Docker DNS name) from project/stage/tag.
		containerName := docker.ContainerName(route.ProjectName, route.StageName, route.ImageTag)

		routeID, err := s.proxyProvider.ConfigureRoute(ctx, fqdn, containerName, route.Port, proxy.RouteOptions{
			SSLCertificateID: newSettings.SSLCertificateID,
		})
		if err != nil {
			slog.Warn("proxy resync: configure route failed",
				"domain", fqdn, "instance", route.InstanceID, "error", err)
			continue
		}

		// Update container row with new route ID.
		c, err := s.store.GetContainerByID(route.InstanceID)
		if err != nil {
			continue
		}
		c.ProxyRouteID = routeID
		if domainChanged {
			slog.Info("proxy resync: domain updated", "container", route.InstanceID, "domain", fqdn)
		}
		if err := s.store.UpdateContainer(c); err != nil {
			slog.Warn("proxy resync: update container", "container", route.InstanceID, "error", err)
		}
		updated++
	}

	slog.Info("proxy resync: completed", "updated", updated, "total", len(routes))
}

// handleDNSSettingsChange reacts to DNS configuration changes:
// - If switching to wildcard mode: remove all managed DNS records from the provider.
// - If switching provider or credentials: remove old records, create new provider, re-sync.
func (s *Server) handleDNSSettingsChange(oldProvider dns.Provider, oldSettings, newSettings store.Settings) {
	ctx := context.Background()

	// Step 1: If there was an old provider, remove all managed DNS records from it.
	if !oldSettings.WildcardDNS && oldSettings.DNSProvider != "" && oldProvider != nil {
		records, err := s.store.ListDNSRecords()
		if err != nil {
			slog.Error("dns settings change: list records for cleanup", "error", err)
		} else {
			for _, rec := range records {
				if err := oldProvider.DeleteRecord(ctx, rec.FQDN); err != nil {
					slog.Warn("dns settings change: delete old record", "fqdn", rec.FQDN, "error", err)
				}
				if err := s.store.DeleteDNSRecord(rec.FQDN); err != nil {
					slog.Warn("dns settings change: remove tracking record", "fqdn", rec.FQDN, "error", err)
				}
			}
			slog.Info("dns settings change: cleaned up old records", "count", len(records))
		}
	}

	// Step 2: Create new provider (or nil for wildcard mode).
	var newProvider dns.Provider
	if !newSettings.WildcardDNS && newSettings.DNSProvider != "" {
		token := newSettings.CloudflareAPIToken
		if token != "" {
			decrypted, err := crypto.Decrypt(s.encKey, token)
			if err != nil {
				slog.Error("dns settings change: decrypt token", "error", err)
				return
			}
			token = decrypted
		}

		provider, err := dns.NewProvider(newSettings.DNSProvider, dns.Config{
			Token:  token,
			ZoneID: newSettings.CloudflareZoneID,
		})
		if err != nil {
			slog.Error("dns settings change: create provider", "error", err)
			return
		}
		newProvider = provider
	}

	// Step 3: Update the server's DNS provider and notify dependents.
	s.SetDNSProvider(newProvider)
	if s.onDNSProviderChanged != nil {
		s.onDNSProviderChanged(newProvider)
	}

	slog.Info("dns settings change: provider updated",
		"wildcard", newSettings.WildcardDNS,
		"provider", newSettings.DNSProvider)
}

// dnsTestRequest is the expected JSON body for testing DNS provider credentials.
type dnsTestRequest struct {
	Provider string `json:"provider"`
	Token    string `json:"token"`
	ZoneID   string `json:"zone_id"`
}

// testNpmConnection handles POST /api/settings/npm/test.
// Tests connectivity and authentication to the NPM API.
func (s *Server) testNpmConnection(w http.ResponseWriter, r *http.Request) {
	var req struct {
		URL      string `json:"npm_url"`
		Email    string `json:"npm_email"`
		Password string `json:"npm_password"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}

	// Use provided values, fall back to stored settings.
	settings, err := s.store.GetSettings()
	if err != nil {
		slog.Error("failed to get settings", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	npmURL := req.URL
	if npmURL == "" {
		npmURL = settings.NpmURL
	}
	if npmURL == "" {
		respondError(w, http.StatusBadRequest, "NPM URL is required")
		return
	}

	email := req.Email
	if email == "" {
		email = settings.NpmEmail
	}

	password := req.Password
	if password == "" && settings.NpmPassword != "" {
		decrypted, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
		if err != nil {
			respondError(w, http.StatusBadRequest, "failed to decrypt stored NPM password")
			return
		}
		password = decrypted
	}

	if email == "" || password == "" {
		respondError(w, http.StatusBadRequest, "NPM email and password are required")
		return
	}

	// Test connectivity.
	client := npm.New(npmURL)
	ctx := r.Context()

	if err := client.Ping(ctx); err != nil {
		slog.Warn("npm test: ping failed", "url", npmURL, "error", err)
		respondError(w, http.StatusBadGateway, "Cannot reach NPM at "+npmURL)
		return
	}

	// Test authentication.
	if err := client.Authenticate(ctx, email, password); err != nil {
		slog.Warn("npm test: auth failed", "url", npmURL, "error", err)
		respondError(w, http.StatusBadGateway, "NPM authentication failed — check email and password")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"status": "connected"})
}

// testDNSConnection handles POST /api/settings/dns/test.
func (s *Server) testDNSConnection(w http.ResponseWriter, r *http.Request) {
	var req dnsTestRequest
	if !decodeJSON(w, r, &req) {
		return
	}

	if req.Provider != "cloudflare" {
		respondError(w, http.StatusBadRequest, "unsupported DNS provider: "+req.Provider)
		return
	}

	token := req.Token
	// If no token provided, use the stored one.
	if token == "" {
		settings, err := s.store.GetSettings()
		if err != nil {
			respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
			return
		}
		if settings.CloudflareAPIToken == "" {
			respondError(w, http.StatusBadRequest, "no Cloudflare API token configured")
			return
		}
		decrypted, err := crypto.Decrypt(s.encKey, settings.CloudflareAPIToken)
		if err != nil {
			respondError(w, http.StatusInternalServerError, "failed to decrypt token: "+err.Error())
			return
		}
		token = decrypted
	}

	provider, err := dns.NewCloudflare(token, req.ZoneID)
	if err != nil {
		respondError(w, http.StatusBadRequest, "invalid configuration: "+err.Error())
		return
	}

	if err := provider.TestConnection(r.Context()); err != nil {
		respondJSON(w, http.StatusOK, map[string]any{
			"success": false,
			"error":   err.Error(),
		})
		return
	}

	respondJSON(w, http.StatusOK, map[string]any{
		"success": true,
	})
}

// dnsZonesRequest is the expected JSON body for listing DNS zones.
type dnsZonesRequest struct {
	Token string `json:"token"`
}

// listDNSZones handles POST /api/settings/dns/zones.
func (s *Server) listDNSZones(w http.ResponseWriter, r *http.Request) {
	var req dnsZonesRequest
	if !decodeJSON(w, r, &req) {
		return
	}
	token := req.Token
	// If no token in body, use stored one.
	if token == "" {
		settings, err := s.store.GetSettings()
		if err != nil {
			respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
			return
		}
		if settings.CloudflareAPIToken == "" {
			respondError(w, http.StatusBadRequest, "no Cloudflare API token configured")
			return
		}
		decrypted, err := crypto.Decrypt(s.encKey, settings.CloudflareAPIToken)
		if err != nil {
			respondError(w, http.StatusInternalServerError, "failed to decrypt token: "+err.Error())
			return
		}
		token = decrypted
	}

	provider, err := dns.NewCloudflare(token, "")
	if err != nil {
		respondError(w, http.StatusBadRequest, "invalid configuration: "+err.Error())
		return
	}

	zones, err := provider.ListZones(r.Context())
	if err != nil {
		respondError(w, http.StatusBadGateway, "failed to list zones: "+err.Error())
		return
	}

	respondJSON(w, http.StatusOK, zones)
}

