"""Doctor mode: environment health checks and remediation."""

import json
import os
import sys
from pathlib import Path

import cli.shared as _shared
from cli.reporter import _build_reporter, _emit_run_started
from cli.shared import (
    _ensure_preflight_runner,
    config,
    log,
)
from common.capabilities import get_capabilities_payload
from common.runtime_modes import MODE_DOCTOR


def _render_rich_doctor_table(checks: list[dict]) -> None:
    """Display doctor results as a Rich table when running interactively."""
    if not sys.stderr.isatty():
        return
    try:
        from rich.console import Console
        from rich.table import Table
    except ImportError:
        return

    console = Console(stderr=True)
    table = Table(title="ScreenForge Doctor", show_lines=True)
    table.add_column("Check", style="bold")
    table.add_column("Status", justify="center")
    table.add_column("Details")

    for check in checks:
        name = check.get("name", "unknown")
        ok = check.get("ok", False)
        advisory = check.get("advisory", False)
        # An advisory finding (ok=False but advisory) is a NOTE, not a failure —
        # render it yellow so it never reads as a broken environment.
        if ok:
            status = "[green]PASS[/]"
        elif advisory:
            status = "[yellow]NOTE[/]"
        else:
            status = "[red]FAIL[/]"
        details = ""
        if not ok:
            issues = check.get("issues", []) or []
            errors = check.get("errors", []) or []
            error = check.get("error", "")
            hint = check.get("hint", "")
            parts = [str(i).strip() for i in issues + errors if str(i).strip()]
            if error and str(error).strip():
                parts.append(str(error).strip())
            if hint and str(hint).strip():
                parts.append(f"[dim]{hint}[/dim]")
            details = "\n".join(parts[:3])
        else:
            if check.get("path"):
                details = f"[dim]{check['path']}[/dim]"
        table.add_row(name, status, details)

    console.print(table)


def _normalize_doctor_message(message: str) -> str:
    lines = [line.strip() for line in str(message).splitlines() if line.strip()]
    return lines[0] if lines else ""


def _iter_doctor_check_findings(check: dict):
    for issue in check.get("issues", []) or []:
        text = _normalize_doctor_message(issue)
        if text:
            yield "issue", text

    for error in check.get("errors", []) or []:
        text = _normalize_doctor_message(error)
        if text:
            yield "error", text

    error_text = _normalize_doctor_message(check.get("error", ""))
    if error_text:
        yield "error", error_text

    hint_text = _normalize_doctor_message(check.get("hint", ""))
    if hint_text:
        yield "hint", hint_text


def _classify_doctor_check(check: dict) -> dict:
    check_name = str(check.get("name", "")).strip()

    if check_name == "config":
        return {"category": "config", "title": "Configuration", "priority": 1}
    if check_name in {"venv_consistency", "runtime_paths"}:
        return {"category": "runtime", "title": "Runtime", "priority": 2}
    if check_name in {"adb", "uiautomator2", "wda", "playwright"}:
        return {"category": "dependency", "title": "Dependency", "priority": 3}
    if check_name in {
        "adb_devices",
        "wda_status",
        "cdp_debug_endpoint",
        "http://localhost:8100",
        "http://localhost:9222",
    } or check_name.startswith(("http://", "https://")):
        return {"category": "connectivity", "title": "Connectivity", "priority": 4}
    if check_name == "orphan_web_browser":
        return {"category": "cleanup", "title": "Cleanup", "priority": 5}
    return {"category": "other", "title": "Other", "priority": 6}


def _doctor_fix_doc_reference(doc_name: str, section: str) -> dict:
    return {
        "fix_doc": doc_name,
        "fix_doc_section": section,
    }


