"""Load and validate YAML rule catalogs."""

from __future__ import annotations

from pathlib import Path
import re
from typing import Any, Dict, List, Optional

import yaml


_RULES_DIR = Path(__file__).parent

_VALID_SEVERITIES = {"FAIL", "WARN", "INFO"}
_URL_PATTERN = re.compile(r"https?://[^\s)]+")
_FILE_BY_PLATFORM = {
    "ios": "ios_rules.yaml",
    "android": "android_rules.yaml",
    "web": "web_rules.yaml",
    "common": "common_rules.yaml",
}

# Used to normalize textual references into canonical, official URLs.
_REFERENCE_ALIASES: tuple[tuple[re.Pattern[str], str], ...] = (
    (re.compile(r"apple app store review guidelines", re.IGNORECASE), "https://developer.apple.com/app-store/review/guidelines/"),
    (re.compile(r"apple app tracking transparency", re.IGNORECASE), "https://developer.apple.com/app-store/user-privacy-and-data-use/"),
    (re.compile(r"apple ats documentation", re.IGNORECASE), "https://developer.apple.com/documentation/bundleresources/information_property_list/nsapptransportsecurity"),
    (re.compile(r"privacy manifest", re.IGNORECASE), "https://developer.apple.com/documentation/bundleresources/privacy_manifest_files"),
    (re.compile(r"keychain services", re.IGNORECASE), "https://developer.apple.com/documentation/security/keychain_services"),
    (re.compile(r"google play .*target", re.IGNORECASE), "https://developer.android.com/google/play/requirements/target-sdk"),
    (re.compile(r"google play permissions policy", re.IGNORECASE), "https://support.google.com/googleplay/android-developer/answer/9888170"),
    (re.compile(r"background location policy", re.IGNORECASE), "https://support.google.com/googleplay/android-developer/answer/9799150"),
    (re.compile(r"sms/call log policy", re.IGNORECASE), "https://support.google.com/googleplay/android-developer/answer/9047303"),
    (re.compile(r"google play ads policy", re.IGNORECASE), "https://support.google.com/googleplay/android-developer/answer/9857753"),
    (re.compile(r"google play developer program policies.*user data", re.IGNORECASE), "https://support.google.com/googleplay/android-developer/answer/10144311"),
    (re.compile(r"owasp secure headers", re.IGNORECASE), "https://owasp.org/www-project-secure-headers/"),
    (re.compile(r"owasp api security", re.IGNORECASE), "https://owasp.org/API-Security/"),
    (re.compile(r"owasp top 10", re.IGNORECASE), "https://owasp.org/www-project-top-ten/"),
    (re.compile(r"wcag 2\.1", re.IGNORECASE), "https://www.w3.org/TR/WCAG21/"),
    (re.compile(r"w3c permissions-policy", re.IGNORECASE), "https://www.w3.org/TR/permissions-policy/"),
    (re.compile(r"mdn .*content-security-policy", re.IGNORECASE), "https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP"),
    (re.compile(r"mdn .*subresource integrity", re.IGNORECASE), "https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity"),
)


def _load_yaml_file(filename: str) -> Dict[str, Any]:
    """Load a single YAML rule file from the rules directory."""
    path = _RULES_DIR / filename
    if not path.exists():
        return {}
    with open(path, "r") as f:
        return yaml.safe_load(f) or {}


def load_rules(platform: str) -> List[Dict[str, Any]]:
    """Load all rules for a given platform from its YAML catalog.

    Returns a flat list of rule dicts with keys: id, summary, severity, fix, reference, detection.
    """
    filename = _FILE_BY_PLATFORM.get(platform)
    if not filename:
        return []

    data = _load_yaml_file(filename)
    categories = data.get("categories", [])

    rules: List[Dict[str, Any]] = []
    for category in categories:
        for rule in category.get("rules", []):
            rules.append(rule)

    return rules


def get_rule_index(platform: str) -> Dict[str, Dict[str, Any]]:
    """Return platform rules indexed by ID."""
    return {rule["id"]: rule for rule in load_rules(platform) if rule.get("id")}


def _resolve_reference(reference: str) -> str:
    """Attach official URL to a textual reference when possible."""
    ref = (reference or "").strip()
    if not ref:
        return ref
    if _URL_PATTERN.search(ref):
        return ref
    for pattern, url in _REFERENCE_ALIASES:
        if pattern.search(ref):
            return f"{ref} — {url}"
    return ref


def apply_catalog_to_findings(platform: str, findings: List[Any]) -> List[str]:
    """Normalize finding metadata from rule catalogs and return unknown rule IDs."""
    rules_by_id = get_rule_index(platform)
    unknown_rule_ids: List[str] = []

    for finding in findings:
        rule = rules_by_id.get(getattr(finding, "rule_id", ""))
        if rule:
            if rule.get("severity"):
                finding.severity = rule["severity"]
            if rule.get("fix"):
                finding.fix = rule["fix"]
            rule_ref = str(rule.get("reference", "")).strip()
            finding.reference = _resolve_reference(rule_ref or finding.reference)
            if not finding.message and rule.get("summary"):
                finding.message = str(rule["summary"])
            continue

        # Even if a rule is not in YAML, still normalize textual references.
        finding.reference = _resolve_reference(getattr(finding, "reference", ""))
        unknown_rule_ids.append(getattr(finding, "rule_id", ""))

    return sorted({rid for rid in unknown_rule_ids if rid})


def get_rule_ids(platform: str) -> List[str]:
    """Return all rule IDs for a given platform."""
    return [r["id"] for r in load_rules(platform)]


def get_rule_by_id(platform: str, rule_id: str) -> Optional[Dict[str, Any]]:
    """Look up a single rule by its ID."""
    for rule in load_rules(platform):
        if rule["id"] == rule_id:
            return rule
    return None


def validate_rules(platform: str) -> List[str]:
    """Validate rule catalog structure. Returns list of error messages."""
    errors: List[str] = []
    rules = load_rules(platform)

    seen_ids: set = set()
    for rule in rules:
        rid = rule.get("id", "")
        if not rid:
            errors.append("Rule missing 'id' field")
            continue

        if rid in seen_ids:
            errors.append(f"Duplicate rule ID: {rid}")
        seen_ids.add(rid)

        if rule.get("severity") not in _VALID_SEVERITIES:
            errors.append(f"{rid}: Invalid severity '{rule.get('severity')}'")
        if not rule.get("summary"):
            errors.append(f"{rid}: Missing 'summary'")
        if not rule.get("fix"):
            errors.append(f"{rid}: Missing 'fix'")
        if not _resolve_reference(str(rule.get("reference", ""))):
            errors.append(f"{rid}: Missing 'reference'")

    return errors
