"""Web compliance scanner — checks security headers, OWASP risks, a11y, and privacy."""

from __future__ import annotations

import re
import subprocess
import json
from pathlib import Path
from typing import List, Optional

from ..engine.runner import Finding


def scan(project_root: Path) -> List[Finding]:
    """Run all web compliance checks and return findings."""
    findings: List[Finding] = []
    root = Path(project_root).resolve()

    config_contents = _collect_config_files(root)
    source_contents = _collect_source(root)
    html_contents = _collect_html(root)

    findings.extend(_check_security_headers(root, config_contents, source_contents))
    findings.extend(_check_owasp(root, source_contents, html_contents))
    findings.extend(_check_secrets(source_contents))
    findings.extend(_check_accessibility(root, html_contents + source_contents))
    findings.extend(_check_privacy(root, html_contents + source_contents, config_contents))

    return findings


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

_CONFIG_GLOBS = [
    "next.config.*", "nuxt.config.*", "astro.config.*",
    "vercel.json", "netlify.toml", "_headers", ".htaccess",
    "nginx.conf", "server.js", "server.ts", "app.js", "app.ts",
    "vite.config.*", "vue.config.*", "svelte.config.*",
]


def _collect_config_files(root: Path) -> str:
    chunks: List[str] = []
    for pattern in _CONFIG_GLOBS:
        for f in root.glob(pattern):
            try:
                chunks.append(f.read_text(errors="ignore"))
            except OSError:
                continue
    return "\n".join(chunks)


def _collect_source(root: Path, limit: int = 300) -> str:
    """Collect JS/TS/Dart source files (excluding node_modules/dist)."""
    chunks: List[str] = []
    count = 0
    for ext in ("*.js", "*.ts", "*.jsx", "*.tsx", "*.dart"):
        if count >= limit:
            break
        for src in root.rglob(ext):
            rel = str(src.relative_to(root))
            if any(skip in rel for skip in ["node_modules/", "dist/", "build/", ".next/", ".nuxt/", ".dart_tool/"]):
                continue
            try:
                chunks.append(src.read_text(errors="ignore"))
            except OSError:
                continue
            count += 1
            if count >= limit:
                break
    return "\n".join(chunks)


def _collect_html(root: Path, limit: int = 200) -> str:
    """Collect HTML / template files (JSX/TSX/Dart handled by _collect_source)."""
    chunks: List[str] = []
    count = 0
    for ext in ("*.html", "*.vue", "*.svelte", "*.astro", "*.hbs", "*.ejs"):
        if count >= limit:
            break
        for src in root.rglob(ext):
            rel = str(src.relative_to(root))
            if any(skip in rel for skip in ["node_modules/", "dist/", "build/", ".next/"]):
                continue
            try:
                chunks.append(src.read_text(errors="ignore"))
            except OSError:
                continue
            count += 1
            if count >= limit:
                break
    return "\n".join(chunks)


# ---------------------------------------------------------------------------
# Security Headers (WEB-SEC-*)
# ---------------------------------------------------------------------------

_HEADER_CHECKS = [
    {
        "id": "WEB-SEC-001",
        "patterns": [r"Content-Security-Policy", r"contentSecurityPolicy", r"\bcsp\b"],
        "summary": "Missing Content-Security-Policy header configuration",
        "fix": "Configure Content-Security-Policy header in your server/deployment config.",
        "reference": "OWASP Secure Headers — Content-Security-Policy",
        "severity": "FAIL",
    },
    {
        "id": "WEB-SEC-002",
        "patterns": [r"Strict-Transport-Security", r"strictTransportSecurity", r"\bhsts\b"],
        "summary": "Missing Strict-Transport-Security header",
        "fix": "Add Strict-Transport-Security header (e.g. max-age=31536000; includeSubDomains).",
        "reference": "OWASP Secure Headers — HSTS",
        "severity": "FAIL",
    },
    {
        "id": "WEB-SEC-003",
        "patterns": [r"X-Frame-Options", r"xFrameOptions", r"frameguard"],
        "summary": "Missing X-Frame-Options header",
        "fix": "Add X-Frame-Options: DENY (or SAMEORIGIN) header to prevent clickjacking.",
        "reference": "OWASP Secure Headers — X-Frame-Options",
        "severity": "WARN",
    },
    {
        "id": "WEB-SEC-004",
        "patterns": [r"X-Content-Type-Options", r"noSniff", r"xContentTypeOptions"],
        "summary": "Missing X-Content-Type-Options header",
        "fix": "Add X-Content-Type-Options: nosniff header to prevent MIME-type sniffing.",
        "reference": "OWASP Secure Headers — X-Content-Type-Options",
        "severity": "WARN",
    },
    {
        "id": "WEB-SEC-005",
        "patterns": [r"Referrer-Policy", r"referrerPolicy"],
        "summary": "Missing Referrer-Policy header",
        "fix": "Add Referrer-Policy header (e.g. strict-origin-when-cross-origin).",
        "reference": "OWASP Secure Headers — Referrer-Policy",
        "severity": "INFO",
    },
    {
        "id": "WEB-SEC-006",
        "patterns": [r"Permissions-Policy", r"permissionsPolicy", r"Feature-Policy"],
        "summary": "Missing Permissions-Policy header",
        "fix": "Add Permissions-Policy header to control browser feature access (camera, mic, geolocation).",
        "reference": "W3C Permissions-Policy",
        "severity": "INFO",
    },
]


