"""Diagnostic endpoints for testing VS Code, LLM proxy, and agent functionality.

Covers:
  - VS Code Web reachability + login
  - LLM proxy health + models
  - Roo Code config in settings.json
  - Extensions installed (via docker exec)
  - Provider API keys configured
  - GitHub Copilot Chat conflict detection
  - Agent end-to-end completion test
"""

from __future__ import annotations

import asyncio
import json
import shutil
import subprocess
import structlog
from pathlib import Path
from typing import Any

import httpx
from fastapi import APIRouter, HTTPException, Request

from proxym.config import get_settings

logger = structlog.get_logger()

router = APIRouter(prefix="/tests", tags=["diagnostics"])

# Internal port where uvicorn listens inside container (always 4000)
_INTERNAL_PORT = 4000

# Extensions that MUST be installed for full functionality
_REQUIRED_EXTENSIONS = [
    "rooveterinary.roo-cline",
    "continue.continue",
    "ms-python.python",
    "charliermarsh.ruff",
]

# Extensions available from Open VSX (code-server marketplace)
_OPEN_VSX_EXTENSIONS = [
    "continue.continue",
    "ms-python.python",
    "charliermarsh.ruff",
    "eamodio.gitlens",
    "mhutchie.git-graph",
    "humao.rest-client",
    "redhat.vscode-yaml",
    "tamasfe.even-better-toml",
    "mikestead.dotenv",
    "usernamehw.errorlens",
    "pkief.material-icon-theme",
]

# Extensions NOT in Open VSX — need manual .vsix install
_VSIX_ONLY = [
    "rooveterinary.roo-cline",
]

# Extensions that conflict with proxym setup
_CONFLICTING_EXTENSIONS = [
    "github.copilot",
    "github.copilot-chat",
]


async def _probe_first_ok(urls: list[str]) -> tuple[str | None, httpx.Response | None, str | None]:
    async with httpx.AsyncClient(timeout=5.0) as client:
        last_err: str | None = None
        for url in urls:
            try:
                resp = await client.get(url, follow_redirects=True)
                if resp.status_code < 500:
                    return url, resp, None
            except Exception as e:
                last_err = str(e)
        return None, None, last_err


async def _check_vscode_status() -> dict[str, Any]:
    """Check if VS Code Web is running and accessible."""
    settings = get_settings()
    internal_candidates = [
        f"http://proxym-vscode:{settings.vscode_port}",
        f"http://vscode:{settings.vscode_port}",
    ]
    public_candidates = [
        f"http://localhost:{settings.vscode_port}",
        f"http://127.0.0.1:{settings.vscode_port}",
    ]
    vscode_url = internal_candidates[0]
    public_url = public_candidates[0]
    
    result = {
        "service": "vscode",
        "url": public_url,
        "internal_url": vscode_url,
        "reachable": False,
        "password_configured": bool(settings.vscode_password),
        "password": settings.vscode_password if settings.vscode_password else None,
        "details": {},
    }
    
    url, response, err = await _probe_first_ok(public_candidates + internal_candidates)
    if response is not None and url is not None:
        result["reachable"] = True
        result["url"] = url
        result["details"]["status_code"] = response.status_code
        result["details"]["has_login_form"] = (
            "password" in response.text.lower() or response.status_code in (401, 403)
        )
    else:
        result["details"]["error"] = err or "All connection attempts failed"
    
    return result


async def _check_llm_proxy(base_url: str) -> dict[str, Any]:
    """Check LLM proxy connectivity and available models."""
    settings = get_settings()
    api_key = settings.master_key

    # Try multiple URLs: the passed base_url, then internal loopback
    candidates = [
        base_url.rstrip("/"),
        f"http://127.0.0.1:{_INTERNAL_PORT}",
        f"http://localhost:{_INTERNAL_PORT}",
    ]
    # Deduplicate while preserving order
    seen = set()
    urls = []
    for u in candidates:
        if u not in seen:
            seen.add(u)
            urls.append(u)

    result = {
        "service": "llm_proxy",
        "url": urls[0],
        "internal_url": urls[0],
        "reachable": False,
        "models_available": 0,
        "details": {},
    }

    for proxy_url in urls:
        try:
            async with httpx.AsyncClient(timeout=5.0) as client:
                health_resp = await client.get(f"{proxy_url}/health")
                if health_resp.status_code == 200:
                    result["reachable"] = True
                    result["url"] = proxy_url
                    result["internal_url"] = proxy_url
                    result["details"]["health"] = health_resp.json()

                    headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
                    models_resp = await client.get(f"{proxy_url}/v1/models", headers=headers)
                    if models_resp.status_code == 200:
                        models_data = models_resp.json()
                        result["models_available"] = len(models_data.get("data", []))
                        result["details"]["aliases"] = [
                            m for m in models_data.get("data", []) if m.get("is_alias")
                        ]
                    break
        except Exception as e:
            result["details"]["error"] = str(e)

    return result


