"""Worker-side diagnostics helpers for CLI support."""

from __future__ import annotations

import importlib.util
import plistlib
import shutil
from collections import deque
from dataclasses import dataclass
from pathlib import Path
from typing import Literal

import httpx
import yaml

from hive_compute.data import bundled_doctor_smoke_audio_path
from hive_compute.handlers.whisper_handler import WhisperHandler
from hive_compute.worker.keychain import describe_worker_token_storage, load_worker_token
from hive_compute.worker.launchd import (
    launch_agent_path,
    service_label_for_worker,
    worker_log_dir,
)

DiagnosticStatus = Literal["ok", "warn", "fail"]


@dataclass(frozen=True)
class DoctorCheck:
    name: str
    status: DiagnosticStatus
    summary: str
    detail: str | None = None


def run_doctor(
    config_path: str,
    *,
    check_network: bool = True,
    run_backend_smoke: bool = False,
) -> list[DoctorCheck]:
    """Collect the current worker's local diagnostics."""
    from hive_compute.worker.config_model import WorkerConfig

    config_path_obj = Path(config_path).expanduser()
    if not config_path_obj.exists():
        return [
            DoctorCheck(
                "config",
                "fail",
                f"Config file is missing: {config_path_obj}",
                "Run 'hive init' first.",
            )
        ]

    try:
        with config_path_obj.open(encoding="utf-8") as handle:
            raw = yaml.safe_load(handle) or {}
    except Exception as exc:
        return [
            DoctorCheck(
                "config",
                "fail",
                f"Config file could not be parsed: {config_path_obj}",
                str(exc),
            )
        ]

    if not isinstance(raw, dict):
        return [
            DoctorCheck(
                "config",
                "fail",
                f"Config file is not a valid YAML mapping: {config_path_obj}",
                f"Got {type(raw).__name__}, expected a key-value mapping. Re-run 'hive init'.",
            )
        ]

    try:
        config = WorkerConfig(**raw)
    except Exception as exc:
        return [
            DoctorCheck(
                "config",
                "fail",
                f"Config validation failed: {config_path_obj}",
                str(exc),
            )
        ]

    checks = [DoctorCheck("config", "ok", f"Loaded {config_path_obj}")]
    worker_id = config.worker_id
    coordinator_url = config.coordinator_url

    if worker_id:
        checks.append(DoctorCheck("worker", "ok", f"Worker ID: {worker_id}"))
    else:
        checks.append(
            DoctorCheck(
                "worker",
                "fail",
                "Config is missing worker_id",
                "Re-run 'hive init' for this machine.",
            )
        )

    if coordinator_url:
        checks.append(DoctorCheck("coordinator", "ok", f"Configured for {coordinator_url}"))
    else:
        checks.append(
            DoctorCheck(
                "coordinator",
                "fail",
                "Config is missing coordinator_url",
                "Re-run 'hive init' with the deployed coordinator URL.",
            )
        )

    checks.append(_check_credentials(config.model_dump()))
    runtime_checks = _check_runtime_requirements(config)
    checks.extend(runtime_checks)
    if run_backend_smoke:
        checks.append(_check_backend_smoke(config, runtime_checks))

    if worker_id:
        checks.append(_check_launch_agent(config_path_obj, worker_id))
        checks.append(_check_logs(worker_id))

    if check_network and coordinator_url:
        checks.append(_check_health(coordinator_url))

    return checks


def resolve_worker_log_paths(worker_id: str) -> dict[str, Path]:
    """Return the expected stdout/stderr log paths for a worker."""
    label = service_label_for_worker(worker_id)
    logs_dir = worker_log_dir()
    return {
        "stdout": logs_dir / f"{label}.out.log",
        "stderr": logs_dir / f"{label}.err.log",
    }


def read_log_tail(path: Path, *, lines: int = 80) -> list[str]:
    """Return the last N lines from a worker log file."""
    if lines <= 0 or not path.exists():
        return []

    with path.open(encoding="utf-8", errors="replace") as handle:
        return [line.rstrip("\n") for line in deque(handle, maxlen=lines)]