def _check_security_headers(root: Path, config_contents: str, source: str = "") -> List[Finding]:
    findings: List[Finding] = []
    for check in _HEADER_CHECKS:
        found = any(re.search(p, config_contents, re.IGNORECASE) for p in check["patterns"])
        if not found:
            findings.append(Finding(
                rule_id=check["id"],
                severity=check["severity"],
                message=check["summary"],
                fix=check["fix"],
                reference=check["reference"],
            ))

    # WEB-SEC-008 — CORS misconfiguration (wildcard origin)
    cors_wildcard_patterns = [
        r'Access-Control-Allow-Origin["\s:]*\*',
        r'(?:allowOrigin|origin)\s*[:=]\s*["\']?\*["\']?',
        r'cors\s*\(\s*\)',  # express cors() with no options = allow all
    ]
    cors_misconfigured = any(re.search(p, config_contents + source, re.IGNORECASE) for p in cors_wildcard_patterns)
    if cors_misconfigured:
        findings.append(Finding(
            rule_id="WEB-SEC-008",
            severity="WARN",
            message="CORS configured with wildcard origin (Access-Control-Allow-Origin: *)",
            fix="Restrict CORS to specific trusted origins instead of using wildcard (*).",
            reference="OWASP — CORS Misconfiguration",
        ))

    # WEB-SEC-009 — CSP with unsafe-inline / unsafe-eval
    csp_content = ""
    csp_match = re.search(r'Content-Security-Policy["\s:]+([^\n"]+)', config_contents, re.IGNORECASE)
    if csp_match:
        csp_content = csp_match.group(1)
    csp_source_match = re.search(r'contentSecurityPolicy["\s:]+([^\n"]+)', config_contents + source, re.IGNORECASE)
    if csp_source_match:
        csp_content += " " + csp_source_match.group(1)
    if csp_content:
        unsafe_found = []
        if re.search(r"'unsafe-inline'", csp_content):
            unsafe_found.append("unsafe-inline")
        if re.search(r"'unsafe-eval'", csp_content):
            unsafe_found.append("unsafe-eval")
        if unsafe_found:
            findings.append(Finding(
                rule_id="WEB-SEC-009",
                severity="WARN",
                message=f"CSP contains {', '.join(unsafe_found)} — weakens XSS protection",
                fix="Remove 'unsafe-inline' and 'unsafe-eval' from CSP. Use nonces or hashes instead.",
                reference="MDN — Content-Security-Policy — unsafe-inline",
            ))

    # WEB-SEC-010 — No rate limiting detected
    rate_limit_patterns = [
        r"rate.?limit", r"rateLimit", r"express-rate-limit", r"RateLimiter",
        r"throttle", r"Throttle", r"slowDown", r"express-slow-down",
    ]
    combined = config_contents + source
    has_rate_limiting = any(re.search(p, combined, re.IGNORECASE) for p in rate_limit_patterns)
    if not has_rate_limiting and source:
        findings.append(Finding(
            rule_id="WEB-SEC-010",
            severity="INFO",
            message="No rate limiting or request throttling detected",
            fix="Add rate limiting middleware (e.g. express-rate-limit) to prevent abuse and DDoS.",
            reference="OWASP API Security — Rate Limiting",
        ))

    return findings


