"""
GUI/Editor integrity checks (24-30).

Checks:
  24. route_coverage — fetch() URLs in HTML/JS that don't match any serve.py route
  25. onclick_defined — onclick handlers in HTML referencing undefined JS functions
  26. editor_module_deps — prepro-console.html missing a <script> import for a module file
  27. standalone_api_compat — Standalone editors using inconsistent API path patterns
  28. launch_cleanup — launch.sh not cleaning up backgrounded server PID on exit
  29. server_timeout — voice_casting_server.py missing timeout on TTS generation
  30. secrets_in_html — API keys/tokens hardcoded in HTML/JS files
"""

import os
import re

from . import register_check, register_section


def check_route_coverage(base, discovered):
    """Verify fetch() URLs in HTML/JS files match serve.py routes."""
    results = {"pass": [], "fail": [], "warn": []}

    # Read serve.py to extract route patterns
    serve_path = os.path.join(base, "editors", "serve.py")
    if not os.path.exists(serve_path):
        results["warn"].append("serve.py not found — cannot verify routes")
        return results

    with open(serve_path) as f:
        serve_content = f.read()

    # Extract API route patterns from serve.py
    # Matches: path == "/api/projects", path.startswith("/api/project/")
    route_patterns = set()
    for match in re.finditer(r'path\s*==\s*["\']([^"\']+)["\']', serve_content):
        route_patterns.add(match.group(1))
    for match in re.finditer(r'path\.startswith\(\s*["\']([^"\']+)["\']', serve_content):
        route_patterns.add(match.group(1))
    # Also include /files/ which is a file-serving catch-all
    route_patterns.add("/files/")

    # Scan HTML/JS files for fetch() calls
    editor_dir = os.path.join(base, "editors")
    fetch_pattern = re.compile(r'fetch\s*\(\s*[`"\']([^`"\'$]+)')

    html_js_files = []
    for rel in sorted(discovered):
        if rel.startswith("editors/") and (rel.endswith(".html") or rel.endswith(".js")):
            html_js_files.append(rel)

    unmatched = []
    for rel in html_js_files:
        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        for match in fetch_pattern.finditer(content):
            url = match.group(1)
            # Skip template literals with ${}, external URLs, data URIs
            if "${" in url or url.startswith("http") or url.startswith("data:"):
                continue
            # Normalize: strip query params
            url_path = url.split("?")[0].split("#")[0]

            # Check if URL matches any known route
            matched = False
            for route in route_patterns:
                if url_path == route or url_path.startswith(route):
                    matched = True
                    break

            if not matched:
                unmatched.append((rel, url_path))

    if unmatched:
        for rel, url in unmatched:
            results["warn"].append(
                f"{os.path.basename(rel)}: fetch('{url}') — "
                f"no matching serve.py route"
            )
    else:
        results["pass"].append("All fetch() URLs match serve.py routes")

    return results


def check_onclick_defined(base, discovered):
    """Verify onclick handlers in HTML reference defined JS functions."""
    results = {"pass": [], "fail": [], "warn": []}

    onclick_pattern = re.compile(r'onclick\s*=\s*["\'](\w+)\s*\(')
    func_def_pattern = re.compile(r'function\s+(\w+)\s*\(')
    arrow_pattern = re.compile(r'(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(')

    html_files = [
        rel for rel in discovered
        if rel.startswith("editors/") and rel.endswith(".html")
    ]

    for rel in html_files:
        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        basename = os.path.basename(rel)

        # Collect all defined functions (including in <script> tags and linked modules)
        defined_funcs = set()
        for match in func_def_pattern.finditer(content):
            defined_funcs.add(match.group(1))
        for match in arrow_pattern.finditer(content):
            defined_funcs.add(match.group(1))

        # Also check linked JS modules
        script_src_pattern = re.compile(r'<script\s+[^>]*src\s*=\s*["\']([^"\']+)["\']')
        for src_match in script_src_pattern.finditer(content):
            src = src_match.group(1)
            # Resolve relative to editors dir
            if not src.startswith("http"):
                js_path = os.path.join(os.path.dirname(full), src)
                if os.path.exists(js_path):
                    try:
                        with open(js_path) as jf:
                            js_content = jf.read()
                        for match in func_def_pattern.finditer(js_content):
                            defined_funcs.add(match.group(1))
                        for match in arrow_pattern.finditer(js_content):
                            defined_funcs.add(match.group(1))
                    except (IOError, OSError):
                        pass

        # Check onclick handlers
        onclick_funcs = set()
        for match in onclick_pattern.finditer(content):
            onclick_funcs.add(match.group(1))

        undefined = onclick_funcs - defined_funcs
        if undefined:
            for func_name in sorted(undefined):
                results["warn"].append(
                    f"{basename}: onclick='{func_name}()' — "
                    f"function not found in file or linked scripts"
                )
        else:
            if onclick_funcs:
                results["pass"].append(
                    f"{basename}: all {len(onclick_funcs)} onclick handlers defined"
                )

    return results


