"""HTML sanitization utilities.

This module provides PII removal from HTML content with ZERO external
dependencies (stdlib only). Designed for sanitizing device web interface
HTML before inclusion in diagnostics or fixture files.

PII Categories Removed:
    - MAC addresses (all formats)
    - Serial numbers
    - Account/Subscriber IDs
    - Private/Public IP addresses (except common gateway IPs)
    - IPv6 addresses
    - Passwords and passphrases
    - Session tokens and cookies
    - CSRF tokens
    - Email addresses
    - Config file paths (may contain ISP/customer info)
    - WiFi credentials in JavaScript variables
"""

from __future__ import annotations

import ipaddress
import re
from typing import TYPE_CHECKING

from har_capture.patterns import (
    Hasher,
    is_allowlisted,
    load_allowlist,
    load_pii_patterns,
    load_sensitive_patterns,
)

if TYPE_CHECKING:
    from typing import Any

    from har_capture.sanitization.collector import RedactionCollector
    from har_capture.sanitization.report import HeuristicMode
else:
    from har_capture.sanitization.report import HeuristicMode


def sanitize_html(
    html: str,
    *,
    salt: str | None = "auto",
    custom_patterns: str | dict[str, Any] | None = None,
    collector: RedactionCollector | None = None,
    heuristics: HeuristicMode = HeuristicMode.DISABLED,
) -> str:
    """Remove sensitive information from HTML.

    This function sanitizes device HTML to remove PII before inclusion in
    diagnostics or fixture files. It's designed to be thorough while
    preserving data structure for debugging.

    By default, only **known patterns** (MACs, IPs, emails, serial numbers)
    are redacted. Enable heuristics to analyze pipe-delimited values that
    lack field labels (e.g., tagValueList in router HTML).

    Args:
        html: Raw HTML from device
        salt: Salt for hashed redaction. Options:
            - "auto" (default): Random salt, correlates within this call
            - None: Static placeholders (legacy behavior)
            - Any string: Consistent hashing across calls with same salt
            (ignored if collector is provided)
        custom_patterns: Custom PII patterns to apply. Options:
            - String: Path to JSON patterns file
            - Dict: Pattern definitions directly (e.g., from modem.yaml)
            - None: Use built-in patterns only
        collector: Optional collector for tracking redactions
        heuristics: How to handle unlabeled pipe-delimited values:
            - DISABLED (default): Skip heuristics, only redact known patterns
            - FLAG: Flag suspicious values for manual review (interactive)
            - REDACT: Auto-redact suspicious values (may over-redact)

    Returns:
        Sanitized HTML with personal info removed

    Example:
        >>> from har_capture.sanitization.report import HeuristicMode
        >>> sanitize_html("MAC: AA:BB:CC:DD:EE:FF")  # doctest: +SKIP
        'MAC: MAC_a1b2c3d4'
        >>> sanitize_html("MAC: AA:BB:CC:DD:EE:FF", salt=None)
        'MAC: ***MAC***'
        >>> # Enable heuristics for pipe-delimited values
        >>> sanitize_html(html, heuristics=HeuristicMode.REDACT)  # doctest: +SKIP
    """
    # Use collector's hasher if provided, otherwise create one
    if collector is not None:
        hasher = collector.hasher
    else:
        hasher = Hasher.create(salt)
        # Import here to avoid circular import
        from har_capture.sanitization.collector import RedactionCollector

        collector = RedactionCollector(hasher=hasher)

    pii = load_pii_patterns(custom_patterns)
    sensitive = load_sensitive_patterns(custom_patterns)

    # 0. Custom patterns (apply first so they take precedence over built-in patterns)
    # Skip built-in patterns that have dedicated replacement logic below
    BUILTIN_PATTERNS = {
        "mac_address",
        "serial_number",
        "account_id",
        "private_ip",
        "public_ip",
        "ipv6",
        "email",
        "password_field",
        "password_input",
        "session_token",
        "csrf_token",
        "config_path",
        "motorola_password",
    }

    for pattern_name, pattern_def in pii.get("patterns", {}).items():
        # Skip built-in patterns with special handling
        if pattern_name in BUILTIN_PATTERNS:
            continue

        if not isinstance(pattern_def, dict) or "regex" not in pattern_def:
            continue

        regex = pattern_def["regex"]
        prefix = pattern_def.get("replacement_prefix", "CUSTOM")

        # Handle regex flags
        flags = 0
        if "flags" in pattern_def:
            for flag_name in pattern_def["flags"]:
                if flag_name == "IGNORECASE":
                    flags |= re.IGNORECASE

        def make_replacer(prefix: str, pname: str) -> Any:
            def replace_custom(match: re.Match[str]) -> str:
                collector.record_auto_redaction(pname)
                return hasher.hash_generic(match.group(0), prefix)

            return replace_custom

        html = re.sub(regex, make_replacer(prefix, pattern_name), html, flags=flags)

    # 1. MAC Addresses (various formats: XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX)
    def replace_mac(match: re.Match[str]) -> str:
        collector.record_auto_redaction("mac_address")
        return hasher.hash_mac(match.group(0))

    html = re.sub(r"\b([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b", replace_mac, html)

    # 2. Serial Numbers (various label formats)
    def replace_serial(match: re.Match[str]) -> str:
        collector.record_auto_redaction("serial_number")
        label = match.group(1)
        serial = match.group(2) if match.lastindex and match.lastindex >= 2 else ""
        hashed = hasher.hash_generic(serial, "SERIAL") if serial else "***SERIAL***"
        return f"{label}: {hashed}"

    html = re.sub(
        r"\b(Serial\s*Number|SerialNum|SN|S/N)\s*[:\s=]*(?:<[^>]*>)*\s*([a-zA-Z0-9\-]{5,})",
        replace_serial,
        html,
        flags=re.IGNORECASE,
    )

    # 3. Account/Subscriber IDs
    def replace_account(match: re.Match[str]) -> str:
        prefix = match.group(1)
        suffix = match.group(2)
        value = match.group(3)

        # Skip if value is already redacted (contains "_" indicating a hash prefix like TEST_xxxxx)
        if "_" in value and re.match(r"^[A-Z]+_[a-f0-9]+$", value):
            return match.group(0)  # Return unchanged

        collector.record_auto_redaction("account")
        return f"{prefix} {suffix}: {hasher.hash_generic(match.group(0), 'ACCOUNT')}"

    html = re.sub(
        r"(Account|Subscriber|Customer|Device)\s*(ID|Number)\s*[:\s=]+(\S+)",
        replace_account,
        html,
        flags=re.IGNORECASE,
    )

    # 4. Private IP addresses (keep common gateway IPs for context)
    preserved_ips = set(pii.get("preserved_gateway_ips", []))

    def replace_private_ip(match: re.Match[str]) -> str:
        ip = match.group(0)
        if ip in preserved_ips:
            return ip
        collector.record_auto_redaction("private_ip")
        return hasher.hash_ip(ip, is_private=True)

    html = re.sub(
        r"\b(?:10\.|172\.(?:1[6-9]|2[0-9]|3[01])\.|192\.168\.)\d{1,3}\.\d{1,3}\b",
        replace_private_ip,
        html,
    )

    # 5. Public IP addresses (any non-private, non-localhost IP)
    def replace_public_ip(match: re.Match[str]) -> str:
        collector.record_auto_redaction("public_ip")
        return hasher.hash_ip(match.group(0), is_private=False)

    html = re.sub(
        r"\b(?!10\.)(?!172\.(?:1[6-9]|2[0-9]|3[01])\.)(?!192\.168\.)"
        r"(?!127\.)(?!0\.)(?!255\.)"
        r"(?:[1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
        r"(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
        r"(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\."
        r"(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\b",
        replace_public_ip,
        html,
    )

    # 6. IPv6 Addresses (full and compressed) - strict validation
    def replace_ipv6(match: re.Match[str]) -> str:
        text: str = match.group(0)
        # Skip if it looks like a MAC address (6 groups of 2 hex chars)
        if re.match(r"^[0-9a-f]{2}(:[0-9a-f]{2}){5}$", text, re.IGNORECASE):
            return text
        # Use strict validation via ipaddress module
        try:
            ipaddress.IPv6Address(text)
            collector.record_auto_redaction("ipv6")
            return hasher.hash_ipv6(text)
        except ipaddress.AddressValueError:
            # Not a valid IPv6 address (e.g., time format "12:34:56")
            return text

    # Match potential IPv6 addresses including compressed forms like ::1
    # Use (?<![:\w]) instead of \b to handle addresses starting with ::
    html = re.sub(
        r"(?<![:\w])([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}(?![:\w])",
        replace_ipv6,
        html,
        flags=re.IGNORECASE,
    )

    # 7. Passwords/Passphrases in HTML forms or text
    def replace_password(match: re.Match[str]) -> str:
        collector.record_auto_redaction("password")
        label = match.group(1)
        return f"{label}={hasher.hash_generic(match.group(2), 'PASS')}"

    html = re.sub(
        r'(password|passphrase|psk|key|wpa[0-9]*key)\s*[=:]\s*["\'\\]?([^"\'<>\s]+)',
        replace_password,
        html,
        flags=re.IGNORECASE,
    )

    # 7a. SSID labels in HTML text (<p>SSID: MyNetwork</p>)
    def replace_ssid_text(match: re.Match[str]) -> str:
        collector.record_auto_redaction("wifi")
        label = match.group(1)
        sep = match.group(2)
        return f"{label}{sep}{hasher.hash_generic(match.group(3), 'WIFI')}"

    html = re.sub(
        r'\b(SSID|Network\s*Name)\s*([=:])\s*([^\s<>"\']+)',
        replace_ssid_text,
        html,
        flags=re.IGNORECASE,
    )

    # 7b. JavaScript object password fields (password_24g: 'value', guest_password: 'value')  # pragma: allowlist secret
    def replace_js_password(match: re.Match[str]) -> str:
        collector.record_auto_redaction("password")
        label = match.group(1)
        sep = match.group(2)
        quote1 = match.group(3)
        quote2 = match.group(5)
        return f"{label}{sep}{quote1}{hasher.hash_generic(match.group(4), 'PASS')}{quote2}"

    html = re.sub(
        r'(\w*password\w*)\s*([=:])\s*(["\'])([^"\']+)(["\'])',
        replace_js_password,
        html,
        flags=re.IGNORECASE,
    )

    # 8. Password input fields
    def replace_password_input(match: re.Match[str]) -> str:
        collector.record_auto_redaction("password")
        prefix = match.group(1)
        suffix = match.group(3)
        return f"{prefix}{hasher.hash_generic(match.group(2), 'PASS')}{suffix}"

    html = re.sub(
        r'(<input[^>]*type=["\'\\]?password["\'\\]?[^>]*value=["\'\\]?)([^"\'\\]+)(["\'\\]?)',
        replace_password_input,
        html,
        flags=re.IGNORECASE,
    )

    # 8b. SSID input fields (input following SSID label)
    # Matches: <label>...SSID...</label><input value="...">
    def replace_ssid_input(match: re.Match[str]) -> str:
        collector.record_auto_redaction("wifi")
        prefix = match.group(1)
        value_start = match.group(2)
        value_end = match.group(4)
        return f"{prefix}{value_start}{hasher.hash_generic(match.group(3), 'WIFI')}{value_end}"

    html = re.sub(
        r'(<label>[^<]*SSID[^<]*</label>\s*<input[^>]*)(value=["\'\\]?)([^"\'\\>]+)(["\'\\]?)',
        replace_ssid_input,
        html,
        flags=re.IGNORECASE,
    )

    # 9. Session tokens/cookies (long alphanumeric strings)
    def replace_token(match: re.Match[str]) -> str:
        collector.record_auto_redaction("token")
        label = match.group(1)
        return f"{label}={hasher.hash_generic(match.group(2), 'TOKEN')}"

    html = re.sub(
        r'(session|token|auth|cookie)\s*[=:]\s*["\'\\]?([^"\'<>\s]{20,})',
        replace_token,
        html,
        flags=re.IGNORECASE,
    )

    # 10. CSRF tokens in meta tags
    def replace_csrf(match: re.Match[str]) -> str:
        collector.record_auto_redaction("csrf")
        prefix = match.group(1)
        suffix = match.group(3)
        return f"{prefix}{hasher.hash_generic(match.group(2), 'CSRF')}{suffix}"

    html = re.sub(
        r'(<meta[^>]*name=["\'\\]?csrf-token["\'\\]?[^>]*content=["\'\\]?)([^"\'\\]+)(["\'\\]?)',
        replace_csrf,
        html,
        flags=re.IGNORECASE,
    )

    # 11. Email addresses (RFC 5321 simplified)
    def replace_email(match: re.Match[str]) -> str:
        collector.record_auto_redaction("email")
        return hasher.hash_email(match.group(0))

    # Pattern supports: user@domain.tld, user.name+tag@sub.domain.co.uk
    html = re.sub(
        r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*\.[A-Za-z]{2,}\b",
        replace_email,
        html,
    )

    # 12. Config file paths (may contain ISP/customer identifiers)
    def replace_config(match: re.Match[str]) -> str:
        collector.record_auto_redaction("config")
        label = match.group(1)
        return f"{label}: {hasher.hash_generic(match.group(2), 'CONFIG')}"

    html = re.sub(
        r"(Config\s*File\s*Name|config\s*file)\s*[:\s=]+([^\s<>]+\.cfg)",
        replace_config,
        html,
        flags=re.IGNORECASE,
    )

    # 13. Motorola JavaScript password variables
    def replace_motorola_pw(match: re.Match[str]) -> str:
        collector.record_auto_redaction("password")
        prefix = match.group(1)
        suffix = match.group(3)
        return f"{prefix}{hasher.hash_generic(match.group(2), 'PASS')}{suffix}"

    html = re.sub(
        r"(var\s+Current(?:Pw|Password)[A-Za-z]*\s*=\s*['\"])([^'\"]+)(['\"])",
        replace_motorola_pw,
        html,
        flags=re.IGNORECASE,
    )

    # 14. WiFi credentials and device names in Netgear tagValueList
    safe_values = set(v.lower() for v in sensitive.get("tagValueList", {}).get("safe_values", []))

    # Import heuristics for detection
    from har_capture.sanitization.heuristics import (
        analyze_value,
        is_safe_value,
    )

    def sanitize_tag_value_list(match: re.Match[str]) -> str:
        """Sanitize pipe-delimited values in tagValueList.

        Always auto-redacts values matching KNOWN patterns (MAC addresses).

        When heuristics are enabled, analyzes remaining values to detect
        potential PII (WiFi credentials, SSIDs, device names) based on
        patterns, entropy, and context:
        - DISABLED: Skip heuristic analysis (default)
        - FLAG: Flag suspicious values for manual review
        - REDACT: Auto-redact suspicious values (may over-redact)
        """
        prefix = match.group(1)
        values_str = match.group(2)
        suffix = match.group(3)

        values = values_str.split("|")
        sanitized_values: list[str] = []

        for i, val in enumerate(values):
            val_stripped = val.strip()
            val_lower = val_stripped.lower()

            # Skip empty values, safe values, and already-redacted placeholders
            if not val_stripped or is_safe_value(val_stripped):
                sanitized_values.append(val)
                continue

            # Skip already-redacted values (e.g., MAC_xxxxx, MODEM_SN_xxxxx)
            if (
                val_stripped.startswith("***")
                or re.match(r"^[A-Z_]+_[a-f0-9]{8}$", val_stripped)
                or val_stripped == "XX:XX:XX:XX:XX:XX"
            ):
                sanitized_values.append(val)
                continue

            # Check if value is in safe list
            if val_lower in safe_values:
                sanitized_values.append(val)
                continue

            # AUTO-REDACT: Only known reliable patterns (MAC addresses within values)
            if re.match(r"^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$", val_stripped):
                collector.record_auto_redaction("mac_address")
                sanitized_values.append(hasher.hash_mac(val_stripped))
                continue

            # HEURISTICS: Analyze unknown values (opt-in)
            if heuristics != HeuristicMode.DISABLED:
                should_flag, confidence, category, reason = analyze_value(
                    val_stripped,
                    values_context=sanitized_values,
                    value_index=len(sanitized_values),
                )

                if should_flag:
                    if heuristics == HeuristicMode.REDACT:
                        # Auto-redact the flagged value
                        redacted = hasher.hash_sensitive_value(val_stripped, category)
                        sanitized_values.append(redacted)
                        collector.record_auto_redaction(category)
                        continue
                    if heuristics == HeuristicMode.FLAG:
                        # Flag for review, preserve value
                        from har_capture.cli.interactive import capture_pipe_context

                        context = capture_pipe_context(values, i)
                        collector.flag_value(
                            val_stripped,
                            category,
                            confidence,
                            context,
                            reason,
                        )
                        # Fall through to append original value

            # Preserve value (either not flagged, or flagged for review)
            sanitized_values.append(val)

        return prefix + "|".join(sanitized_values) + suffix

    html = re.sub(
        r"(var\s+tagValueList\s*=\s*['\"])([^'\"]+)(['\"])",
        sanitize_tag_value_list,
        html,
    )

    # 15. Other pipe-delimited variables (connectedDevices, deviceList, etc.)
    html = re.sub(
        r"(var\s+(?:connected)?[Dd]evice(?:s|List)?\s*=\s*['\"])([^'\"]+)(['\"])",
        sanitize_tag_value_list,
        html,
    )

    # 16. SSID fields in JavaScript objects (ssid_24g: 'value', guest_ssid: 'value')
    def replace_js_ssid(match: re.Match[str]) -> str:
        collector.record_auto_redaction("wifi")
        label = match.group(1)
        sep = match.group(2)
        quote1 = match.group(3)
        quote2 = match.group(5)
        return f"{label}{sep}{quote1}{hasher.hash_generic(match.group(4), 'WIFI')}{quote2}"

    html = re.sub(
        r'(\w*ssid\w*)\s*([=:])\s*(["\'])([^"\']+)(["\'])',
        replace_js_ssid,
        html,
        flags=re.IGNORECASE,
    )

    return html