def _check_credentials(config: dict) -> DoctorCheck:
    keychain_ref = config.get("worker_token_keychain")
    if keychain_ref and not isinstance(keychain_ref, dict):
        return DoctorCheck(
            "credentials",
            "fail",
            "worker_token_keychain is not a valid mapping",
            f"Got {type(keychain_ref).__name__}. Re-run 'hive init'.",
        )
    storage = describe_worker_token_storage(config)
    if keychain_ref and isinstance(keychain_ref, dict):
        try:
            load_worker_token(keychain_ref)
        except Exception as exc:
            return DoctorCheck(
                "credentials",
                "fail",
                f"Stored in {storage}, but the token could not be read",
                str(exc),
            )
        return DoctorCheck("credentials", "ok", f"Credential resolves from {storage}")

    if config.get("worker_token"):
        return DoctorCheck(
            "credentials",
            "warn",
            "Worker token is stored in config.yaml (legacy)",
            "Re-run 'hive init' to migrate the credential into macOS Keychain.",
        )

    return DoctorCheck(
        "credentials",
        "fail",
        "Worker credential is missing",
        "Re-run 'hive init' with a fresh enrollment token.",
    )


def _check_runtime_requirements(config) -> list[DoctorCheck]:
    """Validate local runtime prerequisites for the configured handlers."""
    checks: list[DoctorCheck] = []
    transcribe = config.handlers.get("transcribe")
    if not transcribe:
        return checks

    ffmpeg = _binary_available("ffmpeg")
    if ffmpeg:
        checks.append(DoctorCheck("ffmpeg", "ok", f"ffmpeg available at {ffmpeg}"))
    else:
        checks.append(
            DoctorCheck(
                "ffmpeg",
                "fail",
                "ffmpeg is not installed",
                "Install ffmpeg (for example 'brew install ffmpeg') before starting the worker.",
            )
        )

    backend = transcribe.backend
    if backend == "mlx-whisper":
        if _module_available("mlx_whisper"):
            checks.append(DoctorCheck("backend", "ok", "Configured backend 'mlx-whisper' is importable"))
        else:
            checks.append(
                DoctorCheck(
                    "backend",
                    "fail",
                    "Configured backend 'mlx-whisper' is missing",
                    "Install worker dependencies again with uv/pip so the mlx-whisper package is available.",
                )
            )
        return checks

    if backend == "parakeet-mlx":
        if _module_available("parakeet_mlx"):
            checks.append(DoctorCheck("backend", "ok", "Configured backend 'parakeet-mlx' is importable"))
        else:
            checks.append(
                DoctorCheck(
                    "backend",
                    "fail",
                    "Configured backend 'parakeet-mlx' is missing",
                    "Install the optional parakeet extra "
                    "(for example `uv pip install -e \".[worker,parakeet]\"`) "
                    "or switch the worker config back to mlx-whisper.",
                )
            )
        return checks

    if backend == "whisper-cpp":
        binary = _binary_available("whisper-cpp")
        if binary:
            checks.append(DoctorCheck("backend", "ok", f"Configured backend 'whisper-cpp' available at {binary}"))
        else:
            checks.append(
                DoctorCheck(
                    "backend",
                    "fail",
                    "Configured backend 'whisper-cpp' is missing",
                    "Install the whisper-cpp CLI or switch the worker config back to mlx-whisper.",
                )
            )
        return checks

    checks.append(
        DoctorCheck(
            "backend",
            "fail",
            f"Unsupported transcribe backend: {backend}",
            "Set handlers.transcribe.backend to 'mlx-whisper', 'parakeet-mlx', or 'whisper-cpp'.",
        )
    )
    return checks


