"""Cross-platform and supply chain compliance checks — run for all platforms."""

from __future__ import annotations

import re
from pathlib import Path
from typing import List, Optional, Tuple

from ..engine.runner import Finding


def scan(project_root: Path) -> List[Finding]:
    """Run cross-platform and supply chain compliance checks."""
    findings: List[Finding] = []
    root = Path(project_root).resolve()

    records = _collect_all_source_records(root)
    source = "\n".join(c for _, c in records)

    findings.extend(_check_regulatory(root, source, records))
    findings.extend(_check_gdpr(root, source, records))
    findings.extend(_check_accessibility_contrast(root, source, records))
    findings.extend(_check_data_retention(root, source, records))
    findings.extend(_check_supply_chain(root, source, records))

    return findings


# ---------------------------------------------------------------------------
# File loaders
# ---------------------------------------------------------------------------

def _collect_all_source(root: Path, limit: int = 500) -> str:
    """Collect all source files for cross-platform pattern matching."""
    chunks: List[str] = []
    count = 0
    skip_dirs = [
        "node_modules/", "dist/", "build/", ".next/", ".nuxt/",
        "DerivedData", "Pods/", ".build/", ".gradle/",
    ]
    extensions = (
        "*.swift", "*.m", "*.kt", "*.java",
        "*.js", "*.ts", "*.jsx", "*.tsx",
        "*.py", "*.rb", "*.go", "*.rs",
        "*.html", "*.vue", "*.astro",
        "*.xml", "*.plist", "*.yaml", "*.yml", "*.json",
    )
    for ext in extensions:
        for src in root.rglob(ext):
            rel = str(src)
            if any(skip in rel for skip in skip_dirs):
                continue
            try:
                chunks.append(src.read_text(errors="ignore"))
            except OSError:
                continue
            count += 1
            if count >= limit:
                return "\n".join(chunks)
    return "\n".join(chunks)


def _collect_all_source_records(root: Path, limit: int = 500) -> List[Tuple[Path, str]]:
    """Collect all source files as (path, content) records."""
    records: List[Tuple[Path, str]] = []
    count = 0
    skip_dirs = [
        "node_modules/", "dist/", "build/", ".next/", ".nuxt/",
        "DerivedData", "Pods/", ".build/", ".gradle/",
    ]
    extensions = (
        "*.swift", "*.m", "*.kt", "*.java",
        "*.js", "*.ts", "*.jsx", "*.tsx",
        "*.py", "*.rb", "*.go", "*.rs",
        "*.html", "*.vue", "*.astro",
        "*.xml", "*.plist", "*.yaml", "*.yml", "*.json",
    )
    for ext in extensions:
        for src in root.rglob(ext):
            rel = str(src)
            if any(skip in rel for skip in skip_dirs):
                continue
            try:
                records.append((src, src.read_text(errors="ignore")))
            except OSError:
                continue
            count += 1
            if count >= limit:
                return records
    return records


def _find_in_records(
    records: List[Tuple[Path, str]],
    patterns: List[str],
    root: Path,
    flags: int = re.IGNORECASE,
) -> Optional[Tuple[str, int]]:
    """Return (relative_path, line_number) for first matching line, or None."""
    for path, content in records:
        rel = str(path.relative_to(root))
        for lineno, line in enumerate(content.splitlines(), 1):
            for pattern in patterns:
                if re.search(pattern, line, flags):
                    return (rel, lineno)
    return None


# ---------------------------------------------------------------------------
# Regulatory Compliance (CROSS-*)
# ---------------------------------------------------------------------------