# ---------------------------------------------------------------------------
# OWASP Top 10 (WEB-OWASP-*)
# ---------------------------------------------------------------------------

def _check_owasp(root: Path, source: str, html: str) -> List[Finding]:
    findings: List[Finding] = []

    # WEB-OWASP-001 — Dependency vulnerabilities (via npm audit)
    lockfiles = {
        "package-lock.json": "npm",
        "yarn.lock": "yarn",
        "pnpm-lock.yaml": "pnpm",
        "bun.lockb": "bun",
    }
    for lockfile, manager in lockfiles.items():
        if (root / lockfile).exists():
            vuln_count = _run_audit(root, manager)
            if vuln_count and vuln_count > 0:
                findings.append(Finding(
                    rule_id="WEB-OWASP-001",
                    severity="FAIL",
                    message=f"Known dependency vulnerabilities detected ({vuln_count} via {manager} audit)",
                    fix=f"Run `{manager} audit fix` or update vulnerable packages.",
                    reference="OWASP A06:2021 — Vulnerable and Outdated Components",
                ))
            break  # only run one audit

    # WEB-OWASP-002 — Unsafe eval usage
    eval_patterns = [r'\beval\s*\(', r'new\s+Function\s*\(', r'setTimeout\s*\(\s*["\']', r'setInterval\s*\(\s*["\']']
    matches = sum(len(re.findall(p, source)) for p in eval_patterns)
    if matches > 0:
        findings.append(Finding(
            rule_id="WEB-OWASP-002",
            severity="WARN",
            message=f"Unsafe eval() / new Function() usage found ({matches} occurrence(s))",
            fix="Remove eval() and new Function() calls. Use safer alternatives.",
            reference="OWASP A03:2021 — Injection",
        ))

    # WEB-OWASP-003 — Missing input sanitization (innerHTML / dangerouslySetInnerHTML)
    inner_html_patterns = [
        r'\.innerHTML\s*=', r'\.outerHTML\s*=', r'insertAdjacentHTML\s*\(',
        r'dangerouslySetInnerHTML', r'v-html\s*=', r'\[innerHTML\]\s*=',
        r'document\.write\s*\(',
    ]
    inner_matches = sum(len(re.findall(p, source + html)) for p in inner_html_patterns)
    if inner_matches > 0:
        findings.append(Finding(
            rule_id="WEB-OWASP-003",
            severity="WARN",
            message=f"Potential XSS vectors found ({inner_matches} instance(s) of innerHTML/dangerouslySetInnerHTML)",
            fix="Use textContent instead of innerHTML, or sanitize input with DOMPurify before rendering.",
            reference="OWASP A03:2021 — Injection (Cross-Site Scripting)",
        ))

    # WEB-OWASP-004 — Insecure session/cookie configuration
    insecure_cookie_patterns = [
        r'(?:httpOnly|HttpOnly)\s*[:=]\s*false',
        r'(?:secure|Secure)\s*[:=]\s*false',
        r'(?:sameSite|SameSite)\s*[:=]\s*["\']?none["\']?',
    ]
    insecure_count = sum(len(re.findall(p, source, re.IGNORECASE)) for p in insecure_cookie_patterns)
    if insecure_count > 0:
        findings.append(Finding(
            rule_id="WEB-OWASP-004",
            severity="WARN",
            message=f"Insecure cookie/session configuration detected ({insecure_count} instance(s))",
            fix="Set httpOnly: true, secure: true, and sameSite: 'strict' or 'lax' on cookies.",
            reference="OWASP A07:2021 — Identification and Authentication Failures",
        ))

    # WEB-OWASP-005 — Missing SRI on external scripts
    external_scripts = re.findall(r'<script[^>]+src\s*=\s*["\']https?://[^"\']+["\'][^>]*>', html + source)
    for script in external_scripts:
        if "integrity=" not in script:
            findings.append(Finding(
                rule_id="WEB-OWASP-005",
                severity="INFO",
                message="External script loaded without Subresource Integrity (SRI) hash",
                fix="Add integrity and crossorigin attributes to external <script> tags.",
                reference="MDN — Subresource Integrity",
            ))
            break  # one finding is enough

    # WEB-OWASP-006 — Open redirect patterns
    open_redirect_patterns = [
        r'(?:redirect|returnTo|next|url|return_url|callback)\s*=\s*(?:req\.(?:query|params|body))',
        r'res\.redirect\s*\(\s*(?:req\.(?:query|params|body)\[)',
        r'window\.location\s*=\s*(?:new URLSearchParams|params\.get)',
        r'location\.href\s*=\s*(?:searchParams|urlParams|query)',
    ]
    redirect_matches = sum(len(re.findall(p, source)) for p in open_redirect_patterns)
    if redirect_matches > 0:
        findings.append(Finding(
            rule_id="WEB-OWASP-006",
            severity="WARN",
            message=f"Potential open redirect patterns found ({redirect_matches} instance(s))",
            fix="Validate redirect URLs against an allowlist of trusted domains before redirecting.",
            reference="OWASP A01:2021 — Broken Access Control",
        ))

    # WEB-OWASP-007 — Cookie set without Secure flag
    cookie_set_patterns = re.findall(
        r'(?:setCookie|set-cookie|document\.cookie\s*=)[^\n]*', source, re.IGNORECASE
    )
    for cookie_str in cookie_set_patterns:
        if not re.search(r'\bsecure\b', cookie_str, re.IGNORECASE):
            findings.append(Finding(
                rule_id="WEB-OWASP-007",
                severity="WARN",
                message="Cookie set without Secure flag — may be sent over HTTP",
                fix="Add the Secure flag to all cookies to ensure they are only sent over HTTPS.",
                reference="OWASP — Secure Cookie Attribute",
            ))
            break

    # WEB-OWASP-008 — Weak cryptographic hashing for passwords
    weak_hash_patterns = [
        r'createHash\s*\(\s*["\'](?:md5|sha1)["\']',
        r'\bmd5\s*\(', r'\bsha1\s*\(',
        r'hashlib\.(?:md5|sha1)\s*\(',
        r'MessageDigest\.getInstance\s*\(\s*["\'](?:MD5|SHA-1)["\']',
    ]
    weak_hash_count = sum(len(re.findall(p, source)) for p in weak_hash_patterns)
    if weak_hash_count > 0:
        findings.append(Finding(
            rule_id="WEB-OWASP-008",
            severity="WARN",
            message=f"Weak cryptographic hashing detected ({weak_hash_count} instance(s) of MD5/SHA-1)",
            fix="Use bcrypt, scrypt, or Argon2 for password hashing. Use SHA-256+ for general hashing.",
            reference="OWASP A02:2021 — Cryptographic Failures",
        ))

    return findings


