package cloudflare

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
)

const (
	// rulePrefix identifies felhom-managed WAF rules.
	rulePrefix = "[felhom-geo]"

	// wafPhase is the Cloudflare ruleset phase for custom WAF rules.
	wafPhase = "http_request_firewall_custom"

	// globalRuleDesc is the description for the global geo-restriction rule.
	globalRuleDesc = "[felhom-geo] Global"

	// appRuleDescPrefix is the prefix for per-app geo-restriction rules.
	appRuleDescPrefix = "[felhom-geo] app:"
)

// ruleset represents a Cloudflare ruleset (minimal fields).
type ruleset struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Phase string `json:"phase"`
	Kind  string `json:"kind"`
}

// rule represents a Cloudflare custom rule.
type rule struct {
	ID               string            `json:"id,omitempty"`
	Description      string            `json:"description"`
	Expression       string            `json:"expression"`
	Action           string            `json:"action"`
	ActionParameters *actionParameters `json:"action_parameters,omitempty"`
	Enabled          *bool             `json:"enabled,omitempty"`
}

type actionParameters struct {
	Response *blockResponse `json:"response,omitempty"`
}

type blockResponse struct {
	StatusCode  int    `json:"status_code"`
	Content     string `json:"content"`
	ContentType string `json:"content_type"`
}

// GeoRule represents a felhom-managed WAF custom rule (for external consumption).
type GeoRule struct {
	ID          string
	Description string
	Expression  string
	Action      string
}

// GetCustomRulesetID returns the zone's http_request_firewall_custom ruleset ID.
// Returns empty string if no such ruleset exists yet.
func (c *Client) GetCustomRulesetID(ctx context.Context, zoneID string) (string, error) {
	path := fmt.Sprintf("/zones/%s/rulesets", zoneID)
	resp, err := c.do(ctx, "GET", path, nil)
	if err != nil {
		c.logger.Printf("[ERROR] [cloudflare] Failed to get custom ruleset: %v", err)
		return "", fmt.Errorf("list rulesets: %w", err)
	}

	var rulesets []ruleset
	if err := json.Unmarshal(resp.Result, &rulesets); err != nil {
		return "", fmt.Errorf("decode rulesets: %w", err)
	}

	if c.debug {
		c.logger.Printf("[DEBUG] [cloudflare] GetCustomRulesetID: found %d rulesets for zone %s", len(rulesets), zoneID)
	}

	for _, rs := range rulesets {
		if rs.Phase == wafPhase {
			if c.debug {
				c.logger.Printf("[DEBUG] [cloudflare] GetCustomRulesetID: matched ruleset %s (phase=%s)", rs.ID, wafPhase)
			}
			return rs.ID, nil
		}
	}

	if c.debug {
		c.logger.Printf("[DEBUG] [cloudflare] GetCustomRulesetID: no ruleset with phase %s found", wafPhase)
	}

	return "", nil
}

// CreateCustomRuleset creates the http_request_firewall_custom phase entry point ruleset.
func (c *Client) CreateCustomRuleset(ctx context.Context, zoneID string) (string, error) {
	path := fmt.Sprintf("/zones/%s/rulesets", zoneID)
	body := map[string]interface{}{
		"name":  "felhom custom rules",
		"kind":  "zone",
		"phase": wafPhase,
		"rules": []interface{}{},
	}

	resp, err := c.do(ctx, "POST", path, body)
	if err != nil {
		return "", fmt.Errorf("create ruleset: %w", err)
	}

	var rs ruleset
	if err := json.Unmarshal(resp.Result, &rs); err != nil {
		return "", fmt.Errorf("decode created ruleset: %w", err)
	}

	c.logger.Printf("[INFO] [cloudflare] Created custom ruleset %s for zone %s", rs.ID, zoneID)
	return rs.ID, nil
}

// GetRules returns all rules in a ruleset.
func (c *Client) GetRules(ctx context.Context, zoneID, rulesetID string) ([]rule, error) {
	path := fmt.Sprintf("/zones/%s/rulesets/%s", zoneID, rulesetID)
	resp, err := c.do(ctx, "GET", path, nil)
	if err != nil {
		c.logger.Printf("[WARN] [cloudflare] Failed to get WAF rules: %v", err)
		return nil, fmt.Errorf("get ruleset: %w", err)
	}

	var rs struct {
		Rules []rule `json:"rules"`
	}
	if err := json.Unmarshal(resp.Result, &rs); err != nil {
		return nil, fmt.Errorf("decode rules: %w", err)
	}

	if c.debug {
		c.logger.Printf("[DEBUG] [cloudflare] GetRules: %d total rules in ruleset %s", len(rs.Rules), rulesetID)
	}

	return rs.Rules, nil
}

// GetFelhomRules returns only rules with the [felhom-geo] prefix.
func (c *Client) GetFelhomRules(ctx context.Context, zoneID, rulesetID string) ([]GeoRule, error) {
	rules, err := c.GetRules(ctx, zoneID, rulesetID)
	if err != nil {
		return nil, err
	}

	var result []GeoRule
	for _, r := range rules {
		if strings.HasPrefix(r.Description, rulePrefix) {
			result = append(result, GeoRule{
				ID:          r.ID,
				Description: r.Description,
				Expression:  r.Expression,
				Action:      r.Action,
			})
		}
	}

	if c.debug {
		c.logger.Printf("[DEBUG] [cloudflare] GetFelhomRules: %d felhom rules out of %d total", len(result), len(rules))
	}

	return result, nil
}

