#!/usr/bin/env python3
"""
Cloudflare Zone Security Policy Comparison Tool
================================================
Fetches security policies from multiple Cloudflare zones and generates
a detailed HTML report illustrating differences and deltas.

Usage:
    python cf_zone_security_compare.py --token <API_TOKEN> --account <ACCOUNT_ID> [--zones zone1,zone2,...]
    python cf_zone_security_compare.py --token <API_TOKEN> --zones <ZONE_ID_1>,<ZONE_ID_2>,...
    python cf_zone_security_compare.py --config config.json

Environment variable:
    CLOUDFLARE_API_TOKEN=<token>  (alternative to --token)

Output:
    zone_security_comparison_<timestamp>.html
"""

import argparse
import json
import os
import sys
import time
import hashlib
from datetime import datetime, timezone
from collections import defaultdict
from typing import Any

try:
    import requests
except ImportError:
    print("Installing requests library...")
    import subprocess
    subprocess.check_call([sys.executable, "-m", "pip", "install", "requests", "-q"])
    import requests


# ─── Cloudflare API Client ───────────────────────────────────────────────────

BASE_URL = "https://api.cloudflare.com/client/v4"

SECURITY_PHASES = [
    "http_request_firewall_custom",
    "http_request_firewall_managed",
    "http_ratelimit",
    "ddos_l7",
    "http_request_sbfm",
]

PHASE_LABELS = {
    "http_request_firewall_custom": "WAF Custom Rules",
    "http_request_firewall_managed": "WAF Managed Rules",
    "http_ratelimit": "Rate Limiting Rules",
    "ddos_l7": "DDoS L7 Protection",
    "http_request_sbfm": "Super Bot Fight Mode",
}

SECURITY_SETTINGS_KEYS = [
    "security_level",
    "ssl",
    "always_use_https",
    "min_tls_version",
    "tls_1_3",
    "automatic_https_rewrites",
    "opportunistic_encryption",
    "browser_check",
    "challenge_ttl",
    "privacy_pass",
    "security_header",
    "waf",
    "bot_management",
]


class CloudflareClient:
    def __init__(self, token: str):
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        })
        self._rate_limit_remaining = 100

    def _get(self, path: str, params: dict = None) -> dict:
        """Make a GET request with rate limit awareness."""
        if self._rate_limit_remaining < 5:
            time.sleep(1)
        url = f"{BASE_URL}{path}"
        resp = self.session.get(url, params=params, timeout=30)
        self._rate_limit_remaining = int(resp.headers.get("x-ratelimit-remaining", 100))
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 5))
            print(f"  ⏳ Rate limited, waiting {retry_after}s...")
            time.sleep(retry_after)
            return self._get(path, params)
        data = resp.json()
        if not data.get("success", False):
            errors = data.get("errors", [])
            # Some phases may not exist — that's ok
            if any(e.get("code") == 10000 for e in errors):
                return {"result": None}
            if any("not found" in str(e.get("message", "")).lower() for e in errors):
                return {"result": None}
            print(f"  ⚠️  API error for {path}: {errors}")
            return {"result": None}
        return data

    def _get_paginated(self, path: str, params: dict = None) -> list:
        """Get all pages of a paginated endpoint."""
        params = params or {}
        params["per_page"] = 50
        params["page"] = 1
        all_results = []
        while True:
            data = self._get(path, params)
            results = data.get("result", [])
            if not results:
                break
            all_results.extend(results)
            info = data.get("result_info", {})
            total_pages = info.get("total_pages", 1)
            if params["page"] >= total_pages:
                break
            params["page"] += 1
        return all_results

    def verify_token(self) -> bool:
        data = self._get("/user/tokens/verify")
        return data.get("result", {}).get("status") == "active"

    def list_zones(self, account_id: str = None) -> list:
        params = {"per_page": 50}
        if account_id:
            params["account.id"] = account_id
        return self._get_paginated("/zones", params)

    def get_zone_details(self, zone_id: str) -> dict:
        return self._get(f"/zones/{zone_id}").get("result", {})

    def get_zone_rulesets(self, zone_id: str) -> list:
        data = self._get(f"/zones/{zone_id}/rulesets")
        return data.get("result", []) or []

    def get_ruleset_details(self, zone_id: str, ruleset_id: str) -> dict:
        data = self._get(f"/zones/{zone_id}/rulesets/{ruleset_id}")
        return data.get("result", {}) or {}

    def get_phase_entrypoint(self, zone_id: str, phase: str) -> dict:
        data = self._get(f"/zones/{zone_id}/rulesets/phases/{phase}/entrypoint")
        return data.get("result", {}) or {}

    def get_zone_settings(self, zone_id: str) -> list:
        data = self._get(f"/zones/{zone_id}/settings")
        return data.get("result", []) or []

    def get_ip_access_rules(self, zone_id: str) -> list:
        return self._get_paginated(f"/zones/{zone_id}/firewall/access_rules/rules")

    def get_page_rules(self, zone_id: str) -> list:
        data = self._get(f"/zones/{zone_id}/pagerules", {"per_page": 50})
        return data.get("result", []) or []


# ─── Data Collection ─────────────────────────────────────────────────────────