def _run_audit(root: Path, manager: str) -> Optional[int]:
    """Run a package manager audit and return the number of vulnerabilities, or None."""
    cmd_map = {
        "npm": ["npm", "audit", "--json"],
        "yarn": ["yarn", "npm", "audit", "--json"],
        "pnpm": ["pnpm", "audit", "--json"],
        "bun": ["bun", "pm", "audit", "--json"],
    }
    cmd = cmd_map.get(manager)
    if not cmd:
        return None
    try:
        result = subprocess.run(
            cmd, cwd=root, capture_output=True, text=True, timeout=60,
        )
        # Try standard JSON first (npm, pnpm, bun)
        try:
            data = json.loads(result.stdout)
            if "metadata" in data:
                vuln_meta = data["metadata"].get("vulnerabilities", {})
                return sum(vuln_meta.values()) if isinstance(vuln_meta, dict) else 0
        except json.JSONDecodeError:
            pass
        # Yarn outputs NDJSON — count advisory lines
        if manager == "yarn":
            count = 0
            for line in result.stdout.splitlines():
                try:
                    entry = json.loads(line)
                    if entry.get("type") == "auditAdvisory":
                        count += 1
                except json.JSONDecodeError:
                    continue
            return count if count > 0 else None
    except (subprocess.TimeoutExpired, OSError, KeyError):
        pass
    return None


# ---------------------------------------------------------------------------
# Hardcoded Secrets (WEB-SEC-007)
# ---------------------------------------------------------------------------