def check_for_pii(
    content: str,
    filename: str = "",
    custom_patterns: str | None = None,
) -> list[dict[str, Any]]:
    """Check content for potential PII that should be sanitized.

    This function is intended for CI/PR validation to catch unsanitized
    fixtures before they are committed.

    Args:
        content: Text content to check (HTML, etc.)
        filename: Optional filename for context in warnings
        custom_patterns: Optional path to custom patterns JSON file

    Returns:
        List of dicts with 'pattern', 'match', 'line', and 'filename' for each PII found

    Example:
        >>> findings = check_for_pii("MAC: DE:AD:BE:EF:CA:FE")
        >>> findings[0]["pattern"]
        'mac_address'
    """
    pii = load_pii_patterns(custom_patterns)
    allowlist = load_allowlist(custom_patterns)
    findings: list[dict[str, Any]] = []

    for pattern_name, pattern_def in pii.get("patterns", {}).items():
        if not isinstance(pattern_def, dict) or "regex" not in pattern_def:
            continue

        regex = pattern_def["regex"]
        flags = 0
        if "flags" in pattern_def:
            for flag_name in pattern_def["flags"]:
                if flag_name == "IGNORECASE":
                    flags |= re.IGNORECASE

        matches = re.finditer(regex, content, flags)
        for match in matches:
            matched_text = match.group(0)

            # Skip if it's an allowlisted placeholder
            if is_allowlisted(matched_text, allowlist):
                continue

            # For IPv6 pattern, skip if it doesn't contain hex letters (a-f)
            if pattern_def.get("require_hex_letter") and not re.search(r"[a-f]", matched_text, re.IGNORECASE):
                continue

            # Find line number
            line_num = content.count("\n", 0, match.start()) + 1

            findings.append(
                {
                    "pattern": pattern_name,
                    "match": matched_text,
                    "line": line_num,
                    "filename": filename,
                }
            )

    return findings
