"""Plain-English health diagnostics for non-technical users."""

from __future__ import annotations

import shutil
import socket
import subprocess
import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Callable


class Severity(str, Enum):
    OK = "ok"
    WARNING = "warning"
    ERROR = "error"
    INFO = "info"


@dataclass
class DiagnosticResult:
    name: str
    severity: Severity
    message: str  # plain English, no jargon
    fix_hint: str = ""  # what the user should do
    auto_fixable: bool = False
    fix_fn: Callable | None = field(default=None, repr=False)

    def is_ok(self) -> bool:
        return self.severity == Severity.OK


class DiagnosticRunner:
    """Runs all health diagnostics and returns plain-English results."""

    def run_all(self) -> list[DiagnosticResult]:
        checks = [
            self._check_python_version(),
            self._check_port_available(11434, "Ollama"),
            self._check_ollama_running(),
            self._check_disk_space(),
            self._check_gpu_available(),
        ]
        return checks

    def _check_python_version(self) -> DiagnosticResult:
        major, minor = sys.version_info[:2]
        if (major, minor) >= (3, 10):
            return DiagnosticResult(
                name="Python version",
                severity=Severity.OK,
                message=f"Python {major}.{minor} — supported ✓",
            )
        return DiagnosticResult(
            name="Python version",
            severity=Severity.ERROR,
            message=f"Python {major}.{minor} is too old. LLMHosts needs Python 3.10 or newer.",
            fix_hint="Visit https://python.org/downloads and install the latest Python 3.x.",
        )

    def _check_port_available(self, port: int, service: str) -> DiagnosticResult:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.settimeout(1)
            result = sock.connect_ex(("127.0.0.1", port))
        if result == 0:
            return DiagnosticResult(
                name=f"{service} port ({port})",
                severity=Severity.OK,
                message=f"{service} is reachable on port {port} ✓",
            )
        return DiagnosticResult(
            name=f"{service} port ({port})",
            severity=Severity.WARNING,
            message=f"{service} is not running on port {port}. Some features won't work.",
            fix_hint=f"Start {service} first: run `ollama serve` in another terminal.",
        )

    def _check_ollama_running(self) -> DiagnosticResult:
        try:
            result = subprocess.run(  # nosec B603 B607
                ["ollama", "list"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode == 0:
                lines = [line for line in result.stdout.strip().splitlines() if line and "NAME" not in line]
                count = len(lines)
                if count == 0:
                    return DiagnosticResult(
                        name="Ollama models",
                        severity=Severity.WARNING,
                        message="Ollama is running but no models are downloaded yet.",
                        fix_hint="Run: ollama pull llama3.2:3b (small model, ~2GB)",
                    )
                return DiagnosticResult(
                    name="Ollama models",
                    severity=Severity.OK,
                    message=f"Ollama is running with {count} model(s) ✓",
                )
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass
        return DiagnosticResult(
            name="Ollama models",
            severity=Severity.ERROR,
            message="Ollama is not installed or not in your PATH.",
            fix_hint="Install Ollama from https://ollama.ai — it's free and takes 2 minutes.",
        )

    def _check_disk_space(self) -> DiagnosticResult:
        _total, _used, free = shutil.disk_usage("/")
        free_gb = free / (1024**3)
        if free_gb >= 10:
            return DiagnosticResult(
                name="Disk space",
                severity=Severity.OK,
                message=f"{free_gb:.1f} GB free — plenty of space ✓",
            )
        if free_gb >= 3:
            return DiagnosticResult(
                name="Disk space",
                severity=Severity.WARNING,
                message=f"Only {free_gb:.1f} GB free. Small models will work but large ones may not fit.",
                fix_hint="Free up some disk space if you want to run 13B+ models.",
            )
        return DiagnosticResult(
            name="Disk space",
            severity=Severity.ERROR,
            message=f"Only {free_gb:.1f} GB free. You need at least 3 GB to run any model.",
            fix_hint="Free up disk space before continuing.",
        )

    def _check_gpu_available(self) -> DiagnosticResult:
        # Try nvidia-smi
        try:
            result = subprocess.run(  # nosec B603 B607
                ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode == 0 and result.stdout.strip():
                gpu = result.stdout.strip().splitlines()[0]
                return DiagnosticResult(
                    name="GPU",
                    severity=Severity.OK,
                    message=f"NVIDIA GPU detected: {gpu} ✓",
                )
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass
        # Fallback to CPU-only info
        return DiagnosticResult(
            name="GPU",
            severity=Severity.INFO,
            message="No NVIDIA GPU detected — CPU inference only (slower but works).",
            fix_hint="For faster inference, a GPU with 4GB+ VRAM is recommended.",
        )