def _check_regulatory(root: Path, source: str, records: List[Tuple[Path, str]]) -> List[Finding]:
    findings: List[Finding] = []

    # CROSS-COPPA-001 — COPPA (Children's Apps)
    children_indicators = [
        r"(?i)children|child|kid|minor|under.?13|parental.?consent",
        r"(?i)age.?gate|ageGate|age_gate|age.?verification",
        r"(?i)kCFStreamPropertySSLSettings",  # iOS
        r"(?i)isChildDirected|child_directed|tagForChildDirectedTreatment",
    ]
    coppa_compliance = [
        r"(?i)coppa|parental.?consent|verifiable.?parental",
        r"(?i)age.?verification|age.?gate|ageGate",
    ]
    children_hit = _find_in_records(records, children_indicators, root)
    has_coppa_compliance = any(re.search(p, source) for p in coppa_compliance)
    if children_hit and not has_coppa_compliance:
        findings.append(Finding(
            rule_id="CROSS-COPPA-001",
            severity="FAIL",
            message="App appears to target children but no COPPA compliance mechanisms detected",
            fix="Implement age gating, parental consent flows, and limit data collection per COPPA requirements.",
            reference="FTC COPPA Rule — https://www.ftc.gov/legal-library/browse/rules/childrens-online-privacy-protection-rule-coppa",
            file_path=children_hit[0],
            line=children_hit[1],
        ))

    # CROSS-HIPAA-001 — HIPAA markers
    health_data_patterns = [
        r"(?i)(?:patient|medical|diagnosis|prescription|health.?record|PHI|protected.?health)",
        r"(?i)(?:HL7|FHIR|DICOM|ICD.?10|CPT.?code)",
        r"(?i)(?:HKHealthStore|HealthKit|healthkit)",
    ]
    hipaa_compliance_patterns = [
        r"(?i)(?:hipaa|HIPAA|baa|business.?associate|encryption.?at.?rest)",
        r"(?i)(?:audit.?log|access.?control|data.?encryption)",
    ]
    health_hit = _find_in_records(records, health_data_patterns, root)
    has_hipaa = any(re.search(p, source) for p in hipaa_compliance_patterns)
    if health_hit and not has_hipaa:
        findings.append(Finding(
            rule_id="CROSS-HIPAA-001",
            severity="WARN",
            message="Health/medical data handling detected without HIPAA compliance markers",
            fix="Ensure HIPAA compliance: encryption at rest/transit, access controls, audit logging, BAA with cloud providers.",
            reference="HHS HIPAA Security Rule — https://www.hhs.gov/hipaa/for-professionals/security/index.html",
            file_path=health_hit[0],
            line=health_hit[1],
        ))

    # CROSS-PCI-001 — PCI-DSS (Payment data)
    payment_patterns = [
        r"(?i)(?:credit.?card|card.?number|cvv|cvc|pan\b|card.?holder)",
        r"(?i)(?:stripe|braintree|adyen|square|paypal)",
        r"(?i)(?:payment|checkout|billing)",
    ]
    pci_safe_patterns = [
        r"(?i)(?:stripe\.js|elements|paymentIntent|PaymentSheet|STPPaymentHandler)",
        r"(?i)(?:braintree\.dropin|hosted.?fields|tokenize)",
        r"(?i)(?:pci.?dss|pci.?compliant|tokenized)",
    ]
    has_payments = any(re.search(p, source) for p in payment_patterns)
    has_pci_safe = any(re.search(p, source) for p in pci_safe_patterns)
    raw_card_patterns = [
        r"(?i)card.?number\s*[:=]",
        r"(?i)cvv\s*[:=]",
        r"(?i)input.*(?:card|credit).*number",
    ]
    raw_card_hit = _find_in_records(records, raw_card_patterns, root)
    if has_payments and raw_card_hit and not has_pci_safe:
        findings.append(Finding(
            rule_id="CROSS-PCI-001",
            severity="FAIL",
            message="Raw payment card data handling detected without PCI-DSS tokenization",
            fix="Use a PCI-compliant payment processor (Stripe Elements, Braintree Hosted Fields) instead of handling raw card data.",
            reference="PCI DSS v4.0 — https://www.pcisecuritystandards.org/",
            file_path=raw_card_hit[0],
            line=raw_card_hit[1],
        ))

    return findings


# ---------------------------------------------------------------------------
# GDPR Compliance (CROSS-GDPR-*)
# ---------------------------------------------------------------------------

