package web

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"net/http"
	"regexp"
	"sort"
	"strings"
	"time"

	cfClient "gitea.dooplex.hu/admin/felhom-hub/internal/cloudflare"
	"gitea.dooplex.hu/admin/felhom-hub/internal/configgen"
	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
	"gopkg.in/yaml.v3"
)

var validCustomerID = regexp.MustCompile(`^[a-zA-Z0-9.\-]+$`)

// customerListEntry is a merged view of a customer from both configs and reports.
type customerListEntry struct {
	CustomerID        string
	CustomerName      string
	Domain            string
	HasConfig         bool
	IsBlocked         bool
	OverallStatus     string // ok, warn, down, disabled, pending, "" if no reports
	ControllerVersion string
	TimeSinceReport   time.Duration
	ConfigCreatedAt   time.Time
}

// handleConfigList shows all customers (merged from configs + reports).
func (s *Server) handleConfigList(w http.ResponseWriter, r *http.Request) {
	configs, err := s.store.ListCustomerConfigs()
	if err != nil {
		s.logger.Printf("[ERROR] Failed to list configs: %v", err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	customers, err := s.store.GetCustomers()
	if err != nil {
		s.logger.Printf("[ERROR] Failed to list customers: %v", err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	// Build merged map keyed by customer_id
	merged := make(map[string]*customerListEntry)

	for _, cfg := range configs {
		merged[cfg.CustomerID] = &customerListEntry{
			CustomerID:      cfg.CustomerID,
			CustomerName:    cfg.CustomerName,
			Domain:          cfg.Domain,
			HasConfig:       true,
			IsBlocked:       cfg.Status == "blocked",
			ConfigCreatedAt: cfg.CreatedAt,
		}
	}

	for _, c := range customers {
		status := "ok"
		if c.HealthStatus == "disabled" {
			status = "disabled"
		} else if c.TimeSinceReport > time.Hour {
			status = "down"
		} else if c.TimeSinceReport > 30*time.Minute || c.HealthStatus == "warn" {
			status = "warn"
		} else if c.HealthStatus == "fail" {
			status = "down"
		}

		if entry, ok := merged[c.CustomerID]; ok {
			// Config exists — enrich with report data
			entry.OverallStatus = status
			entry.ControllerVersion = c.ControllerVersion
			entry.TimeSinceReport = c.TimeSinceReport
			if entry.CustomerName == "" {
				entry.CustomerName = c.CustomerName
			}
		} else {
			// Report-only customer (no config)
			merged[c.CustomerID] = &customerListEntry{
				CustomerID:        c.CustomerID,
				CustomerName:      c.CustomerName,
				OverallStatus:     status,
				ControllerVersion: c.ControllerVersion,
				TimeSinceReport:   c.TimeSinceReport,
			}
		}
	}

	// Sort by customer_id
	entries := make([]customerListEntry, 0, len(merged))
	for _, e := range merged {
		entries = append(entries, *e)
	}
	sort.Slice(entries, func(i, j int) bool {
		return entries[i].CustomerID < entries[j].CustomerID
	})

	data := struct {
		Customers []customerListEntry
		ActiveNav string
		Flash     string
		CSRFToken string
	}{
		Customers: entries,
		ActiveNav: "configs",
		Flash:     r.URL.Query().Get("flash"),
		CSRFToken: s.csrfToken(r),
	}
	s.templates.ExecuteTemplate(w, "configs.html", data)
}

// handleCustomerUnified shows the unified customer detail page (config + reports).
func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, customerID string) {
	cfg, _ := s.store.GetCustomerConfig(customerID)
	customer, _ := s.store.GetCustomer(customerID)

	// 404 if neither config nor reports exist
	if cfg == nil && customer == nil {
		http.NotFound(w, r)
		return
	}

	// Determine identity fields from best source
	name := ""
	domain := ""
	email := ""
	if cfg != nil {
		name = cfg.CustomerName
		domain = cfg.Domain
		email = cfg.Email
	}
	if name == "" && customer != nil {
		name = customer.CustomerName
	}

	// Parse report JSON
	var report map[string]interface{}
	if customer != nil {
		json.Unmarshal([]byte(customer.ReportJSON), &report)
	}

	// Parse config overrides
	var overrides map[string]interface{}
	if cfg != nil {
		json.Unmarshal([]byte(cfg.ConfigJSON), &overrides)
	}

	// Overall status
	overallStatus := "pending"
	if customer != nil {
		if customer.HealthStatus == "disabled" {
			overallStatus = "disabled"
		} else if customer.TimeSinceReport > time.Hour {
			overallStatus = "down"
		} else if customer.TimeSinceReport > 30*time.Minute || customer.HealthStatus == "warn" {
			overallStatus = "warn"
		} else if customer.HealthStatus == "fail" {
			overallStatus = "down"
		} else {
			overallStatus = "ok"
		}
	}
	if cfg != nil && cfg.Status == "blocked" {
		overallStatus = "blocked"
	}

	// Controller URL
	controllerURL := ""
	if customer != nil {
		controllerURL = customer.ControllerURL
		if controllerURL == "" {
			var rpt struct {
				ControllerURL string `json:"controller_url"`
			}
			json.Unmarshal([]byte(customer.ReportJSON), &rpt)
			controllerURL = rpt.ControllerURL
		}
	}

	// Config value comparison (parse both YAMLs, compare actual values)
	var configSyncStatus string // "in_sync", "mismatch", "unknown"
	var configDiffCount int
	if cfg != nil {
		infraData, _ := s.store.GetInfraBackup(customerID)
		if infraData != nil {
			controllerYAML := extractControllerYAML(infraData)
			if controllerYAML != "" {
				templateYAML := defaultControllerTemplate
				if s.templateFetcher != nil {
					templateYAML = s.templateFetcher.Template()
				}
				if hubYAML, err := configgen.Generate(templateYAML, cfg); err == nil {
					diffs := compareYAMLValues(hubYAML, controllerYAML)
					configDiffCount = len(diffs)
					if configDiffCount == 0 {
						configSyncStatus = "in_sync"
					} else {
						configSyncStatus = "mismatch"
					}
				}
			} else {
				configSyncStatus = "unknown"
			}
		} else {
			configSyncStatus = "unknown"
		}
	}

	// Version check
	var latestVersion string
	var updateAvailable bool
	if s.versionChecker != nil && customer != nil {
		latestVersion = s.versionChecker.LatestVersion()
		if latestVersion != "" && customer.ControllerVersion != "" {
			updateAvailable = latestVersion != customer.ControllerVersion && compareVersions(latestVersion, customer.ControllerVersion) > 0
		}
	}

	// History, notifications, events, infra backup
	var history []store.CustomerSummary
	var notifPrefs *store.NotificationPrefs
	var recentNotifs []store.NotificationLogEntry
	var infraMeta *store.InfraBackupMeta
	var infraBackupAge string
	var infraBackupVersions []store.InfraBackupVersion
	var events []store.Event
	var eventCounts map[string]int

	var appTelemetry []store.CustomerAppSummary

	if customer != nil {
		history, _ = s.store.GetCustomerHistory(customerID, 24*time.Hour)
		notifPrefs, _ = s.store.GetNotificationPrefs(customerID)
		recentNotifs, _ = s.store.GetRecentNotifications(customerID, 10)
		infraMeta, _ = s.store.GetInfraBackupMeta(customerID)
		if infraMeta != nil {
			infraBackupAge = timeAgo(infraMeta.UpdatedAt)
		}
		infraBackupVersions, _ = s.store.ListInfraBackupVersions(customerID)
		events, _ = s.store.GetRecentEvents(customerID, 50)
		eventCounts, _ = s.store.CountEventsBySeverity(customerID, time.Now().Add(-24*time.Hour))
		appTelemetry, _ = s.store.GetCustomerAppSummary(customerID, time.Now().Add(-7*24*time.Hour))
	}

	type pageData struct {
		CustomerID   string
		CustomerName string
		Domain       string
		Email        string

		HasConfig bool
		Config    *store.CustomerConfig
		Overrides map[string]interface{}
		IsBlocked bool

		HasReports    bool
		Customer      *store.CustomerSummary
		Report        map[string]interface{}
		OverallStatus string

		LatestVersion   string
		UpdateAvailable bool
		ControllerURL   string

		ConfigSyncStatus string // "in_sync", "mismatch", "unknown"
		ConfigDiffCount  int

		InfraBackup         *store.InfraBackupMeta
		InfraBackupAge      string
		InfraBackupVersions []store.InfraBackupVersion
		NotifPrefs          *store.NotificationPrefs
		RecentNotifications []store.NotificationLogEntry
		History             []store.CustomerSummary

		Events      []store.Event
		EventCounts map[string]int // severity → count (last 24h)

		AppTelemetry    []store.CustomerAppSummary
		HasAppTelemetry bool

		Flash     string
		ActiveNav string
		CSRFField template.HTML
		CSRFToken string
	}

	data := pageData{
		CustomerID:   customerID,
		CustomerName: name,
		Domain:       domain,
		Email:        email,

		HasConfig: cfg != nil,
		Config:    cfg,
		Overrides: overrides,
		IsBlocked: cfg != nil && cfg.Status == "blocked",

		HasReports:    customer != nil,
		Customer:      customer,
		Report:        report,
		OverallStatus: overallStatus,

		LatestVersion:   latestVersion,
		UpdateAvailable: updateAvailable,
		ControllerURL:   controllerURL,

		ConfigSyncStatus: configSyncStatus,
		ConfigDiffCount:  configDiffCount,

		InfraBackup:         infraMeta,
		InfraBackupAge:      infraBackupAge,
		InfraBackupVersions: infraBackupVersions,
		NotifPrefs:          notifPrefs,
		RecentNotifications: recentNotifs,
		History:             history,

		Events:      events,
		EventCounts: eventCounts,

		AppTelemetry:    appTelemetry,
		HasAppTelemetry: len(appTelemetry) > 0,

		Flash:     r.URL.Query().Get("flash"),
		ActiveNav: "configs",
		CSRFField: s.csrfField(r),
		CSRFToken: s.csrfToken(r),
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := s.templates.ExecuteTemplate(w, "customer_unified.html", data); err != nil {
		s.logger.Printf("[ERROR] Template render: %v", err)
	}
}

// handleConfigNewForm shows the form to create a new customer config.
func (s *Server) handleConfigNewForm(w http.ResponseWriter, r *http.Request) {
	data := struct {
		IsNew     bool
		Config    *store.CustomerConfig
		Overrides map[string]interface{}
		ActiveNav string
		Error     string
		CSRFField template.HTML
	}{
		IsNew:     true,
		Config:    &store.CustomerConfig{},
		Overrides: make(map[string]interface{}),
		ActiveNav: "configs",
		CSRFField: s.csrfField(r),
	}
	s.templates.ExecuteTemplate(w, "config_form.html", data)
}

// handleConfigCreate processes the form submission to create a new config.
func (s *Server) handleConfigCreate(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Bad request", http.StatusBadRequest)
		return
	}

	customerID := strings.TrimSpace(r.FormValue("customer_id"))
	if customerID == "" || !validCustomerID.MatchString(customerID) {
		s.renderConfigForm(w, r, true, &store.CustomerConfig{
			CustomerName: r.FormValue("customer_name"),
			Domain:       r.FormValue("domain"),
			Email:        r.FormValue("email"),
		}, nil, "Invalid Customer ID. Use only letters, numbers, dots, and hyphens.")
		return
	}

	// Check for duplicates
	existing, _ := s.store.GetCustomerConfig(customerID)
	if existing != nil {
		s.renderConfigForm(w, r, true, &store.CustomerConfig{
			CustomerID:   customerID,
			CustomerName: r.FormValue("customer_name"),
			Domain:       r.FormValue("domain"),
			Email:        r.FormValue("email"),
		}, nil, fmt.Sprintf("Customer ID %q already exists.", customerID))
		return
	}

	// Generate credentials
	retrievalPassword, err := configgen.RandomPassphrase(5)
	if err != nil {
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}
	apiKey, err := configgen.RandomHex(32)
	if err != nil {
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	// Build config_json from optional form fields
	configJSON := buildConfigJSON(r)

	cfg := &store.CustomerConfig{
		CustomerID:        customerID,
		CustomerName:      strings.TrimSpace(r.FormValue("customer_name")),
		Domain:            strings.TrimSpace(r.FormValue("domain")),
		Email:             strings.TrimSpace(r.FormValue("email")),
		RetrievalPassword: retrievalPassword,
		APIKey:            apiKey,
		ConfigJSON:        configJSON,
	}

	if err := s.store.SaveCustomerConfig(cfg); err != nil {
		s.logger.Printf("[ERROR] Failed to save config for %s: %v", customerID, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	s.logger.Printf("[INFO] Customer config created: %s", customerID)
	http.Redirect(w, r, "/customers/"+customerID+"?flash=created", http.StatusSeeOther)
}

// handleConfigEditForm shows the edit form for a customer config.
func (s *Server) handleConfigEditForm(w http.ResponseWriter, r *http.Request, customerID string) {
	cfg, err := s.store.GetCustomerConfig(customerID)
	if err != nil || cfg == nil {
		http.NotFound(w, r)
		return
	}

	var overrides map[string]interface{}
	json.Unmarshal([]byte(cfg.ConfigJSON), &overrides)

	data := struct {
		IsNew     bool
		Config    *store.CustomerConfig
		Overrides map[string]interface{}
		ActiveNav string
		Error     string
		CSRFField template.HTML
	}{
		IsNew:     false,
		Config:    cfg,
		Overrides: overrides,
		ActiveNav: "configs",
		CSRFField: s.csrfField(r),
	}
	s.templates.ExecuteTemplate(w, "config_form.html", data)
}

// handleConfigUpdate processes the edit form submission.
func (s *Server) handleConfigUpdate(w http.ResponseWriter, r *http.Request, customerID string) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Bad request", http.StatusBadRequest)
		return
	}

	cfg, err := s.store.GetCustomerConfig(customerID)
	if err != nil || cfg == nil {
		http.NotFound(w, r)
		return
	}

	cfg.CustomerName = strings.TrimSpace(r.FormValue("customer_name"))
	cfg.Domain = strings.TrimSpace(r.FormValue("domain"))
	cfg.Email = strings.TrimSpace(r.FormValue("email"))
	cfg.ConfigJSON = buildConfigJSON(r)

	if err := s.store.SaveCustomerConfig(cfg); err != nil {
		s.logger.Printf("[ERROR] Failed to update config for %s: %v", customerID, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	s.logger.Printf("[INFO] Customer config updated: %s", customerID)
	http.Redirect(w, r, "/customers/"+customerID+"?flash=updated", http.StatusSeeOther)
}

// handleConfigDelete deletes a customer config.
func (s *Server) handleConfigDelete(w http.ResponseWriter, r *http.Request, customerID string) {
	if err := s.store.DeleteCustomerConfig(customerID); err != nil {
		s.logger.Printf("[ERROR] Failed to delete config %s: %v", customerID, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	s.logger.Printf("[INFO] Customer config deleted: %s", customerID)
	http.Redirect(w, r, "/configs?flash=deleted", http.StatusSeeOther)
}

// handleConfigPreview returns the generated YAML for a customer config.
func (s *Server) handleConfigPreview(w http.ResponseWriter, r *http.Request, customerID string) {
	cfg, err := s.store.GetCustomerConfig(customerID)
	if err != nil || cfg == nil {
		http.NotFound(w, r)
		return
	}

	templateYAML := defaultControllerTemplate
	if s.templateFetcher != nil {
		templateYAML = s.templateFetcher.Template()
	}

	yamlOutput, err := configgen.Generate(templateYAML, cfg)
	if err != nil {
		s.logger.Printf("[ERROR] Failed to generate preview for %s: %v", customerID, err)
		http.Error(w, "Generation error: "+err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/yaml; charset=utf-8")
	w.Write([]byte(yamlOutput))
}

// handleConfigRegenPassword regenerates the retrieval password.
func (s *Server) handleConfigRegenPassword(w http.ResponseWriter, r *http.Request, customerID string) {
	newPassword, err := configgen.RandomPassphrase(5)
	if err != nil {
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	if err := s.store.UpdateRetrievalPassword(customerID, newPassword); err != nil {
		s.logger.Printf("[ERROR] Failed to regen password for %s: %v", customerID, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	s.logger.Printf("[INFO] Retrieval password regenerated for %s", customerID)
	http.Redirect(w, r, "/customers/"+customerID+"?flash=password_regenerated", http.StatusSeeOther)
}

// handleBlockCustomer sets a customer's status to "blocked".
func (s *Server) handleBlockCustomer(w http.ResponseWriter, r *http.Request, customerID string) {
	cfg, _ := s.store.GetCustomerConfig(customerID)
	if cfg == nil {
		http.NotFound(w, r)
		return
	}
	if err := s.store.SetCustomerConfigStatus(customerID, "blocked"); err != nil {
		s.logger.Printf("[ERROR] Failed to block %s: %v", customerID, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}
	s.logger.Printf("[INFO] Customer blocked: %s", customerID)
	http.Redirect(w, r, "/customers/"+customerID+"?flash=blocked", http.StatusSeeOther)
}

// handleUnblockCustomer sets a customer's status back to "active".
func (s *Server) handleUnblockCustomer(w http.ResponseWriter, r *http.Request, customerID string) {
	cfg, _ := s.store.GetCustomerConfig(customerID)
	if cfg == nil {
		http.NotFound(w, r)
		return
	}
	if err := s.store.SetCustomerConfigStatus(customerID, "active"); err != nil {
		s.logger.Printf("[ERROR] Failed to unblock %s: %v", customerID, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}
	s.logger.Printf("[INFO] Customer unblocked: %s", customerID)
	http.Redirect(w, r, "/customers/"+customerID+"?flash=unblocked", http.StatusSeeOther)
}

// handlePushConfig sends the generated YAML config to the controller.
func (s *Server) handlePushConfig(w http.ResponseWriter, r *http.Request, customerID string) {
	cfg, err := s.store.GetCustomerConfig(customerID)
	if err != nil || cfg == nil {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusNotFound)
		w.Write([]byte(`{"ok":false,"error":"No config found for this customer"}`))
		return
	}

	// Get controller URL from latest report
	customer, _ := s.store.GetCustomer(customerID)
	controllerURL := ""
	if customer != nil {
		controllerURL = customer.ControllerURL
		if controllerURL == "" {
			var rpt struct {
				ControllerURL string `json:"controller_url"`
			}
			json.Unmarshal([]byte(customer.ReportJSON), &rpt)
			controllerURL = rpt.ControllerURL
		}
	}
	if controllerURL == "" {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(`{"ok":false,"error":"Controller URL not available — waiting for first report"}`))
		return
	}

	// Generate YAML
	templateYAML := defaultControllerTemplate
	if s.templateFetcher != nil {
		templateYAML = s.templateFetcher.Template()
	}
	yamlOutput, err := configgen.Generate(templateYAML, cfg)
	if err != nil {
		s.logger.Printf("[ERROR] Failed to generate config for push to %s: %v", customerID, err)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(`{"ok":false,"error":"Failed to generate config"}`))
		return
	}

	// POST to controller
	pushURL := controllerURL + "/api/config/apply"
	req, err := http.NewRequest("POST", pushURL, strings.NewReader(yamlOutput))
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(`{"ok":false,"error":"Failed to create request"}`))
		return
	}
	req.Header.Set("Authorization", "Bearer "+s.apiKey)
	req.Header.Set("Content-Type", "text/yaml")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		s.logger.Printf("[ERROR] Push config to %s failed: %v", pushURL, err)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadGateway)
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": fmt.Sprintf("Controller unreachable: %v", err)})
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
	s.logger.Printf("[INFO] Push config to %s — controller responded %d: %s", customerID, resp.StatusCode, string(body))

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(resp.StatusCode)
	w.Write(body)
}

// handleCreateConfigFromReport auto-creates a config entry from report data.
func (s *Server) handleCreateConfigFromReport(w http.ResponseWriter, r *http.Request, customerID string) {
	// Check if config already exists
	existing, _ := s.store.GetCustomerConfig(customerID)
	if existing != nil {
		http.Redirect(w, r, "/configs/"+customerID+"/edit", http.StatusSeeOther)
		return
	}

	// Get report data to pre-fill
	customer, _ := s.store.GetCustomer(customerID)
	name := customerID
	if customer != nil && customer.CustomerName != "" {
		name = customer.CustomerName
	}

	// Generate credentials
	retrievalPassword, _ := configgen.RandomPassphrase(5)
	apiKey, _ := configgen.RandomHex(32)

	cfg := &store.CustomerConfig{
		CustomerID:        customerID,
		CustomerName:      name,
		RetrievalPassword: retrievalPassword,
		APIKey:            apiKey,
		ConfigJSON:        "{}",
	}

	if err := s.store.SaveCustomerConfig(cfg); err != nil {
		s.logger.Printf("[ERROR] Failed to create config from report for %s: %v", customerID, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	s.logger.Printf("[INFO] Config auto-created from report for %s", customerID)
	http.Redirect(w, r, "/configs/"+customerID+"/edit", http.StatusSeeOther)
}

// renderConfigForm is a helper to re-render the form with an error.
func (s *Server) renderConfigForm(w http.ResponseWriter, r *http.Request, isNew bool, cfg *store.CustomerConfig, overrides map[string]interface{}, errMsg string) {
	if overrides == nil {
		overrides = make(map[string]interface{})
	}
	data := struct {
		IsNew     bool
		Config    *store.CustomerConfig
		Overrides map[string]interface{}
		ActiveNav string
		Error     string
		CSRFField template.HTML
	}{
		IsNew:     isNew,
		Config:    cfg,
		Overrides: overrides,
		ActiveNav: "configs",
		Error:     errMsg,
		CSRFField: s.csrfField(r),
	}
	s.templates.ExecuteTemplate(w, "config_form.html", data)
}

// buildConfigJSON builds the config_json from optional form fields.
func buildConfigJSON(r *http.Request) string {
	overrides := make(map[string]interface{})

	// Infrastructure
	infra := make(map[string]interface{})
	if v := strings.TrimSpace(r.FormValue("cf_tunnel_token")); v != "" {
		infra["cf_tunnel_token"] = v
	}
	if v := strings.TrimSpace(r.FormValue("cf_api_token")); v != "" {
		infra["cf_api_token"] = v
	}
	if len(infra) > 0 {
		overrides["infrastructure"] = infra
	}

	// Git
	git := make(map[string]interface{})
	if v := strings.TrimSpace(r.FormValue("git_username")); v != "" {
		git["username"] = v
	}
	if v := strings.TrimSpace(r.FormValue("git_token")); v != "" {
		git["token"] = v
	}
	if len(git) > 0 {
		overrides["git"] = git
	}

	data, _ := json.Marshal(overrides)
	return string(data)
}

// --- Config comparison helpers ---

// extractControllerYAML decodes the controller.yaml from an infra backup JSON payload.
func extractControllerYAML(infraData []byte) string {
	var parsed struct {
		ControllerConfigB64 string `json:"controller_config_b64"`
	}
	if err := json.Unmarshal(infraData, &parsed); err != nil || parsed.ControllerConfigB64 == "" {
		return ""
	}
	data, err := base64.StdEncoding.DecodeString(parsed.ControllerConfigB64)
	if err != nil {
		return ""
	}
	return string(data)
}

// volatileKeys are YAML keys ignored during config comparison (always differ or deprecated).
var volatileKeys = map[string]bool{
	"web.session_secret": true,
}

// sensitiveKeyParts are substrings that indicate a value should be masked in diff output.
var sensitiveKeyParts = []string{"token", "password", "secret", "api_key"}

// flattenYAML recursively flattens a nested map into dot-separated key-value pairs.
func flattenYAML(m map[string]interface{}, prefix string) map[string]string {
	result := make(map[string]string)
	for k, v := range m {
		key := k
		if prefix != "" {
			key = prefix + "." + k
		}
		switch val := v.(type) {
		case map[string]interface{}:
			for fk, fv := range flattenYAML(val, key) {
				result[fk] = fv
			}
		case []interface{}:
			for i, item := range val {
				itemKey := fmt.Sprintf("%s.%d", key, i)
				if sub, ok := item.(map[string]interface{}); ok {
					for fk, fv := range flattenYAML(sub, itemKey) {
						result[fk] = fv
					}
				} else {
					result[itemKey] = fmt.Sprintf("%v", item)
				}
			}
		default:
			result[key] = fmt.Sprintf("%v", v)
		}
	}
	return result
}

// configDiff represents a single key-value difference between two configs.
type configDiff struct {
	Key        string `json:"key"`
	HubValue   string `json:"hub"`
	CtrlValue  string `json:"controller"`
	Status     string `json:"status"` // "changed", "hub_only", "controller_only"
}

// compareYAMLValues parses two YAML strings and returns their value differences.
// Volatile keys (e.g., web.session_secret) are excluded.
func compareYAMLValues(hubYAML, controllerYAML string) []configDiff {
	var hubMap, ctrlMap map[string]interface{}
	yaml.Unmarshal([]byte(hubYAML), &hubMap)
	yaml.Unmarshal([]byte(controllerYAML), &ctrlMap)

	hubFlat := flattenYAML(hubMap, "")
	ctrlFlat := flattenYAML(ctrlMap, "")

	var diffs []configDiff

	// Keys in hub but different/missing in controller
	for k, hv := range hubFlat {
		if volatileKeys[k] {
			continue
		}
		cv, exists := ctrlFlat[k]
		if !exists {
			if hv != "" && hv != "<nil>" {
				diffs = append(diffs, configDiff{Key: k, HubValue: hv, CtrlValue: "(not set)", Status: "hub_only"})
			}
		} else if hv != cv {
			diffs = append(diffs, configDiff{Key: k, HubValue: hv, CtrlValue: cv, Status: "changed"})
		}
	}

	// Keys in controller but missing in hub
	for k, cv := range ctrlFlat {
		if volatileKeys[k] {
			continue
		}
		if _, exists := hubFlat[k]; !exists {
			if cv != "" && cv != "<nil>" {
				diffs = append(diffs, configDiff{Key: k, HubValue: "(not set)", CtrlValue: cv, Status: "controller_only"})
			}
		}
	}

	sort.Slice(diffs, func(i, j int) bool { return diffs[i].Key < diffs[j].Key })
	return diffs
}

// maskSensitive masks a value if the key contains sensitive substrings.
func maskSensitive(key, value string) string {
	if value == "" || value == "(not set)" {
		return value
	}
	keyLower := strings.ToLower(key)
	for _, part := range sensitiveKeyParts {
		if strings.Contains(keyLower, part) {
			if len(value) > 8 {
				return "***" + value[len(value)-4:]
			}
			return "***"
		}
	}
	return value
}

// handleConfigDiff returns a JSON diff between Hub-generated and controller's live config.
func (s *Server) handleConfigDiff(w http.ResponseWriter, r *http.Request, customerID string) {
	cfg, err := s.store.GetCustomerConfig(customerID)
	if err != nil || cfg == nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "No config found"})
		return
	}

	// Get controller URL
	customer, _ := s.store.GetCustomer(customerID)
	controllerURL := ""
	if customer != nil {
		controllerURL = customer.ControllerURL
		if controllerURL == "" {
			var rpt struct {
				ControllerURL string `json:"controller_url"`
			}
			json.Unmarshal([]byte(customer.ReportJSON), &rpt)
			controllerURL = rpt.ControllerURL
		}
	}
	if controllerURL == "" {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Controller URL not available"})
		return
	}

	// Fetch live config from controller
	fetchURL := controllerURL + "/api/config"
	req, err := http.NewRequest("GET", fetchURL, nil)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Failed to create request"})
		return
	}
	req.Header.Set("Authorization", "Bearer "+s.apiKey)

	client := &http.Client{Timeout: 15 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": fmt.Sprintf("Controller unreachable: %v", err)})
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": fmt.Sprintf("Controller returned HTTP %d", resp.StatusCode)})
		return
	}

	controllerYAML, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Failed to read controller response"})
		return
	}

	// Generate Hub YAML
	templateYAML := defaultControllerTemplate
	if s.templateFetcher != nil {
		templateYAML = s.templateFetcher.Template()
	}
	hubYAML, err := configgen.Generate(templateYAML, cfg)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Failed to generate Hub config"})
		return
	}

	// Compare
	diffs := compareYAMLValues(hubYAML, string(controllerYAML))

	// Mask sensitive values
	for i := range diffs {
		diffs[i].HubValue = maskSensitive(diffs[i].Key, diffs[i].HubValue)
		diffs[i].CtrlValue = maskSensitive(diffs[i].Key, diffs[i].CtrlValue)
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"ok":         true,
		"in_sync":    len(diffs) == 0,
		"diff_count": len(diffs),
		"diffs":      diffs,
	})
}

// handlePullConfig fetches the controller's live config and imports it into the Hub.
func (s *Server) handlePullConfig(w http.ResponseWriter, r *http.Request, customerID string) {
	cfg, err := s.store.GetCustomerConfig(customerID)
	if err != nil || cfg == nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "No config found"})
		return
	}

	// Get controller URL
	customer, _ := s.store.GetCustomer(customerID)
	controllerURL := ""
	if customer != nil {
		controllerURL = customer.ControllerURL
		if controllerURL == "" {
			var rpt struct {
				ControllerURL string `json:"controller_url"`
			}
			json.Unmarshal([]byte(customer.ReportJSON), &rpt)
			controllerURL = rpt.ControllerURL
		}
	}
	if controllerURL == "" {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Controller URL not available"})
		return
	}

	// Fetch live config from controller
	fetchURL := controllerURL + "/api/config"
	req, err := http.NewRequest("GET", fetchURL, nil)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Failed to create request"})
		return
	}
	req.Header.Set("Authorization", "Bearer "+s.apiKey)

	client := &http.Client{Timeout: 15 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": fmt.Sprintf("Controller unreachable: %v", err)})
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": fmt.Sprintf("Controller returned HTTP %d", resp.StatusCode)})
		return
	}

	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Failed to read controller response"})
		return
	}

	// Parse controller's YAML
	var parsed map[string]interface{}
	if err := yaml.Unmarshal(body, &parsed); err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Failed to parse controller YAML"})
		return
	}

	// Extract identity fields
	if customer, ok := parsed["customer"].(map[string]interface{}); ok {
		if v, ok := customer["name"].(string); ok && v != "" {
			cfg.CustomerName = v
		}
		if v, ok := customer["domain"].(string); ok && v != "" {
			cfg.Domain = v
		}
		if v, ok := customer["email"].(string); ok && v != "" {
			cfg.Email = v
		}
	}

	// Build config_json from override fields
	overrides := make(map[string]interface{})

	// Infrastructure tokens
	if infra, ok := parsed["infrastructure"].(map[string]interface{}); ok {
		infraOverrides := make(map[string]interface{})
		if v, ok := infra["cf_tunnel_token"].(string); ok && v != "" {
			infraOverrides["cf_tunnel_token"] = v
		}
		if v, ok := infra["cf_api_token"].(string); ok && v != "" {
			infraOverrides["cf_api_token"] = v
		}
		if len(infraOverrides) > 0 {
			overrides["infrastructure"] = infraOverrides
		}
	}

	// Git credentials
	if git, ok := parsed["git"].(map[string]interface{}); ok {
		gitOverrides := make(map[string]interface{})
		if v, ok := git["username"].(string); ok && v != "" {
			gitOverrides["username"] = v
		}
		if v, ok := git["token"].(string); ok && v != "" {
			gitOverrides["token"] = v
		}
		if len(gitOverrides) > 0 {
			overrides["git"] = gitOverrides
		}
	}

	configJSON, _ := json.Marshal(overrides)
	cfg.ConfigJSON = string(configJSON)

	if err := s.store.SaveCustomerConfig(cfg); err != nil {
		s.logger.Printf("[ERROR] Pull config: failed to update config for %s: %v", customerID, err)
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Failed to save config"})
		return
	}

	s.logger.Printf("[INFO] Config pulled from controller for %s", customerID)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Config imported from controller"})
}