async def _check_roo_cline_config() -> dict[str, Any]:
    """Check if Roo Code is configured in VS Code settings."""
    settings = get_settings()
    vscode_user_dir = Path.home() / ".local" / "share" / "code-server" / "User"
    settings_file = vscode_user_dir / "settings.json"
    
    result = {
        "service": "roo_cline",
        "settings_file_exists": False,
        "configured": False,
        "api_provider": None,
        "base_url": None,
        "details": {},
    }
    
    data: dict[str, Any] | None = None

    if settings_file.exists():
        result["settings_file_exists"] = True
        try:
            data = json.loads(settings_file.read_text())
        except Exception as e:
            result["details"]["error"] = str(e)

    if data is None:
        docker_bin = shutil.which("docker")
        if docker_bin:
            try:
                proc = await asyncio.to_thread(
                    subprocess.run,
                    [
                        "docker",
                        "exec",
                        "proxym-vscode",
                        "cat",
                        "/home/coder/.local/share/code-server/User/settings.json",
                    ],
                    capture_output=True,
                    text=True,
                    timeout=10,
                )
                if proc.returncode == 0 and proc.stdout.strip():
                    result["settings_file_exists"] = True
                    data = json.loads(proc.stdout)
                elif proc.returncode != 0:
                    result["details"]["docker_error"] = proc.stderr.strip()
            except Exception as e:
                result["details"]["docker_error"] = str(e)

    if data is not None:
        result["api_provider"] = data.get("roo-cline.apiProvider")
        result["base_url"] = data.get("roo-cline.openAiBaseUrl")
        result["configured"] = (
            result["api_provider"] == "openai" and bool(result["base_url"]) and bool(data.get("roo-cline.openAiApiKey"))
        )
        result["details"]["model_id"] = data.get("roo-cline.openAiModelId")
        result["details"]["custom_modes_count"] = len(data.get("roo-cline.customModes", []))
        result["details"]["has_api_key"] = bool(data.get("roo-cline.openAiApiKey"))
        modes = data.get("roo-cline.modeApiConfigs", {})
        result["details"]["mode_configs"] = list(modes.keys()) if modes else []
        mcp = data.get("roo-cline.mcpServers", {})
        result["details"]["mcp_servers"] = list(mcp.keys()) if mcp else []
        result["details"]["workspace_trust_disabled"] = data.get("security.workspace.trust.enabled") is False
        result["details"]["color_theme"] = data.get("workbench.colorTheme")
    
    return result


async def _check_extensions() -> dict[str, Any]:
    """Check installed VS Code extensions via docker exec."""
    result = {
        "service": "extensions",
        "installed": [],
        "missing_required": [],
        "missing_vsix": [],
        "conflicting": [],
        "total_installed": 0,
        "details": {},
    }

    docker_bin = shutil.which("docker")
    if not docker_bin:
        result["details"]["error"] = "docker not found on PATH"
        return result

    try:
        proc = await asyncio.to_thread(
            subprocess.run,
            ["docker", "exec", "proxym-vscode", "code-server", "--list-extensions"],
            capture_output=True, text=True, timeout=15,
        )
        if proc.returncode != 0:
            result["details"]["error"] = f"docker exec failed: {proc.stderr.strip()}"
            return result

        installed = [e.strip().lower() for e in proc.stdout.strip().splitlines() if e.strip()]
        result["installed"] = installed
        result["total_installed"] = len(installed)

        # Check required (Open VSX)
        for ext in _OPEN_VSX_EXTENSIONS:
            if ext.lower() not in installed:
                result["missing_required"].append(ext)

        # Check VSIX-only (expected to be missing unless manually installed)
        for ext in _VSIX_ONLY:
            if ext.lower() not in installed:
                result["missing_vsix"].append(ext)

        # Check conflicting
        for ext in _CONFLICTING_EXTENSIONS:
            if ext.lower() in installed:
                result["conflicting"].append(ext)

    except subprocess.TimeoutExpired:
        result["details"]["error"] = "docker exec timed out"
    except FileNotFoundError:
        result["details"]["error"] = "docker not available"
    except Exception as e:
        result["details"]["error"] = str(e)

    return result