def collect_zone_security(client: CloudflareClient, zone_id: str, zone_name: str) -> dict:
    """Collect all security policy data for a single zone."""
    print(f"\n📥 Collecting security data for zone: {zone_name} ({zone_id})")
    zone_data = {
        "zone_id": zone_id,
        "zone_name": zone_name,
        "phases": {},
        "rulesets": [],
        "settings": {},
        "ip_access_rules": [],
        "page_rules": [],
    }

    # 1. Phase entrypoint rulesets (main security policies)
    for phase in SECURITY_PHASES:
        label = PHASE_LABELS.get(phase, phase)
        print(f"  → Fetching {label}...")
        entrypoint = client.get_phase_entrypoint(zone_id, phase)
        if entrypoint:
            rules = entrypoint.get("rules", [])
            zone_data["phases"][phase] = {
                "ruleset_id": entrypoint.get("id"),
                "name": entrypoint.get("name", ""),
                "version": entrypoint.get("version", ""),
                "rules": rules,
                "rule_count": len(rules),
            }
            print(f"    ✓ {len(rules)} rules found")
        else:
            zone_data["phases"][phase] = {"rules": [], "rule_count": 0}
            print(f"    · No rules configured")

    # 2. All rulesets (for custom rulesets)
    print(f"  → Fetching all rulesets...")
    rulesets = client.get_zone_rulesets(zone_id)
    custom_rulesets = [r for r in rulesets if r.get("kind") == "custom"]
    for rs in custom_rulesets:
        details = client.get_ruleset_details(zone_id, rs["id"])
        if details:
            zone_data["rulesets"].append(details)
    print(f"    ✓ {len(custom_rulesets)} custom rulesets")

    # 3. Zone security settings
    print(f"  → Fetching zone settings...")
    settings = client.get_zone_settings(zone_id)
    for setting in settings:
        sid = setting.get("id", "")
        if sid in SECURITY_SETTINGS_KEYS or "security" in sid or "ssl" in sid or "tls" in sid:
            zone_data["settings"][sid] = setting.get("value")
    print(f"    ✓ {len(zone_data['settings'])} security settings")

    # 4. IP Access Rules
    print(f"  → Fetching IP access rules...")
    zone_data["ip_access_rules"] = client.get_ip_access_rules(zone_id)
    print(f"    ✓ {len(zone_data['ip_access_rules'])} IP access rules")

    # 5. Page Rules (legacy but still in use)
    print(f"  → Fetching page rules...")
    zone_data["page_rules"] = client.get_page_rules(zone_id)
    print(f"    ✓ {len(zone_data['page_rules'])} page rules")

    return zone_data


# ─── Comparison Engine ────────────────────────────────────────────────────────

def normalize_rule(rule: dict) -> dict:
    """Normalize a rule for comparison by stripping volatile fields."""
    return {
        "action": rule.get("action", ""),
        "expression": rule.get("expression", ""),
        "description": rule.get("description", ""),
        "enabled": rule.get("enabled", True),
        "action_parameters": rule.get("action_parameters", {}),
    }


def rule_fingerprint(rule: dict) -> str:
    """Generate a stable fingerprint for a rule based on its meaningful content."""
    normalized = normalize_rule(rule)
    content = json.dumps(normalized, sort_keys=True)
    return hashlib.sha256(content.encode()).hexdigest()[:12]


def compare_rules(zones_data: list, phase: str) -> dict:
    """Compare rules across zones for a specific phase."""
    comparison = {
        "common": [],       # Rules present in ALL zones (identical)
        "unique": {},       # Rules unique to a specific zone
        "divergent": [],    # Rules with same expression but different config
        "summary": {},
    }

    zone_names = [z["zone_name"] for z in zones_data]
    for name in zone_names:
        comparison["unique"][name] = []

    # Build fingerprint → zone mapping
    fp_to_zones = defaultdict(list)
    fp_to_rule = {}
    expr_to_zones = defaultdict(list)

    for zd in zones_data:
        phase_data = zd["phases"].get(phase, {})
        rules = phase_data.get("rules", [])
        for rule in rules:
            fp = rule_fingerprint(rule)
            fp_to_zones[fp].append(zd["zone_name"])
            fp_to_rule[fp] = rule
            expr = rule.get("expression", "")
            if expr:
                expr_to_zones[expr].append({
                    "zone": zd["zone_name"],
                    "rule": rule,
                    "fingerprint": fp,
                })

    all_zone_names = set(zone_names)
    seen_fps = set()

    # Common rules (exact match across all zones)
    for fp, zones in fp_to_zones.items():
        if set(zones) == all_zone_names:
            comparison["common"].append(fp_to_rule[fp])
            seen_fps.add(fp)

    # Divergent rules (same expression, different config)
    for expr, entries in expr_to_zones.items():
        fps = set(e["fingerprint"] for e in entries)
        if len(fps) > 1:
            # Same expression exists with different configurations
            divergent_entry = {
                "expression": expr,
                "variants": []
            }
            for e in entries:
                if e["fingerprint"] not in seen_fps:
                    divergent_entry["variants"].append({
                        "zone": e["zone"],
                        "rule": e["rule"],
                    })
                    seen_fps.add(e["fingerprint"])
            if divergent_entry["variants"]:
                comparison["divergent"].append(divergent_entry)

    # Unique rules (only in one zone)
    for fp, zones in fp_to_zones.items():
        if fp not in seen_fps and len(zones) == 1:
            comparison["unique"][zones[0]].append(fp_to_rule[fp])
            seen_fps.add(fp)

    # Rules in some but not all zones (partial coverage)
    comparison["partial"] = []
    for fp, zones in fp_to_zones.items():
        if fp not in seen_fps:
            comparison["partial"].append({
                "rule": fp_to_rule[fp],
                "present_in": zones,
                "missing_from": list(all_zone_names - set(zones)),
            })

    # Summary
    comparison["summary"] = {
        "total_common": len(comparison["common"]),
        "total_unique": {z: len(r) for z, r in comparison["unique"].items()},
        "total_divergent": len(comparison["divergent"]),
        "total_partial": len(comparison["partial"]),
    }

    return comparison