// handleGeoDisable removes all [felhom-geo] WAF rules from Cloudflare for a customer,
// and notifies the controller to disable geo-restriction in its settings.
func (s *Server) handleGeoDisable(w http.ResponseWriter, r *http.Request, customerID string) {
	cfg, err := s.store.GetCustomerConfig(customerID)
	if err != nil || cfg == nil {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusNotFound)
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Customer not found"})
		return
	}

	// Extract CF API token from config_json → infrastructure.cf_api_token
	var overrides map[string]interface{}
	if err := json.Unmarshal([]byte(cfg.ConfigJSON), &overrides); err != nil {
		overrides = make(map[string]interface{})
	}
	var cfToken string
	if infra, ok := overrides["infrastructure"].(map[string]interface{}); ok {
		cfToken, _ = infra["cf_api_token"].(string)
	}
	if cfToken == "" {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "No Cloudflare API token configured for this customer"})
		return
	}
	if cfg.Domain == "" {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "No domain configured for this customer"})
		return
	}

	// 1. Remove WAF rules directly via Cloudflare API
	if err := cfClient.RemoveGeoRules(cfToken, cfg.Domain, s.logger); err != nil {
		s.logger.Printf("[ERROR] Geo disable for %s: Cloudflare removal failed: %v", customerID, err)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadGateway)
		json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": fmt.Sprintf("Cloudflare API error: %v", err)})
		return
	}

	s.logger.Printf("[INFO] Geo disable for %s: Cloudflare WAF rules removed", customerID)

	// 2. Background: notify controller to disable geo in settings (retry for up to 10 min)
	customer, _ := s.store.GetCustomer(customerID)
	controllerURL := ""
	if customer != nil {
		controllerURL = customer.ControllerURL
	}
	if controllerURL == "" {
		var rpt struct {
			ControllerURL string `json:"controller_url"`
		}
		if customer != nil {
			json.Unmarshal([]byte(customer.ReportJSON), &rpt)
			controllerURL = rpt.ControllerURL
		}
	}

	if controllerURL != "" && s.apiKey != "" {
		go s.notifyControllerGeoDisable(customerID, controllerURL)
	} else {
		s.logger.Printf("[WARN] Geo disable for %s: cannot notify controller (no URL or API key)", customerID)
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Geo-restriction removed from Cloudflare. Controller will be notified."})
}