def check_editor_module_deps(base, discovered):
    """Verify prepro-console.html imports all required module scripts."""
    results = {"pass": [], "fail": [], "warn": []}

    console_path = os.path.join(base, "editors", "prepro-console.html")
    modules_dir = os.path.join(base, "editors", "modules")

    if not os.path.exists(console_path):
        results["warn"].append("prepro-console.html not found")
        return results

    with open(console_path) as f:
        console_content = f.read()

    if not os.path.isdir(modules_dir):
        results["warn"].append("modules/ directory not found")
        return results

    # Find all .js files in modules/
    module_files = sorted(
        f for f in os.listdir(modules_dir) if f.endswith(".js")
    )

    # Find all <script src="modules/..."> in console HTML
    imported_modules = set()
    for match in re.finditer(r'<script\s+[^>]*src\s*=\s*["\']modules/([^"\']+)["\']', console_content):
        imported_modules.add(match.group(1))

    missing = []
    for module_file in module_files:
        if module_file not in imported_modules:
            missing.append(module_file)

    if missing:
        for m in missing:
            results["warn"].append(
                f"prepro-console.html: modules/{m} exists "
                f"but not imported via <script> tag"
            )
    else:
        results["pass"].append(
            f"prepro-console.html: all {len(module_files)} modules imported"
        )

    return results


def check_standalone_api_compat(base, discovered):
    """Verify standalone editors use consistent API path patterns."""
    results = {"pass": [], "fail": [], "warn": []}

    standalone_dir = os.path.join(base, "editors", "_standalone")
    if not os.path.isdir(standalone_dir):
        results["warn"].append("_standalone/ directory not found")
        return results

    # The main serve.py uses /api/project/<name>/... pattern
    EXPECTED_API_PREFIX = "/api/project/"
    # Some standalone editors might use old patterns like /api/<name>/ or relative paths
    OLD_API_PATTERNS = [
        re.compile(r'fetch\s*\(\s*["`\'](?:\.\.)?/api/(?!project/)[^/]+/'),
        re.compile(r'fetch\s*\(\s*["`\']http://127\.0\.0\.1:\d+/(?!api/)'),
    ]

    standalone_files = [
        f for f in os.listdir(standalone_dir)
        if f.endswith(".html")
    ]

    for filename in sorted(standalone_files):
        full = os.path.join(standalone_dir, filename)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        issues = []
        for pattern in OLD_API_PATTERNS:
            matches = pattern.findall(content)
            if matches:
                issues.append(f"uses old API pattern: {matches[0][:60]}...")

        if issues:
            for issue in issues:
                results["warn"].append(f"{filename}: {issue}")
        else:
            # Check if it uses the standard API at all
            if EXPECTED_API_PREFIX in content or "fetch" not in content:
                results["pass"].append(f"{filename}: API patterns consistent")

    return results