def _check_gdpr(root: Path, source: str, records: List[Tuple[Path, str]]) -> List[Finding]:
    findings: List[Finding] = []

    personal_data_patterns = [
        r"(?i)(?:user.?data|personal.?data|PII|profile|registration|sign.?up)",
        r"(?i)(?:createUser|registerUser|saveProfile|updateProfile)",
        r"(?i)(?:email|phone|address|birth|gender|name\s*[:=])",
    ]
    personal_hit = _find_in_records(records, personal_data_patterns, root)
    has_personal_data = personal_hit is not None

    # CROSS-GDPR-001 — Right to data portability (Art. 20)
    export_patterns = [
        r"(?i)(?:export.?data|download.?data|data.?export|data.?download|data.?portability)",
        r"(?i)(?:takeout|my.?data|request.?data|subject.?access.?request)",
        r"(?i)(?:toJSON|toCSV|data.?dump|generate.?report.*(?:user|personal))",
    ]
    has_export = any(re.search(p, source) for p in export_patterns)
    if has_personal_data and not has_export:
        findings.append(Finding(
            rule_id="CROSS-GDPR-001",
            severity="WARN",
            message="Personal data collection detected but no data export/portability mechanism found",
            fix="Implement data export functionality allowing users to download their data in a portable format (JSON, CSV).",
            reference="GDPR Article 20 — Right to Data Portability",
            file_path=personal_hit[0] if personal_hit else "",
            line=personal_hit[1] if personal_hit else 0,
        ))

    # CROSS-GDPR-002 — Consent before processing (Art. 6/7)
    auto_tracking_patterns = [
        r"(?i)(?:gtag|ga|analytics|fbq|_paq)\s*\(\s*['\"](?:init|create|config|track)['\"]",
        r"(?i)(?:GoogleAnalytics|FirebaseAnalytics\.logEvent|Amplitude\.init|Mixpanel\.init)",
        r"(?i)(?:useEffect|componentDidMount|ngOnInit|mounted|onMount).*(?:analytics|tracking|gtag)",
    ]
    consent_gate_patterns = [
        r"(?i)(?:if.*consent|consent.*then|hasConsent|isConsented|consentGiven)",
        r"(?i)(?:waitForConsent|onConsent|consentCallback|consent.?check)",
        r"(?i)(?:OptanonWrapper|OneTrust|cookiebot|CookieConsent.*accept)",
    ]
    tracking_hit = _find_in_records(records, auto_tracking_patterns, root)
    has_consent_gate = any(re.search(p, source) for p in consent_gate_patterns)
    if tracking_hit and not has_consent_gate:
        findings.append(Finding(
            rule_id="CROSS-GDPR-002",
            severity="FAIL",
            message="Analytics/tracking appears to initialize without waiting for user consent",
            fix="Gate analytics initialization behind user consent. Do not load tracking scripts until consent is granted.",
            reference="GDPR Articles 6 & 7 — Lawfulness of processing / Conditions for consent",
            file_path=tracking_hit[0],
            line=tracking_hit[1],
        ))

    # CROSS-GDPR-003 — Privacy contact / DPO (Art. 13/37)
    dpo_patterns = [
        r"(?i)(?:data.?protection.?officer|DPO\b|privacy.?officer|privacy.?contact)",
        r"(?i)(?:dpo@|privacy@|dataprotection@|gdpr@)",
        r"(?i)(?:privacy.?team|data.?protection.?team|privacy.?department)",
    ]
    has_dpo = any(re.search(p, source) for p in dpo_patterns)
    # Also check privacy policy pages
    privacy_pages = list(root.rglob("*privacy*"))
    privacy_pages = [p for p in privacy_pages if "node_modules" not in str(p) and "build" not in str(p)]
    for pp in privacy_pages[:5]:
        try:
            pp_content = pp.read_text(errors="ignore")
        except OSError:
            continue
        if any(re.search(p, pp_content) for p in dpo_patterns):
            has_dpo = True
            break
    if has_personal_data and not has_dpo:
        findings.append(Finding(
            rule_id="CROSS-GDPR-003",
            severity="INFO",
            message="Personal data processing detected but no Data Protection Officer or privacy contact found",
            fix="Include DPO or privacy contact information in your privacy policy (required for certain controllers/processors).",
            reference="GDPR Articles 13(1)(b) & 37 — DPO designation",
            file_path=personal_hit[0] if personal_hit else "",
            line=personal_hit[1] if personal_hit else 0,
        ))

    return findings


# ---------------------------------------------------------------------------
# Accessibility — Color Contrast (CROSS-A11Y-001)
# ---------------------------------------------------------------------------