def _check_backend_smoke(config, runtime_checks: list[DoctorCheck]) -> DoctorCheck:
    transcribe = config.handlers.get("transcribe")
    if not transcribe:
        return DoctorCheck(
            "backend-smoke",
            "warn",
            "Backend smoke test skipped",
            "No transcribe handler is configured for this worker.",
        )

    if any(check.status == "fail" for check in runtime_checks):
        return DoctorCheck(
            "backend-smoke",
            "warn",
            "Backend smoke test skipped",
            "Fix the ffmpeg/backend prerequisite failures first, then rerun with --smoke-backend.",
        )

    try:
        result = _run_backend_smoke_test(transcribe.model_dump())
    except Exception as exc:
        return DoctorCheck(
            "backend-smoke",
            "fail",
            f"Configured backend '{transcribe.backend}' failed the smoke transcription",
            str(exc),
        )

    duration = result.get("duration_seconds")
    detail = (
        f"fixture={bundled_doctor_smoke_audio_path().name}, "
        f"language={result.get('language', transcribe.language)}, "
        f"words={result.get('word_count', 0)}, "
        f"duration={duration if duration is not None else 'n/a'}s"
    )
    return DoctorCheck(
        "backend-smoke",
        "ok",
        f"Configured backend '{transcribe.backend}' completed a bundled smoke transcription",
        detail,
    )


def _run_backend_smoke_test(transcribe_config: dict) -> dict:
    handler = WhisperHandler(transcribe_config)
    return handler._transcribe(bundled_doctor_smoke_audio_path())


def _binary_available(name: str) -> str | None:
    return shutil.which(name)


def _module_available(name: str) -> bool:
    return importlib.util.find_spec(name) is not None


def _check_launch_agent(config_path: Path, worker_id: str) -> DoctorCheck:
    plist_path = launch_agent_path(worker_id)
    if not plist_path.exists():
        return DoctorCheck(
            "launch-agent",
            "warn",
            "LaunchAgent is not installed",
            "Run 'hive install-service' if this worker should stay running in the background.",
        )

    try:
        plist = plistlib.loads(plist_path.read_bytes())
    except Exception as exc:
        return DoctorCheck("launch-agent", "fail", f"LaunchAgent plist is unreadable: {plist_path}", str(exc))

    config_arg = str(config_path)
    program_args = plist.get("ProgramArguments", [])
    if config_arg not in program_args:
        return DoctorCheck(
            "launch-agent",
            "warn",
            f"LaunchAgent exists at {plist_path}",
            "The plist does not reference this config path. Reinstall it with 'hive install-service'.",
        )

    return DoctorCheck("launch-agent", "ok", f"LaunchAgent plist present at {plist_path}")


def _check_logs(worker_id: str) -> DoctorCheck:
    log_paths = resolve_worker_log_paths(worker_id)
    existing = [str(path) for path in log_paths.values() if path.exists()]
    if not existing:
        return DoctorCheck(
            "logs",
            "warn",
            "No worker log files found yet",
            "Run 'hive logs' after the worker starts, or install the LaunchAgent first.",
        )
    return DoctorCheck("logs", "ok", f"Found {len(existing)} log file(s)", ", ".join(existing))


def _check_health(coordinator_url: str) -> DoctorCheck:
    health_url = f"{coordinator_url.rstrip('/')}/health"
    try:
        response = httpx.get(health_url, timeout=5)
    except Exception as exc:
        return DoctorCheck("network", "fail", f"Coordinator health check failed: {health_url}", str(exc))

    try:
        payload = response.json()
    except Exception as exc:
        return DoctorCheck(
            "network",
            "fail",
            f"Coordinator health check returned non-JSON: HTTP {response.status_code}",
            str(exc),
        )

    status = payload.get("status") if isinstance(payload, dict) else None
    if response.status_code != 200 or status != "ok":
        return DoctorCheck(
            "network",
            "fail",
            f"Coordinator health check returned HTTP {response.status_code}",
            f"Payload: {payload}",
        )

    return DoctorCheck("network", "ok", f"Coordinator /health responded from {health_url}")