// notifyControllerGeoDisable retries sending geo-disable to the controller every 30s for up to 10 min.
func (s *Server) notifyControllerGeoDisable(customerID, controllerURL string) {
	geoURL := controllerURL + "/api/geo/settings"

	for attempt := 1; attempt <= 20; attempt++ {
		req, err := http.NewRequest("POST", geoURL, strings.NewReader(`{"enabled":false,"allowed_countries":["HU"]}`))
		if err != nil {
			s.logger.Printf("[ERROR] Geo disable notify %s attempt %d: create request: %v", customerID, attempt, err)
			return
		}
		req.Header.Set("Authorization", "Bearer "+s.apiKey)
		req.Header.Set("Content-Type", "application/json")

		client := &http.Client{Timeout: 15 * time.Second}
		resp, err := client.Do(req)
		if err != nil {
			s.logger.Printf("[WARN] Geo disable notify %s attempt %d: %v", customerID, attempt, err)
			time.Sleep(30 * time.Second)
			continue
		}
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
		resp.Body.Close()

		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			s.logger.Printf("[INFO] Geo disable notify %s: controller confirmed (attempt %d): %s", customerID, attempt, string(body))
			return
		}
		s.logger.Printf("[WARN] Geo disable notify %s attempt %d: status %d: %s", customerID, attempt, resp.StatusCode, string(body))
		time.Sleep(30 * time.Second)
	}

	s.logger.Printf("[ERROR] Geo disable notify %s: gave up after 20 attempts", customerID)
}