// CreateRule adds a new rule to the ruleset.
func (c *Client) CreateRule(ctx context.Context, zoneID, rulesetID string, r rule) (string, error) {
	c.logger.Printf("[INFO] [cloudflare] Creating WAF rule: %s", r.Description)
	path := fmt.Sprintf("/zones/%s/rulesets/%s/rules", zoneID, rulesetID)
	resp, err := c.do(ctx, "POST", path, r)
	if err != nil {
		c.logger.Printf("[ERROR] [cloudflare] Failed to create WAF rule: %v", err)
		return "", fmt.Errorf("create rule: %w", err)
	}

	// The response is the full ruleset; find the new rule by description.
	var rs struct {
		Rules []rule `json:"rules"`
	}
	if err := json.Unmarshal(resp.Result, &rs); err != nil {
		return "", fmt.Errorf("decode created rule response: %w", err)
	}

	for _, created := range rs.Rules {
		if created.Description == r.Description {
			c.logger.Printf("[INFO] [cloudflare] Created rule %q → %s", r.Description, created.ID)
			if c.debug {
				expr := r.Expression
				if len(expr) > 120 {
					expr = expr[:120] + "..."
				}
				c.logger.Printf("[DEBUG] [cloudflare] CreateRule: expression: %s", expr)
			}
			return created.ID, nil
		}
	}

	return "", fmt.Errorf("created rule not found in response")
}

// UpdateRule updates an existing rule in the ruleset.
func (c *Client) UpdateRule(ctx context.Context, zoneID, rulesetID, ruleID string, r rule) error {
	c.logger.Printf("[INFO] [cloudflare] Updating WAF rule %s", ruleID)
	path := fmt.Sprintf("/zones/%s/rulesets/%s/rules/%s", zoneID, rulesetID, ruleID)
	_, err := c.do(ctx, "PATCH", path, r)
	if err != nil {
		c.logger.Printf("[ERROR] [cloudflare] Failed to update WAF rule %s: %v", ruleID, err)
		return fmt.Errorf("update rule %s: %w", ruleID, err)
	}
	c.logger.Printf("[INFO] [cloudflare] Updated rule %q (%s)", r.Description, ruleID)
	if c.debug {
		expr := r.Expression
		if len(expr) > 120 {
			expr = expr[:120] + "..."
		}
		c.logger.Printf("[DEBUG] [cloudflare] UpdateRule: expression: %s", expr)
	}
	return nil
}

// DeleteRule removes a rule from the ruleset.
func (c *Client) DeleteRule(ctx context.Context, zoneID, rulesetID, ruleID string) error {
	c.logger.Printf("[INFO] [cloudflare] Deleting WAF rule %s", ruleID)
	path := fmt.Sprintf("/zones/%s/rulesets/%s/rules/%s", zoneID, rulesetID, ruleID)
	_, err := c.do(ctx, "DELETE", path, nil)
	if err != nil {
		c.logger.Printf("[ERROR] [cloudflare] Failed to delete WAF rule %s: %v", ruleID, err)
		return fmt.Errorf("delete rule %s: %w", ruleID, err)
	}
	c.logger.Printf("[INFO] [cloudflare] Deleted rule %s", ruleID)
	return nil
}

// BuildGlobalExpression builds the Cloudflare filter expression for the global geo rule.
// countries: allowed ISO country codes.
// excludeHostnames: app hostnames that have their own rules (excluded from global).
//
// Example output: (not ip.src.country in {"HU" "DE"}) and (http.host ne "app1.example.com")
func BuildGlobalExpression(countries []string, excludeHostnames []string) string {
	if len(countries) == 0 {
		return "true" // block everything (no countries allowed)
	}

	// Build country part: (not ip.src.country in {"HU" "DE"})
	quoted := make([]string, len(countries))
	for i, c := range countries {
		quoted[i] = `"` + c + `"`
	}
	expr := "(not ip.src.country in {" + strings.Join(quoted, " ") + "})"

	// Add hostname exclusions for apps with their own rules
	for _, host := range excludeHostnames {
		expr += ` and (http.host ne "` + host + `")`
	}

	return expr
}

// BuildAppExpression builds the Cloudflare filter expression for a per-app geo rule.
// hostname: full hostname of the app (e.g., "nextcloud.demo-felhom.eu").
// countries: allowed ISO country codes for this app.
//
// Example output: (http.host eq "nextcloud.demo-felhom.eu" and not ip.src.country in {"HU" "US"})
func BuildAppExpression(hostname string, countries []string) string {
	if len(countries) == 0 {
		return fmt.Sprintf(`(http.host eq "%s")`, hostname) // block all traffic to this host
	}

	quoted := make([]string, len(countries))
	for i, c := range countries {
		quoted[i] = `"` + c + `"`
	}

	return fmt.Sprintf(`(http.host eq "%s" and not ip.src.country in {%s})`,
		hostname, strings.Join(quoted, " "))
}

// AppRuleDescription returns the rule description for a per-app rule.
func AppRuleDescription(appName string) string {
	return appRuleDescPrefix + appName
}

// IsGlobalRule checks if a rule description matches the global rule.
func IsGlobalRule(desc string) bool {
	return desc == globalRuleDesc
}

// IsAppRule checks if a rule description is a per-app rule and returns the app name.
func IsAppRule(desc string) (string, bool) {
	if strings.HasPrefix(desc, appRuleDescPrefix) {
		return strings.TrimPrefix(desc, appRuleDescPrefix), true
	}
	return "", false
}

// newBlockRule creates a rule struct with a block action.
// Custom response body requires a paid CF plan, so we use a plain block (403).
func newBlockRule(description, expression string) rule {
	enabled := true
	return rule{
		Description: description,
		Expression:  expression,
		Action:      "block",
		Enabled:     &enabled,
	}
}