def _build_doctor_remediation(check_name: str, message: str) -> dict:
    message = str(message).strip()
    normalized_check_name = str(check_name).strip()
    common_doc = _doctor_fix_doc_reference(
        "docs/agent_guide.md", "Troubleshooting"
    )

    remediation = {
        "fix_label": "See diagnostics docs",
        "fix_command": "",
        **common_doc,
    }

    if normalized_check_name == "config":
        return {
            "fix_label": "Complete runtime configuration",
            "fix_command": "",
            **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
        }

    if normalized_check_name == "venv_consistency":
        return {
            "fix_label": "Repair venv entry-point drift",
            "fix_command": "./.venv/bin/python scripts/repair_venv.py",
            **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
        }

    if normalized_check_name == "runtime_paths":
        return {
            "fix_label": "Verify working directory is writable",
            "fix_command": "",
            **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
        }

    if normalized_check_name == "uiautomator2":
        return {
            "fix_label": "Install Android extras",
            "fix_command": 'pip install -e ".[android]"',
            **_doctor_fix_doc_reference("README.md", "Platform-specific extras"),
        }

    if normalized_check_name == "playwright":
        return {
            "fix_label": "Install web dependencies (default extra)",
            "fix_command": "pip install -e .",
            **_doctor_fix_doc_reference("README.md", "Platform-specific extras"),
        }

    if normalized_check_name == "wda":
        return {
            "fix_label": "Install iOS extras",
            "fix_command": 'pip install -e ".[ios]"',
            **_doctor_fix_doc_reference("README.md", "Platform-specific extras"),
        }

    if normalized_check_name == "adb":
        return {
            "fix_label": "Install adb and add to PATH",
            "fix_command": "",
            **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
        }

    if normalized_check_name == "adb_devices":
        if "environment restricts" in message or "host terminal" in message:
            return {
                "fix_label": "Retry adb check in host terminal",
                "fix_command": "adb devices",
                **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
            }
        return {
            "fix_label": "Check Android device connection",
            "fix_command": "adb devices",
            **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
        }

    if normalized_check_name in {"http://localhost:8100", "wda_status"}:
        return {
            "fix_label": "Verify WebDriverAgent service status",
            "fix_command": "",
            **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
        }

    if normalized_check_name == "orphan_web_browser":
        return {
            "fix_label": "Stop the leaked persistent Chromium",
            "fix_command": "screenforge --web-stop",
            **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
        }

    if normalized_check_name in {"http://localhost:9222", "cdp_debug_endpoint"}:
        if "environment restricts" in message or "host terminal" in message:
            return {
                "fix_label": "Check Chrome DevTools port from host terminal",
                "fix_command": "curl -sS http://localhost:9222/json/version",
                **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
            }
        return {
            "fix_label": "Verify Chrome DevTools debug port",
            "fix_command": "",
            **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
        }

    if "OPENAI_API_KEY" in message or "WEB_CDP_URL" in message:
        return {
            "fix_label": "Complete runtime configuration",
            "fix_command": "",
            **_doctor_fix_doc_reference("docs/agent_guide.md", "Troubleshooting"),
        }

    return remediation


def _doctor_action_signature(category: str, item: dict) -> tuple:
    return (
        category,
        tuple(item.get("check_names", [])),
        item.get("fix_label", ""),
        item.get("fix_command", ""),
        item.get("fix_doc", ""),
        item.get("fix_doc_section", ""),
    )


def _append_recommended_action(actions: list[dict], category: str, priority: int, item: dict) -> None:
    candidate = {
        "category": category,
        "priority": priority,
        **item,
    }
    candidate_signature = _doctor_action_signature(category, item)

    for index, existing in enumerate(actions):
        existing_signature = _doctor_action_signature(existing.get("category", ""), existing)
        if existing_signature != candidate_signature:
            continue

        if candidate.get("kind") == "hint":
            actions[index] = candidate
            return

        if existing.get("kind") == "hint":
            return

    actions.append(candidate)


def _build_doctor_summary(checks: list[dict]) -> dict:
    groups = {}
    advisories = []
    severity_rank = {"error": 0, "issue": 1, "hint": 2}

    for check in checks:
        if check.get("ok", False):
            continue

        # Advisory findings (e.g. a healthy persistent browser) are NOTES, not
        # blockers: collect them separately so they never affect summary["ok"]
        # or sort among real failures, but the remediation note still reaches
        # the user (surfaced even on the success path).
        if check.get("advisory", False):
            check_name = str(check.get("name", "unknown")).strip() or "unknown"
            # One note per advisory check: the error is the "what", the
            # remediation carries the "how" (fix_command). The hint largely
            # restates the remediation, so don't emit it as a second line.
            message = _normalize_doctor_message(check.get("error", "")) or next(
                (m for _k, m in _iter_doctor_check_findings(check)), ""
            )
            if message:
                remediation = _build_doctor_remediation(check_name, message)
                advisories.append(
                    {
                        "message": message,
                        "check_names": [check_name],
                        "fix_label": remediation.get("fix_label", ""),
                        "fix_command": remediation.get("fix_command", ""),
                        "fix_doc": remediation.get("fix_doc", ""),
                        "fix_doc_section": remediation.get("fix_doc_section", ""),
                    }
                )
            continue

        group_meta = _classify_doctor_check(check)
        category = group_meta["category"]
        group = groups.setdefault(
            category,
            {
                "category": category,
                "title": group_meta["title"],
                "priority": group_meta["priority"],
                "items": [],
                "_item_map": {},
            },
        )

        check_name = str(check.get("name", "unknown")).strip() or "unknown"
        for kind, message in _iter_doctor_check_findings(check):
            remediation = _build_doctor_remediation(check_name, message)
            existing = group["_item_map"].get(message)
            if existing:
                if check_name not in existing["check_names"]:
                    existing["check_names"].append(check_name)
                if severity_rank[kind] < severity_rank[existing["kind"]]:
                    existing["kind"] = kind
                if not existing["fix_command"] and remediation.get("fix_command", ""):
                    existing["fix_command"] = remediation.get("fix_command", "")
                if existing.get("fix_doc_section", "") == "Troubleshooting":
                    existing["fix_label"] = remediation.get("fix_label", existing["fix_label"])
                    existing["fix_doc"] = remediation.get("fix_doc", existing["fix_doc"])
                    existing["fix_doc_section"] = remediation.get(
                        "fix_doc_section", existing["fix_doc_section"]
                    )
                continue

            item = {
                "message": message,
                "kind": kind,
                "check_names": [check_name],
                "fix_label": remediation.get("fix_label", ""),
                "fix_command": remediation.get("fix_command", ""),
                "fix_doc": remediation.get("fix_doc", ""),
                "fix_doc_section": remediation.get("fix_doc_section", ""),
                "fix_priority": group["priority"],
            }
            group["_item_map"][message] = item
            group["items"].append(item)

    ordered_groups = sorted(
        groups.values(),
        key=lambda item: (item["priority"], item["category"]),
    )
    for group in ordered_groups:
        group.pop("_item_map", None)

    top_items = []
    recommended_actions = []
    for group in ordered_groups:
        for item in group["items"]:
            top_items.append(item["message"])
            _append_recommended_action(
                recommended_actions,
                group["category"],
                group["priority"],
                item,
            )

    return {
        "ok": not ordered_groups,
        "group_count": len(ordered_groups),
        "top_items": top_items,
        "groups": ordered_groups,
        "recommended_actions": recommended_actions,
        "advisories": advisories,
    }