async def _check_providers_config() -> dict[str, Any]:
    """Check which LLM providers are configured with API keys."""
    settings = get_settings()
    providers = settings.available_providers()

    provider_details = []
    for name, key in providers.items():
        provider_details.append({
            "name": name,
            "has_key": bool(key) and key != "ollama",
            "is_local": name == "ollama",
            "key_prefix": key[:8] + "..." if key and key != "ollama" and len(key) > 8 else None,
        })

    # Check model aliases resolve to available providers.
    # Use the MODELS registry for accurate provider lookup instead of
    # fragile string parsing (e.g. "claude-haiku-4-5" has no "/" prefix).
    from proxym.providers import MODELS, get_model
    alias_status = []
    for alias, model_name in settings.model_aliases.items():
        # Try MODELS registry first for accurate provider
        spec = get_model(model_name)
        if spec is None:
            # Try partial match (e.g. alias resolves to short name)
            for mid, m in MODELS.items():
                if model_name.lower() in mid.lower() or model_name.lower() in m.display_name.lower():
                    spec = m
                    break
        if spec:
            provider = spec.provider
        else:
            # Fallback: infer from model name prefix
            provider = model_name.split("/")[0] if "/" in model_name else "anthropic"
            if provider == "gemini":
                provider = "google"
        available = provider in providers
        alias_status.append({
            "alias": alias,
            "model": model_name,
            "provider": provider,
            "available": available,
        })

    return {
        "service": "providers",
        "total_providers": len(providers),
        "providers": provider_details,
        "aliases": alias_status,
        "all_aliases_available": all(a["available"] for a in alias_status),
        "details": {},
    }


async def _check_copilot_conflict() -> dict[str, Any]:
    """Detect GitHub Copilot / Copilot Chat extension conflicts.

    GitHub Copilot Chat requires a GitHub account and subscription.
    It is NOT available in code-server (Open VSX) and causes:
      'The extension GitHub.copilot-chat cannot be installed because it was not found.'
    """
    result = {
        "service": "copilot_conflict",
        "has_conflict": False,
        "conflicting_extensions": [],
        "recommendation": None,
        "details": {},
    }

    # Check via docker exec
    docker_bin = shutil.which("docker")
    if not docker_bin:
        result["details"]["skipped"] = "docker not on PATH"
        return result

    try:
        proc = await asyncio.to_thread(
            subprocess.run,
            ["docker", "exec", "proxym-vscode", "code-server", "--list-extensions"],
            capture_output=True, text=True, timeout=15,
        )
        if proc.returncode == 0:
            installed = [e.strip().lower() for e in proc.stdout.strip().splitlines()]
            for ext in _CONFLICTING_EXTENSIONS:
                if ext.lower() in installed:
                    result["has_conflict"] = True
                    result["conflicting_extensions"].append(ext)

        # Also check if settings.json references copilot
        settings_proc = await asyncio.to_thread(
            subprocess.run,
            ["docker", "exec", "proxym-vscode",
             "cat", "/home/coder/.local/share/code-server/User/settings.json"],
            capture_output=True, text=True, timeout=10,
        )
        if settings_proc.returncode == 0:
            settings_text = settings_proc.stdout.lower()
            if "copilot" in settings_text:
                result["details"]["copilot_in_settings"] = True
                if not result["has_conflict"]:
                    result["has_conflict"] = True
                    result["details"]["reason"] = "copilot referenced in settings.json"

    except Exception as e:
        result["details"]["error"] = str(e)

    if result["has_conflict"]:
        result["recommendation"] = (
            "Uninstall GitHub Copilot extensions. They are not available in code-server "
            "(Open VSX) and conflict with Roo Code. Use Roo Code or Continue.dev instead. "
            "Error: 'The extension GitHub.copilot-chat cannot be installed because it was not found.'"
        )
    else:
        result["recommendation"] = "No Copilot conflicts detected. Roo Code is the primary AI assistant."

    return result