_SECRET_PATTERNS = [
    (r'(?i)(?:api[_-]?key|apikey)\s*[:=]\s*["\'][A-Za-z0-9_\-]{16,}["\']', "API key"),
    (r'(?i)(?:secret|token|password|passwd)\s*[:=]\s*["\'][A-Za-z0-9_\-]{8,}["\']', "Secret/token"),
    (r'(?i)sk_(?:live|test)_[A-Za-z0-9]{24,}', "Stripe key"),
    (r'AIza[0-9A-Za-z_\-]{35}', "Google API key"),
    (r'(?i)aws[_-]?(?:access[_-]?key|secret)\s*[:=]\s*["\'][A-Za-z0-9/+=]{16,}["\']', "AWS credential"),
    (r'(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}', "GitHub token"),
    (r'github_pat_[A-Za-z0-9_]{20,}', "GitHub fine-grained token"),
]


def _check_secrets(source: str) -> List[Finding]:
    """Detect hardcoded secrets in source code."""
    findings: List[Finding] = []
    found_types: List[str] = []

    for pattern, label in _SECRET_PATTERNS:
        if re.search(pattern, source):
            found_types.append(label)

    if found_types:
        findings.append(Finding(
            rule_id="WEB-SEC-007",
            severity="FAIL",
            message=f"Hardcoded secrets detected in source code: {', '.join(found_types)}",
            fix="Move secrets to environment variables (.env) and ensure .env files are in .gitignore.",
            reference="OWASP A02:2021 — Cryptographic Failures",
        ))

    return findings


# ---------------------------------------------------------------------------
# Accessibility (WEB-A11Y-*)
# ---------------------------------------------------------------------------

def _check_accessibility(root: Path, content: str) -> List[Finding]:
    findings: List[Finding] = []

    # WEB-A11Y-001 — Images missing alt
    img_no_alt = re.findall(r'<(?:img|Image)\b(?![^>]*\balt\s*=)[^>]*/?\s*>', content)
    if img_no_alt:
        findings.append(Finding(
            rule_id="WEB-A11Y-001",
            severity="WARN",
            message=f"Images missing alt text ({len(img_no_alt)} instance(s))",
            fix="Add descriptive alt attributes to all <img> and <Image> elements.",
            reference="WCAG 2.1 — 1.1.1 Non-text Content",
        ))

    # WEB-A11Y-002 — Non-semantic click handlers
    click_patterns = [
        r'<(?:div|span)\b[^>]*\bonClick\b',       # React / JSX
        r'<(?:div|span)\b[^>]*\bonclick\b',        # HTML
        r'<(?:div|span)\b[^>]*\b@click\b',         # Vue
        r'<(?:div|span)\b[^>]*\bon:click\b',       # Svelte
        r'<(?:div|span)\b[^>]*\b\(click\)\s*=',    # Angular
    ]
    bad_click = sum(len(re.findall(p, content)) for p in click_patterns)
    if bad_click:
        findings.append(Finding(
            rule_id="WEB-A11Y-002",
            severity="WARN",
            message=f"Non-semantic elements with click handlers ({bad_click} instance(s))",
            fix="Use semantic elements (<button>, <a>) instead of <div>/<span> with click handlers.",
            reference="WCAG 2.2 — 2.1.1 Keyboard",
        ))

    # WEB-A11Y-003 — Missing focus visibility
    focus_outline_none = re.findall(r'outline\s*:\s*(?:none|0)\b', content)
    if focus_outline_none:
        findings.append(Finding(
            rule_id="WEB-A11Y-003",
            severity="WARN",
            message=f"Focus outline removed ({len(focus_outline_none)} instance(s) of outline: none)",
            fix="Provide visible focus indicators instead of removing outlines. Use :focus-visible for custom styles.",
            reference="WCAG 2.1 — 2.4.7 Focus Visible",
        ))

    # WEB-A11Y-004 — Form inputs missing labels
    inputs_no_label = re.findall(r'<input\b(?![^>]*(?:\baria-label\b|\baria-labelledby\b|\bid\s*=))[^>]*/?\s*>', content)
    if inputs_no_label:
        findings.append(Finding(
            rule_id="WEB-A11Y-004",
            severity="WARN",
            message=f"Form inputs potentially missing labels ({len(inputs_no_label)} instance(s))",
            fix="Associate each <input> with a <label> element or add aria-label attribute.",
            reference="WCAG 2.1 — 1.3.1 Info and Relationships",
        ))

    # WEB-A11Y-005 — Missing lang attribute
    has_html_lang = bool(re.search(r'<html[^>]*\blang\s*=', content))
    has_html_tag = bool(re.search(r'<html\b', content))
    if has_html_tag and not has_html_lang:
        findings.append(Finding(
            rule_id="WEB-A11Y-005",
            severity="WARN",
            message="HTML document missing lang attribute",
            fix='Add lang attribute to <html> element (e.g. <html lang="en">).',
            reference="WCAG 2.1 — 3.1.1 Language of Page",
        ))

    return findings