def compare_settings(zones_data: list) -> dict:
    """Compare zone security settings across zones."""
    all_keys = set()
    for zd in zones_data:
        all_keys.update(zd["settings"].keys())

    comparison = {"identical": {}, "different": {}}

    for key in sorted(all_keys):
        values = {}
        for zd in zones_data:
            values[zd["zone_name"]] = zd["settings"].get(key, "NOT SET")

        unique_values = set(json.dumps(v, sort_keys=True) if isinstance(v, (dict, list)) else str(v) for v in values.values())
        if len(unique_values) == 1:
            comparison["identical"][key] = list(values.values())[0]
        else:
            comparison["different"][key] = values

    return comparison


def compare_ip_access_rules(zones_data: list) -> dict:
    """Compare IP access rules across zones."""
    comparison = {"common": [], "unique": {}, "partial": []}
    zone_names = [z["zone_name"] for z in zones_data]
    for name in zone_names:
        comparison["unique"][name] = []

    # Build IP/range → zone mapping
    target_to_zones = defaultdict(list)
    target_to_rule = {}

    for zd in zones_data:
        for rule in zd.get("ip_access_rules", []):
            config = rule.get("configuration", {})
            target = f"{config.get('target', '')}:{config.get('value', '')}"
            mode = rule.get("mode", "")
            key = f"{target}|{mode}"
            target_to_zones[key].append(zd["zone_name"])
            target_to_rule[key] = rule

    all_zone_names = set(zone_names)
    for key, zones in target_to_zones.items():
        rule = target_to_rule[key]
        if set(zones) == all_zone_names:
            comparison["common"].append(rule)
        elif len(zones) == 1:
            comparison["unique"][zones[0]].append(rule)
        else:
            comparison["partial"].append({
                "rule": rule,
                "present_in": zones,
                "missing_from": list(all_zone_names - set(zones)),
            })

    return comparison


# ─── HTML Report Generator ───────────────────────────────────────────────────

