package api

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

	"github.com/alexei/docker-watcher/internal/crypto"
	"github.com/alexei/docker-watcher/internal/dns"
	"github.com/alexei/docker-watcher/internal/npm"
	"github.com/alexei/docker-watcher/internal/store"
	"github.com/alexei/docker-watcher/internal/volume"
	"github.com/alexei/docker-watcher/internal/webhook"
)

// settingsRequest is the expected JSON body for updating settings.
type settingsRequest struct {
	Domain           string `json:"domain"`
	ServerIP         string `json:"server_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"`
	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"`
}

// 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,
		"network":                   settings.Network,
		"subdomain_pattern":         settings.SubdomainPattern,
		"notification_url":          settings.NotificationURL,
		"npm_url":                   settings.NpmURL,
		"npm_email":                 settings.NpmEmail,
		"has_npm_password":          settings.NpmPassword != "",
		"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,
		"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.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
	}

	// 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 err := s.store.UpdateSettings(updated); err != nil {
		respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error())
		return
	}

	// If SSL cert changed, update all existing NPM proxy hosts in the background.
	if sslChanged {
		go s.reapplySSLToAllProxies(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"})
}

// getWebhookURL handles GET /api/settings/webhook-url.
func (s *Server) getWebhookURL(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
	}

	webhookURL := ""
	if settings.WebhookSecret != "" {
		host := settings.Domain
		scheme := "https"
		if host == "" {
			// Fall back to request host for dev/local setups.
			host = r.Host
			scheme = "http"
		}
		webhookURL = fmt.Sprintf("%s://%s/api/webhook/%s", scheme, host, settings.WebhookSecret)
	}

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

// regenerateWebhookSecret handles POST /api/settings/regenerate.
func (s *Server) regenerateWebhookSecret(w http.ResponseWriter, r *http.Request) {
	secret, err := webhook.RegenerateWebhookSecret(s.store)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "failed to regenerate webhook secret: "+err.Error())
		return
	}

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

	host := settings.Domain
	scheme := "https"
	if host == "" {
		host = r.Host
		scheme = "http"
	}
	webhookURL := fmt.Sprintf("%s://%s/api/webhook/%s", scheme, host, secret)

	respondJSON(w, http.StatusOK, map[string]string{
		"webhook_url":    webhookURL,
		"webhook_secret": secret,
	})
}

// 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)
}

// 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
}

// reapplySSLToAllProxies updates all existing NPM proxy hosts managed by Docker Watcher
// to use the new SSL certificate. Runs in the background after settings change.
func (s *Server) reapplySSLToAllProxies(settings store.Settings) {
	ctx := context.Background()

	npmPassword, err := crypto.Decrypt(s.encKey, settings.NpmPassword)
	if err != nil {
		slog.Error("reapply SSL: decrypt npm password", "error", err)
		return
	}

	npmClient := npm.New(settings.NpmURL)
	if err := npmClient.Authenticate(ctx, settings.NpmEmail, npmPassword); err != nil {
		slog.Error("reapply SSL: authenticate to NPM", "error", err)
		return
	}

	// Get all proxy hosts from NPM.
	hosts, err := npmClient.ListProxyHosts(ctx)
	if err != nil {
		slog.Error("reapply SSL: list proxy hosts", "error", err)
		return
	}

	// Get all our managed instances to identify which proxy hosts are ours.
	projects, err := s.store.GetAllProjects()
	if err != nil {
		slog.Error("reapply SSL: get projects", "error", err)
		return
	}

	// Build a set of NPM proxy IDs that belong to our instances.
	managedProxyIDs := make(map[int]bool)
	for _, p := range projects {
		stages, err := s.store.GetStagesByProjectID(p.ID)
		if err != nil {
			continue
		}
		for _, st := range stages {
			instances, err := s.store.GetInstancesByStageID(st.ID)
			if err != nil {
				continue
			}
			for _, inst := range instances {
				if inst.NpmProxyID > 0 {
					managedProxyIDs[inst.NpmProxyID] = true
				}
			}
		}
	}

	updated := 0
	for _, host := range hosts {
		if !managedProxyIDs[host.ID] {
			continue
		}

		config := npm.ProxyHostConfig{
			DomainNames:    host.DomainNames,
			ForwardScheme:  host.ForwardScheme,
			ForwardHost:    host.ForwardHost,
			ForwardPort:    host.ForwardPort,
			BlockExploits:  true,
			AllowWebsocket: true,
			HTTP2Support:   true,
			Meta:           npm.Meta{},
			Locations:      []any{},
		}

		if settings.SSLCertificateID > 0 {
			config.CertificateID = settings.SSLCertificateID
			config.SSLForced = true
			config.HSTSEnabled = true
		} else {
			config.CertificateID = 0
			config.SSLForced = false
			config.HSTSEnabled = false
		}

		if _, err := npmClient.UpdateProxyHost(ctx, host.ID, config); err != nil {
			slog.Warn("reapply SSL: update proxy host failed", "host_id", host.ID, "error", err)
			continue
		}
		updated++
	}

	slog.Info("reapply SSL: completed", "updated", updated, "total_managed", len(managedProxyIDs))
}

// 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)
}