def _build_doctor_remediation_items(checks: list[dict]) -> list[str]:
    return _build_doctor_summary(checks).get("top_items", [])


def _build_doctor_check_failure_message(check: dict) -> str:
    details = []

    def add_detail(message: str) -> None:
        text = _normalize_doctor_message(message)
        if text and text not in details:
            details.append(text)

    for issue in check.get("issues", []) or []:
        add_detail(issue)

    for error in check.get("errors", []) or []:
        add_detail(error)

    add_detail(check.get("error", ""))

    if not details:
        if "path" in check and not str(check.get("path", "")).strip():
            details.append("Executable not found")
        elif check.get("name") == "runtime_paths":
            details.append("Runtime directory unavailable or not writable")
        else:
            details.append("Check failed")

    return f"   - {check.get('name', 'unknown')}: {'; '.join(details)}"


def run_doctor_mode(args, output_script_path: str) -> int:
    reporter = _build_reporter(args, output_script_path, MODE_DOCTOR)
    final_status = "failed"
    exit_code = 1
    final_error = ""
    _emit_run_started(reporter, args, output_script_path, MODE_DOCTOR)

    try:
        _ensure_preflight_runner()
        result = _shared.run_preflight(
            platform=args.platform,
            script_dir=Path(os.path.dirname(os.path.abspath(output_script_path))),
            run_dir=Path(config.RUN_REPORT_BASE_DIR),
        )
        for check in result.get("checks", []):
            reporter.emit_event(
                "doctor_check",
                check_name=check.get("name", ""),
                success=check.get("ok", False),
                detail=check,
            )
        doctor_summary = _build_doctor_summary(result.get("checks", []))
        reporter.update_summary(doctor_summary=doctor_summary)
        reporter.emit_event(
            "doctor_summary",
            ok=doctor_summary.get("ok", False),
            group_count=doctor_summary.get("group_count", 0),
            top_items=doctor_summary.get("top_items", []),
            groups=doctor_summary.get("groups", []),
            recommended_actions=doctor_summary.get("recommended_actions", []),
            advisories=doctor_summary.get("advisories", []),
        )

        _render_rich_doctor_table(result.get("checks", []))

        if result.get("ok"):
            log.info("🩺 [Doctor] Environment check passed.")
            final_status = "success"
            exit_code = 0
        else:
            final_error = "Doctor check failed"
            log.error("❌ [Doctor] Environment check failed. Fix prerequisites first.")
            for check in result.get("checks", []):
                # Advisory findings are NOTES, not failures — keep them out of
                # the error list (they're surfaced below regardless of verdict).
                if not check.get("ok", False) and not check.get("advisory", False):
                    log.error(_build_doctor_check_failure_message(check))
            remediation_items = doctor_summary.get("recommended_actions", [])
            if remediation_items:
                log.error("🧭 [Doctor] Recommended actions:")
                for index, item in enumerate(remediation_items, start=1):
                    log.error(f"   {index}. {item.get('message', '')}")
                    if item.get("fix_command"):
                        log.error(f"      Run: {item.get('fix_command', '')}")
                    if item.get("fix_doc"):
                        log.error(
                            "      Docs: "
                            f"{item.get('fix_doc', '')} ({item.get('fix_doc_section', '')})"
                        )

        # Advisory notes surface on BOTH paths (a healthy env can still have a
        # leftover browser worth reclaiming). They never affect status/exit code.
        for note in doctor_summary.get("advisories", []):
            log.info(f"💡 [Doctor] Note: {note.get('message', '')}")
            if note.get("fix_command"):
                log.info(f"      Run: {note.get('fix_command', '')}")
    finally:
        reporter.finalize(
            status=final_status,
            exit_code=exit_code,
            steps_executed=len(result.get("checks", [])) if "result" in locals() else 0,
            last_error=final_error,
        )
    return exit_code


def run_capabilities_mode(args) -> int:
    payload = get_capabilities_payload()
    sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
    sys.stdout.flush()
    return 0