def generate_html_report(zones_data: list, comparisons: dict, settings_comp: dict,
                         ip_rules_comp: dict) -> str:
    """Generate a comprehensive HTML comparison report."""
    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
    zone_names = [z["zone_name"] for z in zones_data]
    num_zones = len(zone_names)

    def escape(s):
        return str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")

    def rule_card(rule, zone_label=None):
        action = rule.get("action", "N/A")
        expr = rule.get("expression", "N/A")
        desc = rule.get("description", "No description")
        enabled = rule.get("enabled", True)
        status_class = "enabled" if enabled else "disabled"
        status_text = "Enabled" if enabled else "Disabled"
        action_class = get_action_class(action)

        zone_badge = ""
        if zone_label:
            zone_badge = f'<span class="zone-badge">{escape(zone_label)}</span>'

        ap = rule.get("action_parameters", {})
        ap_html = ""
        if ap:
            # Show managed ruleset overrides or other action parameters
            if "id" in ap:
                ap_html = f'<div class="rule-param">Executes ruleset: <code>{escape(ap["id"][:16])}...</code></div>'
            overrides = ap.get("overrides", {})
            if overrides:
                override_count = len(overrides.get("rules", []))
                cat_count = len(overrides.get("categories", []))
                if override_count or cat_count:
                    ap_html += f'<div class="rule-param">Overrides: {override_count} rules, {cat_count} categories</div>'

        return f'''
        <div class="rule-card">
            <div class="rule-header">
                <span class="action-badge {action_class}">{escape(action)}</span>
                <span class="status-badge {status_class}">{status_text}</span>
                {zone_badge}
            </div>
            <div class="rule-desc">{escape(desc)}</div>
            <div class="rule-expr"><code>{escape(expr)}</code></div>
            {ap_html}
        </div>'''

    def get_action_class(action):
        action_map = {
            "block": "action-block",
            "challenge": "action-challenge",
            "managed_challenge": "action-challenge",
            "js_challenge": "action-challenge",
            "allow": "action-allow",
            "skip": "action-skip",
            "log": "action-log",
            "execute": "action-execute",
            "rewrite": "action-rewrite",
        }
        return action_map.get(action, "action-default")

    # Build phase comparison sections
    phase_sections = ""
    for phase in SECURITY_PHASES:
        label = PHASE_LABELS.get(phase, phase)
        comp = comparisons.get(phase, {})
        summary = comp.get("summary", {})

        # Rule counts per zone
        zone_counts = ""
        for zd in zones_data:
            pd = zd["phases"].get(phase, {})
            count = pd.get("rule_count", 0)
            zone_counts += f'<span class="zone-count"><strong>{escape(zd["zone_name"])}</strong>: {count} rules</span>'

        # Common rules
        common_rules = comp.get("common", [])
        common_html = ""
        if common_rules:
            common_html = '<div class="subsection"><h4>✅ Identical Across All Zones</h4><div class="rule-list">'
            for r in common_rules:
                common_html += rule_card(r)
            common_html += '</div></div>'
        else:
            common_html = '<div class="subsection"><h4>✅ Identical Across All Zones</h4><p class="muted">No identical rules found across all zones.</p></div>'

        # Divergent rules
        divergent = comp.get("divergent", [])
        divergent_html = ""
        if divergent:
            divergent_html = '<div class="subsection divergent-section"><h4>⚠️ Same Expression, Different Configuration</h4>'
            for entry in divergent:
                divergent_html += f'<div class="divergent-group"><div class="divergent-expr"><strong>Expression:</strong> <code>{escape(entry["expression"])}</code></div>'
                for variant in entry["variants"]:
                    divergent_html += rule_card(variant["rule"], zone_label=variant["zone"])
                divergent_html += '</div>'
            divergent_html += '</div>'

        # Unique rules
        unique = comp.get("unique", {})
        unique_html = ""
        has_unique = any(len(rules) > 0 for rules in unique.values())
        if has_unique:
            unique_html = '<div class="subsection unique-section"><h4>🔶 Zone-Specific Rules (Not in Other Zones)</h4>'
            for zone_name, rules in unique.items():
                if rules:
                    unique_html += f'<div class="zone-unique-group"><h5>{escape(zone_name)}</h5><div class="rule-list">'
                    for r in rules:
                        unique_html += rule_card(r)
                    unique_html += '</div></div>'
            unique_html += '</div>'

        # Partial coverage
        partial = comp.get("partial", [])
        partial_html = ""
        if partial:
            partial_html = '<div class="subsection partial-section"><h4>🔀 Partial Coverage (Some Zones Only)</h4>'
            for entry in partial:
                present = ", ".join(entry["present_in"])
                missing = ", ".join(entry["missing_from"])
                partial_html += f'<div class="partial-note">Present in: <strong>{escape(present)}</strong> | Missing from: <strong class="text-warning">{escape(missing)}</strong></div>'
                partial_html += rule_card(entry["rule"])
            partial_html += '</div>'

        # Delta summary bar
        n_common = summary.get("total_common", 0)
        n_unique_total = sum(summary.get("total_unique", {}).values())
        n_divergent = summary.get("total_divergent", 0)
        n_partial = summary.get("total_partial", 0)
        total = n_common + n_unique_total + n_divergent + n_partial or 1

        phase_sections += f'''
        <div class="phase-section">
            <h3>{escape(label)}</h3>
            <div class="phase-id"><code>{escape(phase)}</code></div>
            <div class="zone-counts">{zone_counts}</div>
            <div class="delta-bar">
                <div class="delta-segment common" style="width:{n_common/total*100:.1f}%"
                     title="Identical: {n_common}"></div>
                <div class="delta-segment divergent" style="width:{n_divergent/total*100:.1f}%"
                     title="Divergent: {n_divergent}"></div>
                <div class="delta-segment partial" style="width:{n_partial/total*100:.1f}%"
                     title="Partial: {n_partial}"></div>
                <div class="delta-segment unique" style="width:{n_unique_total/total*100:.1f}%"
                     title="Unique: {n_unique_total}"></div>
            </div>
            <div class="delta-legend">
                <span class="legend-item"><span class="dot common-dot"></span> Identical ({n_common})</span>
                <span class="legend-item"><span class="dot divergent-dot"></span> Divergent ({n_divergent})</span>
                <span class="legend-item"><span class="dot partial-dot"></span> Partial ({n_partial})</span>
                <span class="legend-item"><span class="dot unique-dot"></span> Unique ({n_unique_total})</span>
            </div>
            {common_html}
            {divergent_html}
            {unique_html}
            {partial_html}
        </div>'''

    # Settings comparison section
    settings_html = '<div class="phase-section"><h3>Zone Security Settings</h3>'

    # Different settings (THE IMPORTANT ONES)
    diff_settings = settings_comp.get("different", {})
    if diff_settings:
        settings_html += '<div class="subsection divergent-section"><h4>⚠️ Settings That Differ Between Zones</h4>'
        settings_html += '<table class="settings-table"><thead><tr><th>Setting</th>'
        for zn in zone_names:
            settings_html += f'<th>{escape(zn)}</th>'
        settings_html += '</tr></thead><tbody>'
        for key, values in sorted(diff_settings.items()):
            settings_html += f'<tr><td class="setting-name">{escape(key)}</td>'
            for zn in zone_names:
                val = values.get(zn, "N/A")
                if isinstance(val, (dict, list)):
                    val = json.dumps(val, indent=2)
                settings_html += f'<td class="setting-diff">{escape(str(val))}</td>'
            settings_html += '</tr>'
        settings_html += '</tbody></table></div>'
    else:
        settings_html += '<div class="subsection"><h4>✅ All Security Settings Are Identical</h4></div>'

    # Identical settings (collapsed)
    identical = settings_comp.get("identical", {})
    if identical:
        settings_html += '<div class="subsection"><h4>✅ Identical Settings</h4>'
        settings_html += '<details><summary>Show identical settings ({} items)</summary>'.format(len(identical))
        settings_html += '<table class="settings-table"><thead><tr><th>Setting</th><th>Value (all zones)</th></tr></thead><tbody>'
        for key, val in sorted(identical.items()):
            if isinstance(val, (dict, list)):
                val = json.dumps(val, indent=2)
            settings_html += f'<tr><td>{escape(key)}</td><td>{escape(str(val))}</td></tr>'
        settings_html += '</tbody></table></details></div>'

    settings_html += '</div>'

    # IP Access Rules section
    ip_html = '<div class="phase-section"><h3>IP Access Rules</h3>'
    ip_common = ip_rules_comp.get("common", [])
    ip_unique = ip_rules_comp.get("unique", {})
    ip_partial = ip_rules_comp.get("partial", [])

    total_ip = len(ip_common) + sum(len(v) for v in ip_unique.values()) + len(ip_partial) or 1

    ip_html += f'''
    <div class="delta-bar">
        <div class="delta-segment common" style="width:{len(ip_common)/total_ip*100:.1f}%"></div>
        <div class="delta-segment unique" style="width:{sum(len(v) for v in ip_unique.values())/total_ip*100:.1f}%"></div>
        <div class="delta-segment partial" style="width:{len(ip_partial)/total_ip*100:.1f}%"></div>
    </div>
    <div class="delta-legend">
        <span class="legend-item"><span class="dot common-dot"></span> Common ({len(ip_common)})</span>
        <span class="legend-item"><span class="dot unique-dot"></span> Unique ({sum(len(v) for v in ip_unique.values())})</span>
        <span class="legend-item"><span class="dot partial-dot"></span> Partial ({len(ip_partial)})</span>
    </div>'''

    if any(len(v) > 0 for v in ip_unique.values()):
        ip_html += '<div class="subsection unique-section"><h4>🔶 Zone-Specific IP Rules</h4>'
        for zn, rules in ip_unique.items():
            if rules:
                ip_html += f'<div class="zone-unique-group"><h5>{escape(zn)}</h5>'
                for r in rules:
                    cfg = r.get("configuration", {})
                    ip_html += f'<div class="ip-rule"><span class="action-badge action-{r.get("mode", "block")}">{escape(r.get("mode", ""))}</span> {escape(cfg.get("target", ""))}: <code>{escape(cfg.get("value", ""))}</code> <span class="muted">{escape(r.get("notes", ""))}</span></div>'
                ip_html += '</div>'
        ip_html += '</div>'

    if ip_partial:
        ip_html += '<div class="subsection partial-section"><h4>🔀 Partial IP Rules</h4>'
        for entry in ip_partial:
            r = entry["rule"]
            cfg = r.get("configuration", {})
            present = ", ".join(entry["present_in"])
            missing = ", ".join(entry["missing_from"])
            ip_html += f'<div class="ip-rule"><span class="action-badge action-{r.get("mode", "block")}">{escape(r.get("mode", ""))}</span> {escape(cfg.get("target", ""))}: <code>{escape(cfg.get("value", ""))}</code><div class="partial-note">Present in: <strong>{escape(present)}</strong> | Missing from: <strong class="text-warning">{escape(missing)}</strong></div></div>'
        ip_html += '</div>'

    ip_html += '</div>'

    # Executive summary
    total_deltas = 0
    total_common_rules = 0
    for phase in SECURITY_PHASES:
        comp = comparisons.get(phase, {})
        summary = comp.get("summary", {})
        total_common_rules += summary.get("total_common", 0)
        total_deltas += sum(summary.get("total_unique", {}).values())
        total_deltas += summary.get("total_divergent", 0)
        total_deltas += summary.get("total_partial", 0)
    total_deltas += len(diff_settings)

    # Consistency score
    total_items = total_common_rules + total_deltas
    consistency = (total_common_rules / total_items * 100) if total_items > 0 else 100

    exec_summary = f'''
    <div class="exec-summary">
        <div class="summary-grid">
            <div class="summary-card">
                <div class="summary-number">{num_zones}</div>
                <div class="summary-label">Zones Compared</div>
            </div>
            <div class="summary-card">
                <div class="summary-number">{total_common_rules}</div>
                <div class="summary-label">Common Rules</div>
            </div>
            <div class="summary-card {"warning" if total_deltas > 0 else ""}">
                <div class="summary-number">{total_deltas}</div>
                <div class="summary-label">Total Deltas</div>
            </div>
            <div class="summary-card {"warning" if consistency < 80 else "good" if consistency > 95 else ""}">
                <div class="summary-number">{consistency:.0f}%</div>
                <div class="summary-label">Consistency Score</div>
            </div>
            <div class="summary-card">
                <div class="summary-number">{len(diff_settings)}</div>
                <div class="summary-label">Setting Differences</div>
            </div>
        </div>
    </div>'''

    # Zone overview cards
    zone_overview = '<div class="zone-overview"><h3>Zone Overview</h3><div class="zone-cards">'
    for zd in zones_data:
        total_rules = sum(zd["phases"].get(p, {}).get("rule_count", 0) for p in SECURITY_PHASES)
        zone_overview += f'''
        <div class="zone-card">
            <h4>{escape(zd["zone_name"])}</h4>
            <div class="zone-stat">Total security rules: <strong>{total_rules}</strong></div>
            <div class="zone-stat">IP access rules: <strong>{len(zd.get("ip_access_rules", []))}</strong></div>
            <div class="zone-stat">Page rules: <strong>{len(zd.get("page_rules", []))}</strong></div>
            <div class="zone-stat">Settings tracked: <strong>{len(zd.get("settings", {}))}</strong></div>
            <div class="zone-phases">'''
        for phase in SECURITY_PHASES:
            pd = zd["phases"].get(phase, {})
            count = pd.get("rule_count", 0)
            label = PHASE_LABELS.get(phase, phase)
            zone_overview += f'<div class="phase-count"><span class="phase-label">{escape(label)}</span> <span class="phase-num">{count}</span></div>'
        zone_overview += '</div></div>'
    zone_overview += '</div></div>'

    html = f'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloudflare Zone Security Policy Comparison</title>