def check_launch_cleanup(base, _discovered):
    """Verify launch.sh cleans up backgrounded server PID on exit."""
    results = {"pass": [], "fail": [], "warn": []}

    launch_path = os.path.join(base, "editors", "launch.sh")
    if not os.path.exists(launch_path):
        results["warn"].append("launch.sh not found")
        return results

    with open(launch_path) as f:
        content = f.read()

    has_trap = "trap" in content
    has_kill = "kill" in content

    if has_trap and has_kill:
        results["pass"].append("launch.sh: has trap for PID cleanup")
    elif "SERVER_PID" in content and not has_trap:
        results["fail"].append(
            "launch.sh: backgrounds server (SERVER_PID) but has no "
            "trap to clean up on exit — orphan processes will accumulate"
        )
    else:
        results["pass"].append("launch.sh: no backgrounded process concerns")

    return results


def check_server_timeout(base, _discovered):
    """Verify voice_casting_server.py has timeout on TTS generation."""
    results = {"pass": [], "fail": [], "warn": []}

    server_path = os.path.join(base, "editors", "voice_casting_server.py")
    if not os.path.exists(server_path):
        results["warn"].append("voice_casting_server.py not found")
        return results

    with open(server_path) as f:
        content = f.read()

    # Check for timeout in the generate/preview handler
    has_timeout = (
        "timeout" in content.lower() or
        "signal.alarm" in content or
        "threading.Timer" in content or
        "asyncio.wait_for" in content
    )

    if has_timeout:
        results["pass"].append("voice_casting_server.py: TTS generation has timeout")
    else:
        results["warn"].append(
            "voice_casting_server.py: no timeout on TTS generation — "
            "slow/hung model could block the server indefinitely"
        )

    return results


def check_secrets_in_html(base, discovered):
    """Scan HTML/JS files specifically for hardcoded API keys/tokens."""
    results = {"pass": [], "fail": [], "warn": []}

    KEY_PATTERNS = [
        ("ElevenLabs", re.compile(r'sk_[a-f0-9]{40,}')),
        ("Google AI", re.compile(r'AIza[A-Za-z0-9_-]{35}')),
        ("OpenAI", re.compile(r'sk-[A-Za-z0-9]{20,}')),
    ]

    # Scan HTML/JS in editors
    editor_files = [
        rel for rel in discovered
        if rel.startswith("editors/") and
        (rel.endswith(".html") or rel.endswith(".js"))
    ]

    findings = []
    for rel in editor_files:
        full = os.path.join(base, rel)
        try:
            with open(full) as f:
                content = f.read()
        except (IOError, OSError):
            continue

        for provider, pattern in KEY_PATTERNS:
            for match in pattern.finditer(content):
                pos = match.start()
                line_num = content[:pos].count('\n') + 1
                findings.append((rel, line_num, provider))

    if findings:
        for rel, line_num, provider in findings:
            results["fail"].append(
                f"SECURITY: {rel}:{line_num} — hardcoded {provider} API key"
            )
    else:
        results["pass"].append("No API keys found in HTML/JS editor files")

    return results


# ═══════════════════════════════════════════════════════════════
# REGISTRATION
# ═══════════════════════════════════════════════════════════════

register_check("route_coverage", "Route Coverage", check_route_coverage, "gui")
register_check("onclick_defined", "Onclick Handlers Defined", check_onclick_defined, "gui")
register_check("editor_module_deps", "Editor Module Dependencies", check_editor_module_deps, "gui")
register_check("standalone_api_compat", "Standalone API Compatibility", check_standalone_api_compat, "gui")
register_check("launch_cleanup", "Launch Script Cleanup", check_launch_cleanup, "gui")
register_check("server_timeout", "Server Timeout", check_server_timeout, "gui")
register_check("secrets_in_html", "Secrets in HTML", check_secrets_in_html, "gui")

register_section("gui", [
    "route_coverage", "onclick_defined", "editor_module_deps",
    "standalone_api_compat", "launch_cleanup", "server_timeout", "secrets_in_html",
])
