package cloudflare

import (
	"context"
	"fmt"
	"log"
	"sort"
	"sync"

	"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)

// StackLister provides deployed app hostnames (interface to break circular import).
type StackLister interface {
	// GetDeployedHostnames returns appName → full hostname (e.g., "nextcloud.demo-felhom.eu").
	GetDeployedHostnames() map[string]string
}

// GeoSyncManager synchronizes geo-restriction settings to Cloudflare WAF rules.
type GeoSyncManager struct {
	client   *Client
	settings *settings.Settings
	domain   string
	stacks   StackLister
	logger   *log.Logger
	debug    bool

	mu      sync.Mutex
	running bool
}

// NewGeoSyncManager creates a new geo sync manager.
func NewGeoSyncManager(client *Client, sett *settings.Settings, domain string, stacks StackLister, logger *log.Logger) *GeoSyncManager {
	return &GeoSyncManager{
		client:   client,
		settings: sett,
		domain:   domain,
		stacks:   stacks,
		logger:   logger,
	}
}

// SetDebug enables or disables debug logging for the geo sync manager.
func (g *GeoSyncManager) SetDebug(debug bool) {
	g.debug = debug
}

// IsRunning returns true if a sync operation is in progress.
func (g *GeoSyncManager) IsRunning() bool {
	g.mu.Lock()
	defer g.mu.Unlock()
	return g.running
}

// Sync reads current geo settings and pushes/updates/deletes CF WAF rules.
func (g *GeoSyncManager) Sync(ctx context.Context) error {
	g.mu.Lock()
	if g.running {
		g.mu.Unlock()
		return fmt.Errorf("sync already in progress")
	}
	g.running = true
	g.mu.Unlock()

	defer func() {
		g.mu.Lock()
		g.running = false
		g.mu.Unlock()
	}()

	g.logger.Printf("[INFO] [cloudflare] Geo-restriction sync starting")

	geo := g.settings.GetGeoRestriction()

	// If geo is nil or disabled, delete all felhom rules and return.
	if geo == nil || !geo.Enabled {
		return g.deleteAllRules(ctx, geo)
	}

	g.logger.Printf("[INFO] [cloudflare] Starting sync for domain %s (%d allowed countries, %d app overrides)",
		g.domain, len(geo.AllowedCountries), len(geo.AppOverrides))

	// 1. Resolve zone ID (use cached value if available)
	zoneID := geo.ZoneID
	if zoneID == "" {
		if g.debug {
			g.logger.Printf("[DEBUG] [cloudflare] Zone ID not cached, resolving via API for domain %s", g.domain)
		}
		var err error
		zoneID, err = g.client.GetZoneID(ctx, g.domain)
		if err != nil {
			g.saveError(zoneID, "", err.Error())
			return fmt.Errorf("resolve zone: %w", err)
		}
		if g.debug {
			g.logger.Printf("[DEBUG] [cloudflare] Resolved zone ID: %s", zoneID)
		}
	} else if g.debug {
		g.logger.Printf("[DEBUG] [cloudflare] Using cached zone ID: %s", zoneID)
	}

	// 2. Get or create the custom WAF ruleset
	rulesetID := geo.RulesetID
	if rulesetID == "" {
		if g.debug {
			g.logger.Printf("[DEBUG] [cloudflare] Ruleset ID not cached, looking up for zone %s", zoneID)
		}
		var err error
		rulesetID, err = g.client.GetCustomRulesetID(ctx, zoneID)
		if err != nil {
			g.saveError(zoneID, "", err.Error())
			return fmt.Errorf("get ruleset: %w", err)
		}
		if rulesetID == "" {
			if g.debug {
				g.logger.Printf("[DEBUG] [cloudflare] No existing custom ruleset found, creating new one")
			}
			rulesetID, err = g.client.CreateCustomRuleset(ctx, zoneID)
			if err != nil {
				g.saveError(zoneID, "", err.Error())
				return fmt.Errorf("create ruleset: %w", err)
			}
		}
		if g.debug {
			g.logger.Printf("[DEBUG] [cloudflare] Using ruleset ID: %s", rulesetID)
		}
	} else if g.debug {
		g.logger.Printf("[DEBUG] [cloudflare] Using cached ruleset ID: %s", rulesetID)
	}

	// 3. List existing felhom-managed rules
	existing, err := g.client.GetFelhomRules(ctx, zoneID, rulesetID)
	if err != nil {
		g.saveError(zoneID, rulesetID, err.Error())
		return fmt.Errorf("list existing rules: %w", err)
	}
	if g.debug {
		g.logger.Printf("[DEBUG] [cloudflare] Found %d existing felhom rules", len(existing))
		for _, r := range existing {
			g.logger.Printf("[DEBUG] [cloudflare]   existing: %s (id=%s)", r.Description, r.ID)
		}
	}

	// 4. Build desired rules
	desired := g.buildDesiredRules(geo)
	if g.debug {
		g.logger.Printf("[DEBUG] [cloudflare] Built %d desired rules", len(desired))
		for _, d := range desired {
			g.logger.Printf("[DEBUG] [cloudflare]   desired: %s", d.description)
		}
	}

	// 5. Diff and apply
	if err := g.applyDiff(ctx, zoneID, rulesetID, existing, desired); err != nil {
		g.saveError(zoneID, rulesetID, err.Error())
		return fmt.Errorf("apply diff: %w", err)
	}

	// 6. Save success state
	g.saveError(zoneID, rulesetID, "")
	// Count rules for summary
	finalRules, _ := g.client.GetFelhomRules(ctx, zoneID, rulesetID)
	g.logger.Printf("[INFO] [cloudflare] Geo-restriction sync complete (%d active rules)", len(finalRules))
	return nil
}

// deleteAllRules removes all felhom-geo rules when the feature is disabled.
func (g *GeoSyncManager) deleteAllRules(ctx context.Context, geo *settings.GeoRestriction) error {
	// Need zone and ruleset IDs to delete rules
	zoneID := ""
	rulesetID := ""
	if geo != nil {
		zoneID = geo.ZoneID
		rulesetID = geo.RulesetID
	}

	if zoneID == "" || rulesetID == "" {
		if g.debug {
			g.logger.Printf("[DEBUG] [cloudflare] deleteAllRules: no cached zone/ruleset IDs, nothing to clean up")
		}
		// No cached IDs — nothing to clean up
		return nil
	}

	if g.debug {
		g.logger.Printf("[DEBUG] [cloudflare] deleteAllRules: listing rules for zone=%s ruleset=%s", zoneID, rulesetID)
	}

	existing, err := g.client.GetFelhomRules(ctx, zoneID, rulesetID)
	if err != nil {
		g.logger.Printf("[WARN] [cloudflare] Could not list rules for cleanup: %v", err)
		return nil
	}

	if g.debug {
		g.logger.Printf("[DEBUG] [cloudflare] deleteAllRules: found %d felhom rules to delete", len(existing))
	}

	deleted := 0
	for _, r := range existing {
		if err := g.client.DeleteRule(ctx, zoneID, rulesetID, r.ID); err != nil {
			g.logger.Printf("[ERROR] [cloudflare] Failed to delete WAF rule %s: %v", r.ID, err)
		} else {
			deleted++
		}
	}

	if len(existing) > 0 {
		g.logger.Printf("[INFO] [cloudflare] Deleted %d felhom-geo rules (feature disabled)", len(existing))
		if g.debug {
			g.logger.Printf("[DEBUG] [cloudflare] deleteAllRules: successfully deleted %d/%d rules", deleted, len(existing))
		}
	}

	g.saveError(zoneID, rulesetID, "")
	return nil
}

// desiredRule describes a rule that should exist.
type desiredRule struct {
	description string
	expression  string
}

// buildDesiredRules builds the set of rules that should exist in Cloudflare.
func (g *GeoSyncManager) buildDesiredRules(geo *settings.GeoRestriction) []desiredRule {
	var rules []desiredRule

	hostnames := g.stacks.GetDeployedHostnames()
	if g.debug {
		g.logger.Printf("[DEBUG] [cloudflare] buildDesiredRules: %d deployed hostnames from stacks", len(hostnames))
	}

	// Collect app hostnames that have overrides (to exclude from global rule)
	var excludeHostnames []string
	overrideApps := make(map[string]bool)

	for appName, override := range geo.AppOverrides {
		hostname, ok := hostnames[appName]
		if !ok {
			if g.debug {
				g.logger.Printf("[DEBUG] [cloudflare] buildDesiredRules: skipping override for %q (not deployed)", appName)
			}
			continue // app not deployed, skip
		}
		overrideApps[appName] = true
		excludeHostnames = append(excludeHostnames, hostname)

		// Per-app rule
		rules = append(rules, desiredRule{
			description: AppRuleDescription(appName),
			expression:  BuildAppExpression(hostname, override.AllowedCountries),
		})
	}

	if g.debug && len(overrideApps) > 0 {
		g.logger.Printf("[DEBUG] [cloudflare] buildDesiredRules: %d app overrides active (deployed)", len(overrideApps))
	}

	// Sort exclude hostnames for deterministic expression
	sort.Strings(excludeHostnames)

	// Global rule (excludes apps with their own rules)
	globalExpr := BuildGlobalExpression(geo.AllowedCountries, excludeHostnames)
	rules = append(rules, desiredRule{
		description: globalRuleDesc,
		expression:  globalExpr,
	})

	if g.debug {
		g.logger.Printf("[DEBUG] [cloudflare] buildDesiredRules: global rule expression: %s", globalExpr)
	}

	return rules
}

// applyDiff applies the difference between existing and desired rules.
func (g *GeoSyncManager) applyDiff(ctx context.Context, zoneID, rulesetID string, existing []GeoRule, desired []desiredRule) error {
	// Index existing by description
	existingByDesc := make(map[string]GeoRule)
	for _, r := range existing {
		existingByDesc[r.Description] = r
	}

	// Index desired by description
	desiredByDesc := make(map[string]desiredRule)
	for _, r := range desired {
		desiredByDesc[r.description] = r
	}

	// Create or update
	for _, d := range desired {
		if ex, ok := existingByDesc[d.description]; ok {
			// Rule exists — check if expression changed
			if ex.Expression != d.expression {
				if g.debug {
					g.logger.Printf("[DEBUG] [cloudflare] applyDiff: updating rule %q (id=%s) — expression changed", d.description, ex.ID)
				}
				r := newBlockRule(d.description, d.expression)
				if err := g.client.UpdateRule(ctx, zoneID, rulesetID, ex.ID, r); err != nil {
					return fmt.Errorf("update rule %q: %w", d.description, err)
				}
				g.logger.Printf("[INFO] [cloudflare] Updated WAF rule %s", d.description)
			} else if g.debug {
				g.logger.Printf("[DEBUG] [cloudflare] applyDiff: rule %q unchanged, skipping", d.description)
			}
		} else {
			// New rule — create
			if g.debug {
				g.logger.Printf("[DEBUG] [cloudflare] applyDiff: creating new rule %q", d.description)
			}
			r := newBlockRule(d.description, d.expression)
			if _, err := g.client.CreateRule(ctx, zoneID, rulesetID, r); err != nil {
				return fmt.Errorf("create rule %q: %w", d.description, err)
			}
			g.logger.Printf("[INFO] [cloudflare] Created WAF rule %s", d.description)
		}
	}

	// Delete rules that are no longer desired
	for _, ex := range existing {
		if _, ok := desiredByDesc[ex.Description]; !ok {
			if g.debug {
				g.logger.Printf("[DEBUG] [cloudflare] applyDiff: deleting obsolete rule %q (id=%s)", ex.Description, ex.ID)
			}
			if err := g.client.DeleteRule(ctx, zoneID, rulesetID, ex.ID); err != nil {
				return fmt.Errorf("delete rule %q: %w", ex.Description, err)
			}
			g.logger.Printf("[INFO] [cloudflare] Deleted WAF rule %s", ex.Description)
		}
	}

	return nil
}

// saveError updates the sync state in settings.
func (g *GeoSyncManager) saveError(zoneID, rulesetID, errMsg string) {
	if errMsg != "" {
		g.logger.Printf("[ERROR] [cloudflare] Geo-sync error: %v", errMsg)
	}
	if err := g.settings.SetGeoSyncState(zoneID, rulesetID, errMsg); err != nil {
		g.logger.Printf("[WARN] [cloudflare] Failed to save sync state: %v", err)
	}
}