async def _check_agent_setup(base_url: str) -> dict[str, Any]:
    """Comprehensive agent readiness check — config + connectivity + extension."""
    result = {
        "service": "agent_setup",
        "ready": False,
        "checks": [],
        "details": {},
    }

    checks = []

    # 1. Check settings.json exists and has Roo Code config
    roo_config = await _check_roo_cline_config()
    checks.append({
        "name": "settings.json exists",
        "pass": roo_config["settings_file_exists"],
        "detail": "Roo Code settings file found" if roo_config["settings_file_exists"]
                  else "settings.json not found — run entrypoint.sh or restart VS Code container",
    })
    checks.append({
        "name": "Roo Code API configured",
        "pass": roo_config["configured"],
        "detail": f"Provider: {roo_config['api_provider']}, URL: {roo_config['base_url']}"
                  if roo_config["configured"]
                  else "Roo Code not configured — check roo-cline.apiProvider and roo-cline.openAiBaseUrl",
    })
    checks.append({
        "name": "API key set",
        "pass": roo_config.get("details", {}).get("has_api_key", False),
        "detail": "API key present" if roo_config.get("details", {}).get("has_api_key")
                  else "No API key — set PROXYM_API_KEY in .env",
    })
    checks.append({
        "name": "Mode configs present",
        "pass": len(roo_config.get("details", {}).get("mode_configs", [])) >= 4,
        "detail": f"Modes: {', '.join(roo_config.get('details', {}).get('mode_configs', []))}"
                  if roo_config.get("details", {}).get("mode_configs")
                  else "Missing mode configs (code, architect, debug, ask)",
    })

    # 2. Check proxy reachable
    proxy_status = await _check_llm_proxy(base_url=base_url)
    checks.append({
        "name": "LLM proxy reachable",
        "pass": proxy_status["reachable"],
        "detail": f"{proxy_status['models_available']} models available"
                  if proxy_status["reachable"]
                  else f"Cannot reach proxy: {proxy_status.get('details', {}).get('error', 'unknown')}",
    })

    # 3. Check at least one provider has a key
    providers = await _check_providers_config()
    has_provider = any(p["has_key"] or p["is_local"] for p in providers["providers"])
    checks.append({
        "name": "LLM provider available",
        "pass": has_provider,
        "detail": f"{providers['total_providers']} providers configured"
                  if has_provider
                  else "No LLM providers configured — add API keys to .env",
    })

    # 4. Check no Copilot conflict
    copilot = await _check_copilot_conflict()
    checks.append({
        "name": "No Copilot conflicts",
        "pass": not copilot["has_conflict"],
        "detail": copilot["recommendation"],
    })

    result["checks"] = checks
    result["ready"] = all(c["pass"] for c in checks)
    result["details"]["total_checks"] = len(checks)
    result["details"]["passed"] = sum(1 for c in checks if c["pass"])
    result["details"]["failed"] = sum(1 for c in checks if not c["pass"])

    return result


async def _test_agent_completion() -> dict[str, Any]:
    """Test a simple completion through the proxy to verify agent functionality."""
    settings = get_settings()
    # Use localhost since we're inside the proxy container
    proxy_url = f"http://127.0.0.1:{_INTERNAL_PORT}"
    api_key = settings.master_key
    
    result = {
        "service": "agent_test",
        "test_type": "simple_completion",
        "success": False,
        "response_time_ms": 0,
        "details": {},
    }
    
    if not settings.available_providers():
        result["details"]["skipped"] = "No LLM providers configured"
        return result
    
    try:
        import time
        start = time.time()
        
        async with httpx.AsyncClient(timeout=30.0) as client:
            headers = {
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
            }
            
            # Use a simple/cheap model for testing
            test_model = "cheap"  # alias for Haiku or similar
            
            payload = {
                "model": test_model,
                "messages": [
                    {"role": "system", "content": "You are a helpful assistant. Reply with only: OK"},
                    {"role": "user", "content": "Say OK"}
                ],
                "max_tokens": 10,
            }
            
            response = await client.post(
                f"{proxy_url}/v1/chat/completions",
                headers=headers,
                json=payload
            )
            
            result["response_time_ms"] = round((time.time() - start) * 1000, 2)
            
            if response.status_code == 200:
                data = response.json()
                content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
                result["success"] = "OK" in content.upper() or len(content) > 0
                result["details"]["response"] = content[:100]
                result["details"]["model_used"] = data.get("model")
            else:
                result["details"]["error"] = f"HTTP {response.status_code}: {response.text[:200]}"
                
    except Exception as e:
        result["details"]["error"] = str(e)
    
    return result