def _check_accessibility_contrast(root: Path, source: str, records: List[Tuple[Path, str]]) -> List[Finding]:
    findings: List[Finding] = []

    low_contrast_patterns = [
        r'color\s*:\s*#(?:fff|FFF|ffffff|FFFFFF)[^;]*;[^}]*background[^:]*:\s*#(?:fff|FFF|ffffff|FFFFFF|fafafa|f5f5f5)',
        r'color\s*:\s*#(?:eee|ddd|ccc|bbb|aaa)[^;]*;',
        r'color\s*:\s*(?:lightgray|lightgrey|silver)\b',
    ]

    contrast_hit = _find_in_records(records, low_contrast_patterns, root)

    # Also check CSS files specifically
    if not contrast_hit:
        css_records: List[Tuple[Path, str]] = []
        for css_file in root.rglob("*.css"):
            if any(skip in str(css_file) for skip in ["node_modules/", "dist/", "build/"]):
                continue
            try:
                css_records.append((css_file, css_file.read_text(errors="ignore")))
            except OSError:
                continue
        contrast_hit = _find_in_records(css_records, low_contrast_patterns, root)

    if contrast_hit:
        findings.append(Finding(
            rule_id="CROSS-A11Y-001",
            severity="WARN",
            message="Potential low color contrast detected in styles",
            fix="Ensure text meets WCAG 2.1 contrast ratios: 4.5:1 for normal text, 3:1 for large text.",
            reference="WCAG 2.1 — 1.4.3 Contrast (Minimum)",
            file_path=contrast_hit[0],
            line=contrast_hit[1],
        ))

    return findings


# ---------------------------------------------------------------------------
# Data Retention (CROSS-DATA-001)
# ---------------------------------------------------------------------------

def _check_data_retention(root: Path, source: str, records: List[Tuple[Path, str]]) -> List[Finding]:
    findings: List[Finding] = []

    data_collection_patterns = [
        r"(?i)(?:firebase|analytics|tracking|telemetry)",
        r"(?i)(?:user.?data|personal.?data|PII|personally.?identifiable)",
        r"(?i)(?:database|collection|store|persist)",
    ]
    retention_patterns = [
        r"(?i)(?:data.?retention|retention.?policy|data.?deletion|delete.?account)",
        r"(?i)(?:right.?to.?erasure|right.?to.?delete|forget.?me|data.?purge)",
        r"(?i)(?:TTL|time.?to.?live|expir|auto.?delete|scheduled.?deletion)",
    ]
    data_hit = _find_in_records(records, data_collection_patterns, root)
    has_retention_policy = any(re.search(p, source) for p in retention_patterns)
    if data_hit and not has_retention_policy:
        findings.append(Finding(
            rule_id="CROSS-DATA-001",
            severity="INFO",
            message="Data collection detected but no data retention or deletion policy found",
            fix="Implement data retention policies and account deletion capability (required by GDPR Art. 17 and App Store guidelines).",
            reference="GDPR Article 17 — Right to Erasure",
            file_path=data_hit[0],
            line=data_hit[1],
        ))

    return findings


# ---------------------------------------------------------------------------
# Supply Chain (SUPPLY-*)
# ---------------------------------------------------------------------------