<style>
:root {{
    --cf-orange: #f6821f;
    --cf-dark: #1a1a2e;
    --cf-gray: #404040;
    --cf-light: #f5f5f5;
    --cf-green: #2ecc71;
    --cf-red: #e74c3c;
    --cf-yellow: #f39c12;
    --cf-blue: #3498db;
    --cf-purple: #9b59b6;
    --common-color: #2ecc71;
    --divergent-color: #e74c3c;
    --partial-color: #f39c12;
    --unique-color: #3498db;
}}

* {{ margin: 0; padding: 0; box-sizing: border-box; }}

body {{
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
    background: #f8f9fa;
    color: #333;
    line-height: 1.6;
}}

.header {{
    background: linear-gradient(135deg, var(--cf-dark) 0%, #16213e 100%);
    color: white;
    padding: 2rem 3rem;
    border-bottom: 4px solid var(--cf-orange);
}}

.header h1 {{
    font-size: 1.8rem;
    font-weight: 700;
    margin-bottom: 0.5rem;
}}

.header .subtitle {{
    opacity: 0.8;
    font-size: 0.95rem;
}}

.header .meta {{
    margin-top: 1rem;
    font-size: 0.85rem;
    opacity: 0.7;
}}

.container {{
    max-width: 1400px;
    margin: 0 auto;
    padding: 2rem;
}}

.exec-summary {{
    margin-bottom: 2rem;
}}

.summary-grid {{
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 1rem;
}}

.summary-card {{
    background: white;
    border-radius: 12px;
    padding: 1.5rem;
    text-align: center;
    box-shadow: 0 2px 8px rgba(0,0,0,0.08);
    border-left: 4px solid var(--cf-blue);
}}

.summary-card.warning {{
    border-left-color: var(--cf-red);
}}

.summary-card.good {{
    border-left-color: var(--cf-green);
}}

.summary-number {{
    font-size: 2.2rem;
    font-weight: 800;
    color: var(--cf-dark);
}}

.summary-label {{
    font-size: 0.85rem;
    color: #666;
    margin-top: 0.25rem;
}}

.zone-overview {{
    margin-bottom: 2rem;
}}

.zone-overview h3 {{
    font-size: 1.3rem;
    margin-bottom: 1rem;
    color: var(--cf-dark);
}}

.zone-cards {{
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1rem;
}}

.zone-card {{
    background: white;
    border-radius: 10px;
    padding: 1.25rem;
    box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}}

.zone-card h4 {{
    color: var(--cf-dark);
    margin-bottom: 0.75rem;
    font-size: 1.05rem;
    padding-bottom: 0.5rem;
    border-bottom: 2px solid var(--cf-orange);
}}

.zone-stat {{
    font-size: 0.9rem;
    margin-bottom: 0.25rem;
    color: #555;
}}

.zone-phases {{
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid #eee;
}}

.phase-count {{
    display: flex;
    justify-content: space-between;
    font-size: 0.85rem;
    padding: 0.2rem 0;
}}

.phase-label {{ color: #666; }}
.phase-num {{ font-weight: 600; color: var(--cf-dark); }}

.phase-section {{
    background: white;
    border-radius: 12px;
    padding: 1.5rem 2rem;
    margin-bottom: 1.5rem;
    box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}}

.phase-section h3 {{
    font-size: 1.25rem;
    color: var(--cf-dark);
    margin-bottom: 0.5rem;
}}

.phase-id {{
    font-size: 0.8rem;
    margin-bottom: 1rem;
}}

.phase-id code {{
    background: #eee;
    padding: 0.15rem 0.5rem;
    border-radius: 4px;
    font-size: 0.8rem;
}}

.zone-counts {{
    display: flex;
    gap: 1.5rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}}

.zone-count {{
    font-size: 0.9rem;
    color: #555;
    background: #f8f9fa;
    padding: 0.3rem 0.75rem;
    border-radius: 6px;
}}

.delta-bar {{
    display: flex;
    height: 8px;
    border-radius: 4px;
    overflow: hidden;
    background: #eee;
    margin-bottom: 0.5rem;
}}

.delta-segment {{ min-width: 2px; transition: width 0.3s; }}
.delta-segment.common {{ background: var(--common-color); }}
.delta-segment.divergent {{ background: var(--divergent-color); }}
.delta-segment.partial {{ background: var(--partial-color); }}
.delta-segment.unique {{ background: var(--unique-color); }}

.delta-legend {{
    display: flex;
    gap: 1.5rem;
    flex-wrap: wrap;
    margin-bottom: 1.5rem;
    font-size: 0.85rem;
    color: #666;
}}

.legend-item {{ display: flex; align-items: center; gap: 0.3rem; }}
.dot {{ width: 10px; height: 10px; border-radius: 50%; display: inline-block; }}
.common-dot {{ background: var(--common-color); }}
.divergent-dot {{ background: var(--divergent-color); }}
.partial-dot {{ background: var(--partial-color); }}
.unique-dot {{ background: var(--unique-color); }}

.subsection {{
    margin-top: 1.25rem;
    padding-top: 1rem;
    border-top: 1px solid #eee;
}}

.subsection h4 {{
    font-size: 1rem;
    margin-bottom: 0.75rem;
    color: var(--cf-gray);
}}

.divergent-section {{ }}
.unique-section {{ }}
.partial-section {{ }}

.rule-list {{
    display: grid;
    gap: 0.75rem;
}}

.rule-card {{
    background: #f8f9fa;
    border: 1px solid #e9ecef;
    border-radius: 8px;
    padding: 0.75rem 1rem;
}}

.rule-header {{
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin-bottom: 0.4rem;
    flex-wrap: wrap;
}}

.action-badge {{
    display: inline-block;
    padding: 0.15rem 0.6rem;
    border-radius: 4px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}}

.action-block {{ background: #fde8e8; color: #c0392b; }}
.action-challenge {{ background: #fef3e2; color: #d68910; }}
.action-allow {{ background: #e8f8f0; color: #27ae60; }}
.action-skip {{ background: #eee; color: #666; }}
.action-log {{ background: #e8eaf6; color: #5c6bc0; }}
.action-execute {{ background: #e3f2fd; color: #1976d2; }}
.action-rewrite {{ background: #f3e5f5; color: #8e24aa; }}
.action-default {{ background: #f5f5f5; color: #666; }}
.action-whitelist {{ background: #e8f8f0; color: #27ae60; }}
.action-block {{ background: #fde8e8; color: #c0392b; }}

.status-badge {{
    font-size: 0.7rem;
    padding: 0.1rem 0.5rem;
    border-radius: 3px;
}}

.status-badge.enabled {{ background: #e8f8f0; color: #27ae60; }}
.status-badge.disabled {{ background: #fde8e8; color: #c0392b; }}

.zone-badge {{
    font-size: 0.7rem;
    padding: 0.1rem 0.5rem;
    border-radius: 3px;
    background: #e3f2fd;
    color: #1565c0;
    font-weight: 600;
}}

.rule-desc {{
    font-size: 0.85rem;
    color: #555;
    margin-bottom: 0.3rem;
}}

.rule-expr code {{
    font-size: 0.8rem;
    background: #fff;
    padding: 0.3rem 0.5rem;
    border-radius: 4px;
    border: 1px solid #e0e0e0;
    display: block;
    word-break: break-all;
    white-space: pre-wrap;
    max-height: 100px;
    overflow-y: auto;
}}

.rule-param {{
    font-size: 0.8rem;
    color: #777;
    margin-top: 0.3rem;
}}

.divergent-group {{
    background: #fff5f5;
    border: 1px solid #fed7d7;
    border-radius: 8px;
    padding: 1rem;
    margin-bottom: 0.75rem;
}}

.divergent-expr {{
    margin-bottom: 0.75rem;
    font-size: 0.9rem;
}}

.divergent-expr code {{
    background: #fff;
    padding: 0.2rem 0.5rem;
    border-radius: 4px;
    font-size: 0.8rem;
    word-break: break-all;
}}

.zone-unique-group {{
    margin-bottom: 1rem;
}}

.zone-unique-group h5 {{
    font-size: 0.95rem;
    color: var(--cf-blue);
    margin-bottom: 0.5rem;
    padding-left: 0.5rem;
    border-left: 3px solid var(--cf-blue);
}}

.partial-note {{
    font-size: 0.85rem;
    margin-bottom: 0.5rem;
    padding: 0.3rem 0.5rem;
    background: #fffbf0;
    border-radius: 4px;
}}

.text-warning {{ color: var(--cf-red); }}

.settings-table {{
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
    margin-top: 0.5rem;
}}

.settings-table th {{
    background: #f8f9fa;
    padding: 0.6rem 0.75rem;
    text-align: left;
    border-bottom: 2px solid #dee2e6;
    font-size: 0.85rem;
    color: #555;
}}

.settings-table td {{
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid #eee;
}}

.setting-name {{
    font-weight: 600;
    color: var(--cf-dark);
    white-space: nowrap;
}}

.setting-diff {{
    background: #fff8e1;
}}

details {{
    margin-top: 0.5rem;
}}

details summary {{
    cursor: pointer;
    color: #666;
    font-size: 0.85rem;
}}

.ip-rule {{
    padding: 0.5rem 0.75rem;
    background: #f8f9fa;
    border-radius: 6px;
    margin-bottom: 0.5rem;
    font-size: 0.9rem;
}}

.muted {{ color: #999; font-style: italic; }}

.footer {{
    text-align: center;
    padding: 2rem;
    color: #999;
    font-size: 0.8rem;
}}

@media print {{
    .header {{ background: var(--cf-dark) !important; -webkit-print-color-adjust: exact; }}
    .container {{ max-width: 100%; padding: 1rem; }}
    .phase-section {{ page-break-inside: avoid; }}
    details {{ open; }}
}}
</style>
</head>
<body>

<div class="header">
    <h1>🛡️ Cloudflare Zone Security Policy Comparison</h1>
    <div class="subtitle">Comprehensive security policy diff across {num_zones} zones: {", ".join(escape(z) for z in zone_names)}</div>
    <div class="meta">Generated: {timestamp} | Phases analyzed: {len(SECURITY_PHASES)} | Tool: cf_zone_security_compare.py</div>
</div>

<div class="container">
    {exec_summary}
    {zone_overview}
    {phase_sections}
    {settings_html}
    {ip_html}
</div>

<div class="footer">
    Generated by Cloudflare Zone Security Policy Comparison Tool &mdash; {timestamp}
</div>

</body>
</html>'''

    return html


# ─── Main ─────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(
        description="Compare Cloudflare zone security policies and generate an HTML diff report.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Compare all zones in an account
  python cf_zone_security_compare.py --token $CF_TOKEN --account acc123

  # Compare specific zones
  python cf_zone_security_compare.py --token $CF_TOKEN --zones zone1_id,zone2_id,zone3_id

  # Use config file
  python cf_zone_security_compare.py --config config.json

  # Use environment variable for token
  export CLOUDFLARE_API_TOKEN=your_token
  python cf_zone_security_compare.py --account acc123

Config file format (config.json):
  {
    "api_token": "your_token",
    "account_id": "optional_account_id",
    "zone_ids": ["zone1", "zone2"],
    "output": "report.html"
  }
        """,
    )
    parser.add_argument("--token", "-t", help="Cloudflare API token (or set CLOUDFLARE_API_TOKEN env var)")
    parser.add_argument("--account", "-a", help="Account ID — fetch all zones in this account")
    parser.add_argument("--zones", "-z", help="Comma-separated zone IDs to compare")
    parser.add_argument("--config", "-c", help="Path to JSON config file")
    parser.add_argument("--output", "-o", help="Output HTML file path")
    parser.add_argument("--json-dump", action="store_true", help="Also dump raw JSON data")

    args = parser.parse_args()

    # Load config file if provided
    config = {}
    if args.config:
        with open(args.config) as f:
            config = json.load(f)

    # Resolve token
    token = args.token or config.get("api_token") or os.environ.get("CLOUDFLARE_API_TOKEN")
    if not token:
        print("❌ No API token provided. Use --token, config file, or CLOUDFLARE_API_TOKEN env var.")
        sys.exit(1)

    # Resolve zones
    zone_ids = []
    if args.zones:
        zone_ids = [z.strip() for z in args.zones.split(",")]
    elif config.get("zone_ids"):
        zone_ids = config["zone_ids"]

    account_id = args.account or config.get("account_id")

    if not zone_ids and not account_id:
        print("❌ Provide either --zones or --account to specify which zones to compare.")
        sys.exit(1)

    # Initialize client
    client = CloudflareClient(token)

    print("🔑 Verifying API token...")
    if not client.verify_token():
        print("❌ API token is invalid or expired.")
        sys.exit(1)
    print("✅ Token verified")

    # Discover zones if needed
    if not zone_ids:
        print(f"\n📋 Fetching zones for account {account_id}...")
        zones = client.list_zones(account_id)
        if not zones:
            print("❌ No zones found for this account.")
            sys.exit(1)
        zone_ids = [z["id"] for z in zones]
        print(f"✅ Found {len(zones)} zones:")
        for z in zones:
            print(f"   • {z['name']} ({z['id']})")
    else:
        zones = []
        for zid in zone_ids:
            details = client.get_zone_details(zid)
            if details:
                zones = zones  # We'll get names during collection

    if len(zone_ids) < 2:
        print("⚠️  Need at least 2 zones to compare. Found only", len(zone_ids))
        sys.exit(1)

    # Collect security data for each zone
    print(f"\n{'='*60}")
    print(f"  Collecting security policies for {len(zone_ids)} zones")
    print(f"{'='*60}")

    zones_data = []
    for zid in zone_ids:
        # Get zone name
        details = client.get_zone_details(zid)
        zone_name = details.get("name", zid) if details else zid
        zone_data = collect_zone_security(client, zid, zone_name)
        zones_data.append(zone_data)

    # Run comparisons
    print(f"\n{'='*60}")
    print(f"  Analyzing differences across zones")
    print(f"{'='*60}")

    comparisons = {}
    for phase in SECURITY_PHASES:
        label = PHASE_LABELS.get(phase, phase)
        print(f"  🔍 Comparing {label}...")
        comparisons[phase] = compare_rules(zones_data, phase)

    print(f"  🔍 Comparing zone settings...")
    settings_comp = compare_settings(zones_data)

    print(f"  🔍 Comparing IP access rules...")
    ip_rules_comp = compare_ip_access_rules(zones_data)

    # Generate report
    print(f"\n📊 Generating HTML report...")
    html = generate_html_report(zones_data, comparisons, settings_comp, ip_rules_comp)

    # Write output
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    output_path = args.output or config.get("output") or f"zone_security_comparison_{timestamp}.html"
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(html)
    print(f"✅ Report saved to: {output_path}")

    # Optional JSON dump
    if args.json_dump:
        json_path = output_path.replace(".html", ".json")
        dump = {
            "generated": timestamp,
            "zones": zones_data,
            "comparisons": {},
            "settings_comparison": settings_comp,
            "ip_rules_comparison": {
                "common_count": len(ip_rules_comp["common"]),
                "unique_counts": {z: len(r) for z, r in ip_rules_comp["unique"].items()},
                "partial_count": len(ip_rules_comp["partial"]),
            },
        }
        for phase, comp in comparisons.items():
            dump["comparisons"][phase] = comp["summary"]
        with open(json_path, "w") as f:
            json.dump(dump, f, indent=2, default=str)
        print(f"✅ JSON data saved to: {json_path}")

    # Print summary
    total_deltas = 0
    total_common = 0
    for phase, comp in comparisons.items():
        s = comp["summary"]
        total_common += s["total_common"]
        total_deltas += sum(s["total_unique"].values()) + s["total_divergent"] + s["total_partial"]

    total_deltas += len(settings_comp.get("different", {}))

    print(f"\n{'='*60}")
    print(f"  SUMMARY")
    print(f"{'='*60}")
    print(f"  Zones compared:     {len(zones_data)}")
    print(f"  Common rules:       {total_common}")
    print(f"  Total deltas:       {total_deltas}")
    print(f"  Setting diffs:      {len(settings_comp.get('different', {}))}")
    total = total_common + total_deltas
    if total > 0:
        print(f"  Consistency score:  {total_common/total*100:.0f}%")
    print(f"{'='*60}")
    print(f"\n  Open {output_path} in a browser to view the full report.")


if __name__ == "__main__":
    main()