@router.get("/status")
async def run_all_tests(request: Request):
    """Run all diagnostic tests and return comprehensive status."""
    test_names = [
        "vscode", "llm_proxy", "roo_cline",
        "extensions", "providers", "copilot_conflict",
        "agent_setup", "agent_test",
    ]
    tests = [
        _check_vscode_status(),
        _check_llm_proxy(base_url=str(request.base_url).rstrip("/")),
        _check_roo_cline_config(),
        _check_extensions(),
        _check_providers_config(),
        _check_copilot_conflict(),
        _check_agent_setup(base_url=str(request.base_url).rstrip("/")),
        _test_agent_completion(),
    ]

    results = await asyncio.gather(*tests, return_exceptions=True)

    processed_results: list[dict[str, Any]] = []
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            processed_results.append({
                "service": test_names[i],
                "error": str(result),
                "status": "error",
            })
        else:
            processed_results.append(result)

    # Determine pass/fail per test
    def _is_passing(r: dict) -> bool:
        if r.get("status") == "error":
            return False
        return bool(
            r.get(
                "reachable",
                r.get(
                    "configured",
                    r.get(
                        "success",
                        r.get(
                            "ready",
                            r.get(
                                "all_aliases_available",
                                not r.get("has_conflict", True),
                            ),
                        ),
                    ),
                ),
            )
        )

    def _as_issues(r: dict) -> list[str]:
        issues: list[str] = []

        if r.get("status") == "error" and r.get("error"):
            issues.append(str(r.get("error")))
            return issues

        details = r.get("details") or {}
        if isinstance(details, dict):
            if details.get("error"):
                issues.append(str(details["error"]))
            if details.get("docker_error"):
                issues.append(str(details["docker_error"]))
            if details.get("skipped"):
                issues.append(f"skipped: {details['skipped']}")

        if r.get("service") == "extensions":
            for e in r.get("missing_required") or []:
                issues.append(f"missing_required: {e}")
            for e in r.get("missing_vsix") or []:
                issues.append(f"missing_vsix: {e}")
            for e in r.get("conflicting") or []:
                issues.append(f"conflicting: {e}")

        if r.get("service") == "providers":
            for a in r.get("aliases") or []:
                if not a.get("available"):
                    issues.append(f"alias '{a.get('alias')}' provider '{a.get('provider')}' not configured")

        if r.get("service") == "copilot_conflict" and r.get("has_conflict"):
            for e in r.get("conflicting_extensions") or []:
                issues.append(f"conflicting_extension: {e}")
            if r.get("recommendation"):
                issues.append(str(r.get("recommendation")))

        if r.get("service") == "agent_setup":
            for c in r.get("checks") or []:
                if not c.get("pass"):
                    issues.append(f"{c.get('name')}: {c.get('detail')}")

        if r.get("service") == "vscode" and not r.get("reachable"):
            err = (details.get("error") if isinstance(details, dict) else None) or "VS Code not reachable"
            issues.append(str(err))

        if r.get("service") == "llm_proxy" and not r.get("reachable"):
            err = (details.get("error") if isinstance(details, dict) else None) or "Proxy not reachable"
            issues.append(str(err))

        if r.get("service") == "roo_cline" and not r.get("configured"):
            if isinstance(details, dict) and details.get("error"):
                issues.append(str(details.get("error")))
            else:
                issues.append("Roo Code not configured")

        if r.get("service") == "agent_test":
            skipped = (details.get("skipped") if isinstance(details, dict) else None)
            if skipped:
                issues.append(str(skipped))
            if isinstance(details, dict) and details.get("error"):
                issues.append(str(details.get("error")))

        return issues

    # Normalize for frontend summary/copy: ensure passed/message/issues exist.
    normalized: list[dict[str, Any]] = []
    for r in processed_results:
        passed_test = _is_passing(r)
        rr = dict(r)
        rr["passed"] = passed_test
        rr.setdefault("message", "OK" if passed_test else "Problem")
        rr["issues"] = _as_issues(rr) if not passed_test else []
        normalized.append(rr)

    processed_results = normalized

    passed = sum(1 for r in processed_results if r.get("passed"))
    failed = len(processed_results) - passed

    return {
        "overall_status": "healthy" if failed == 0 else "degraded",
        "passed": passed,
        "failed": failed,
        "total": len(processed_results),
        "tests": processed_results,
    }


@router.get("/vscode")
async def test_vscode():
    """Test VS Code Web connectivity."""
    return await _check_vscode_status()


@router.get("/proxy")
async def test_proxy(request: Request):
    """Test LLM proxy connectivity."""
    return await _check_llm_proxy(base_url=str(request.base_url).rstrip("/"))


@router.get("/roo")
async def test_roo():
    """Test Roo Code configuration."""
    return await _check_roo_cline_config()


@router.get("/extensions")
async def test_extensions():
    """Check installed VS Code extensions."""
    return await _check_extensions()


@router.get("/providers")
async def test_providers():
    """Check configured LLM providers and model aliases."""
    return await _check_providers_config()


@router.get("/copilot")
async def test_copilot():
    """Detect GitHub Copilot extension conflicts."""
    return await _check_copilot_conflict()


@router.get("/agent-setup")
async def test_agent_setup(request: Request):
    """Comprehensive agent readiness check."""
    return await _check_agent_setup(base_url=str(request.base_url).rstrip("/"))


@router.get("/agent")
async def test_agent():
    """Test agent with a simple completion."""
    return await _test_agent_completion()