def _check_supply_chain(root: Path, source: str, records: List[Tuple[Path, str]]) -> List[Finding]:
    findings: List[Finding] = []

    # SUPPLY-LIC-001 — GPL license in potentially proprietary app
    license_files = list(root.rglob("LICENSE*")) + list(root.rglob("LICENCE*"))
    license_files = [f for f in license_files if "node_modules/" not in str(f)]
    package_json = root / "package.json"
    podfile = root / "Podfile"
    build_gradle_files = list(root.rglob("build.gradle")) + list(root.rglob("build.gradle.kts"))
    dep_content = ""
    dep_file_path = ""
    for dep_file in [package_json, podfile] + build_gradle_files:
        if dep_file.exists():
            try:
                dep_content += dep_file.read_text(errors="ignore") + "\n"
                if not dep_file_path:
                    dep_file_path = str(dep_file.relative_to(root))
            except OSError:
                continue
    # Check node_modules for GPL licenses
    gpl_indicators: list[tuple[str, str]] = []
    node_modules = root / "node_modules"
    if node_modules.exists():
        for pkg_json in node_modules.glob("*/package.json"):
            try:
                import json
                pkg_data = json.loads(pkg_json.read_text(errors="ignore"))
                license_val = pkg_data.get("license", "")
                if isinstance(license_val, str) and re.search(r'\bGPL\b', license_val):
                    gpl_indicators.append((
                        pkg_data.get("name", str(pkg_json.parent.name)),
                        str(pkg_json.relative_to(root)),
                    ))
            except (json.JSONDecodeError, OSError):
                continue
    if gpl_indicators:
        names = [name for name, _ in gpl_indicators[:5]]
        findings.append(Finding(
            rule_id="SUPPLY-LIC-001",
            severity="WARN",
            message=f"GPL-licensed dependencies found: {', '.join(names)}",
            fix="Review GPL dependencies for license compatibility. GPL code in proprietary apps may require open-sourcing your code.",
            reference="GNU GPL License — https://www.gnu.org/licenses/gpl-3.0.html",
            file_path=gpl_indicators[0][1],
        ))

    # SUPPLY-DEP-001 — Outdated SDK version checks
    outdated_patterns = [
        (r'"react"\s*:\s*"[\^~]?(?:16|15|14)\.', "React (< 17)"),
        (r'"angular/core"\s*:\s*"[\^~]?(?:[0-9]|1[0-4])\.', "Angular (< 15)"),
        (r'"vue"\s*:\s*"[\^~]?2\.', "Vue.js 2.x"),
        (r'"next"\s*:\s*"[\^~]?(?:[0-9]|1[0-2])\.', "Next.js (< 13)"),
        (r'"express"\s*:\s*"[\^~]?[0-3]\.', "Express (< 4)"),
        (r'compileSdkVersion\s+(?:2[0-9]|3[0-2])', "Android compileSdk (< 33)"),
    ]
    outdated_found = []
    dep_hit: Optional[Tuple[str, int]] = None
    for pattern, label in outdated_patterns:
        if re.search(pattern, dep_content + source):
            outdated_found.append(label)
            if dep_hit is None:
                dep_hit = _find_in_records(records, [pattern], root, flags=0)
    if outdated_found:
        findings.append(Finding(
            rule_id="SUPPLY-DEP-001",
            severity="INFO",
            message=f"Outdated framework/SDK versions detected: {', '.join(outdated_found)}",
            fix="Update to the latest stable versions for security patches and feature support.",
            reference="OWASP A06:2021 — Vulnerable and Outdated Components",
            file_path=dep_hit[0] if dep_hit else dep_file_path,
            line=dep_hit[1] if dep_hit else 0,
        ))

    # SUPPLY-API-001 — Deprecated API usage
    deprecated_api_patterns = [
        (r'\bcomponentWillMount\b', "React componentWillMount (deprecated)"),
        (r'\bcomponentWillReceiveProps\b', "React componentWillReceiveProps (deprecated)"),
        (r'\bUIWebView\b', "iOS UIWebView (deprecated, use WKWebView)"),
        (r'\bABAddressBook\b', "iOS ABAddressBook (deprecated, use CNContactStore)"),
        (r'\bUIAlertView\b', "iOS UIAlertView (deprecated, use UIAlertController)"),
        (r'\bAsyncTask\b', "Android AsyncTask (deprecated, use coroutines/RxJava)"),
        (r'\bIntentService\b', "Android IntentService (deprecated, use WorkManager)"),
        (r'\bgetExternalStorageDirectory\b', "Android getExternalStorageDirectory (deprecated)"),
    ]
    deprecated_found = []
    api_hit: Optional[Tuple[str, int]] = None
    for pattern, label in deprecated_api_patterns:
        if re.search(pattern, source):
            deprecated_found.append(label)
            if api_hit is None:
                api_hit = _find_in_records(records, [pattern], root, flags=0)
    if deprecated_found:
        findings.append(Finding(
            rule_id="SUPPLY-API-001",
            severity="INFO",
            message=f"Deprecated API usage detected: {', '.join(deprecated_found[:5])}",
            fix="Migrate to modern API replacements to ensure continued platform support.",
            reference="Platform deprecation documentation",
            file_path=api_hit[0] if api_hit else "",
            line=api_hit[1] if api_hit else 0,
        ))

    return findings