# ---------------------------------------------------------------------------
# Privacy (WEB-PRIV-*)
# ---------------------------------------------------------------------------

_TRACKING_PATTERNS = [
    r"google-analytics", r"googletagmanager", r"\bgtag\b",
    r"fbevents", r"facebook.*pixel", r"hotjar", r"mixpanel", r"segment",
]
_CONSENT_PATTERNS = [
    r"cookie-consent", r"cookieConsent", r"CookieBanner", r"cookie-banner",
    r"\bgdpr\b", r"OneTrust", r"cookiebot",
]


def _check_privacy(root: Path, content: str, config: str) -> List[Finding]:
    findings: List[Finding] = []

    has_tracking = any(re.search(p, content, re.IGNORECASE) for p in _TRACKING_PATTERNS)
    has_consent = any(re.search(p, content, re.IGNORECASE) for p in _CONSENT_PATTERNS)

    if has_tracking and not has_consent:
        findings.append(Finding(
            rule_id="WEB-PRIV-001",
            severity="WARN",
            message="Tracking scripts detected but no cookie consent mechanism found",
            fix="Implement a cookie consent banner/manager (required by GDPR/CCPA).",
            reference="GDPR Article 7 — Conditions for consent",
        ))

    # WEB-PRIV-002 — Privacy policy page
    privacy_files = (
        list(root.rglob("privacy*"))
        + list(root.rglob("privacy-policy*"))
    )
    privacy_files = [f for f in privacy_files if "node_modules" not in str(f)]
    privacy_ref = bool(re.search(r'/privacy|privacy[.\-]policy', content, re.IGNORECASE))
    if not privacy_files and not privacy_ref:
        findings.append(Finding(
            rule_id="WEB-PRIV-002",
            severity="FAIL",
            message="No privacy policy page or reference found",
            fix="Add a privacy policy page to your site (required if collecting any user data).",
            reference="GDPR / CCPA / CalOPPA requirements",
        ))

    # WEB-PRIV-003 — Cookie consent recording
    if has_consent:
        consent_record_patterns = [
            r"consent.*log|logConsent|recordConsent|consentRecord",
            r"localStorage.*consent|sessionStorage.*consent",
            r"setCookie.*consent",
        ]
        records_consent = any(re.search(p, content, re.IGNORECASE) for p in consent_record_patterns)
        if not records_consent:
            findings.append(Finding(
                rule_id="WEB-PRIV-003",
                severity="INFO",
                message="Cookie consent mechanism found but no consent recording/logging detected",
                fix="Record and store user consent choices for audit trail compliance.",
                reference="GDPR Article 7(1) — Demonstrating Consent",
            ))

    # WEB-PRIV-004 — CCPA / Do Not Sell link
    if has_tracking:
        ccpa_patterns = [r"do.not.sell", r"doNotSell", r"ccpa", r"opt.out", r"optOut"]
        has_ccpa = any(re.search(p, content, re.IGNORECASE) for p in ccpa_patterns)
        if not has_ccpa:
            findings.append(Finding(
                rule_id="WEB-PRIV-004",
                severity="INFO",
                message="No 'Do Not Sell' or CCPA opt-out mechanism found",
                fix="Add a 'Do Not Sell My Personal Information' link if serving California users.",
                reference="CCPA / CPRA Requirements",
            ))

    # WEB-PRIV-005 — Terms of Service page
    tos_files = list(root.rglob("terms*"))
    tos_files = [f for f in tos_files if "node_modules" not in str(f)]
    tos_ref = bool(re.search(r'/terms|terms[.\-]of[.\-]service|terms-and-conditions', content, re.IGNORECASE))
    if not tos_files and not tos_ref:
        findings.append(Finding(
            rule_id="WEB-PRIV-005",
            severity="INFO",
            message="No Terms of Service page or reference found",
            fix="Add a Terms of Service page to your site.",
            reference="Legal best practices for web applications",
        ))

    return findings
