"""Structured CLI error formatter for LLMHosts.

Provides unified, color-coded error output with error codes,
human-readable summaries, root causes, and remediation hints.
"""

from __future__ import annotations

import sys
import traceback
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from rich.console import Console


class Severity(Enum):
    """Error severity levels with associated Rich color tags."""

    FATAL = "red"
    WARNING = "yellow"
    INFO = "blue"


@dataclass(frozen=True, slots=True)
class CLIError:
    """A structured CLI error with context and remediation."""

    code: str
    summary: str
    cause: str
    fix: str
    severity: Severity = Severity.FATAL


# ---------------------------------------------------------------------------
# Error catalog — at least 10 common scenarios
# ---------------------------------------------------------------------------

ERRORS: dict[str, CLIError] = {
    "PORT_IN_USE": CLIError(
        code="E001",
        summary="Port already in use",
        cause="Another process is bound to {host}:{port}.",
        fix="Kill the process: lsof -ti :{port} | xargs kill\nOr use a different port: llmhost serve --port {alt_port}",
    ),
    "STARTUP_FAILED": CLIError(
        code="E002",
        summary="Server startup failed",
        cause="{detail}",
        fix="Run llmhost doctor for diagnostics.\nCheck config: cat ~/.config/llmhosts/config.toml",
    ),
    "INVALID_API_KEY": CLIError(
        code="E003",
        summary="Invalid API key format",
        cause="{detail}",
        fix="Check the key format for {provider}.\nRe-add: llmhost keys add {provider} <key>",
    ),
    "KEY_VALIDATION_FAILED": CLIError(
        code="E004",
        summary="API key validation failed",
        cause="The {provider} API rejected the key: {detail}",
        fix="Verify the key has credits: https://platform.openai.com/usage\n"
        "Re-add: llmhost keys add {provider} <new-key> --validate",
    ),
    "CONFIG_PARSE_ERROR": CLIError(
        code="E005",
        summary="Configuration file is invalid",
        cause="Failed to parse {path}: {detail}",
        fix="Check TOML syntax: cat {path}\nReset to defaults: rm {path} && llmhost setup",
    ),
    "CONFIG_WRITE_ERROR": CLIError(
        code="E006",
        summary="Could not write configuration",
        cause="Failed to save to {path}: {detail}",
        fix="Check file permissions: ls -la {path}\nCreate parent directory: mkdir -p $(dirname {path})",
    ),
    "OLLAMA_UNREACHABLE": CLIError(
        code="E007",
        summary="Inference engine is not reachable",
        cause="Cannot connect to inference engine at {host}.",
        fix="Run: llmhosts serve (auto-installs and starts the inference engine)\n"
        "Or run: llmhosts doctor (for detailed diagnostics)",
        severity=Severity.WARNING,
    ),
    "NO_BACKENDS": CLIError(
        code="E008",
        summary="No inference backends available",
        cause="Neither local nor cloud (BYOK) backends found.",
        fix="Run: llmhosts serve (auto-provisions a local inference engine)\n"
        "Or add a cloud key: llmhosts keys add openai sk-...",
        severity=Severity.WARNING,
    ),
    "MODEL_NOT_FOUND": CLIError(
        code="E009",
        summary="Model not found",
        cause='Requested model "{model}" is not available on any backend.',
        fix="List available models: llmhosts serve, then curl localhost:4000/v1/models\n"
        "Pull a model through LLMHosts: llmhosts serve (auto-provisions)",
    ),
    "DB_MIGRATION_ERROR": CLIError(
        code="E010",
        summary="Database migration failed",
        cause="{detail}",
        fix="Check database file permissions.\n"
        "Try: llmhost db current  (to see current revision)\n"
        "Reset: rm -f ~/.config/llmhosts/audit.db && llmhost db upgrade head",
    ),
    "TUNNEL_NOT_FOUND": CLIError(
        code="E011",
        summary="Relay extension not available",
        cause="The Rust relay core (_llmhosts.relay) is not built or installed.",
        fix="Build with: cd relay_core && maturin develop --release\n"
        "Or install the pre-built wheel: pip install llmhosts[relay]",
    ),
    "KEY_DECRYPT_ERROR": CLIError(
        code="E012",
        summary="Could not decrypt API keys",
        cause="Key file corrupted or encryption key mismatch.",
        fix="Re-add keys: llmhost keys add <provider> <key>\nKeys stored in: ~/.config/llmhosts/keys/",
        severity=Severity.WARNING,
    ),
    "PERMISSION_DENIED": CLIError(
        code="E013",
        summary="Permission denied",
        cause="Insufficient permissions for {detail}.",
        fix="Check ownership: ls -la {path}\nFix: sudo chown -R $USER:$USER ~/.config/llmhosts/",
    ),
}


def format_error(
    error_key: str,
    *,
    debug: bool = False,
    exc: BaseException | None = None,
    **kwargs: str,
) -> str:
    """Format a structured error message for terminal output.

    Parameters
    ----------
    error_key:
        Key into the ERRORS catalog (e.g. ``"PORT_IN_USE"``).
    debug:
        If True, append the full Python traceback.
    exc:
        The original exception (used for traceback in debug mode).
    **kwargs:
        Interpolation values for the error template fields.
    """
    err = ERRORS.get(error_key)
    if err is None:
        return f"[red]Error:[/red] {kwargs.get('detail', 'Unknown error')}"

    color = err.severity.value

    # Safe string interpolation — missing keys show as {key}
    def _fmt(template: str) -> str:
        try:
            return template.format(**kwargs)
        except KeyError:
            return template

    lines = [
        f"[{color} bold]{err.code} — {err.summary}[/{color} bold]",
        "",
        f"[dim]Cause:[/dim]  {_fmt(err.cause)}",
        "",
        "[dim]Fix:[/dim]",
    ]
    for fix_line in _fmt(err.fix).split("\n"):
        lines.append(f"  {fix_line}")

    if debug and exc is not None:
        lines.append("")
        lines.append("[dim]─── Debug traceback ───[/dim]")
        tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
        lines.append(f"[dim]{tb}[/dim]")

    return "\n".join(lines)


def print_error(
    console: Console,
    error_key: str,
    *,
    debug: bool = False,
    exc: BaseException | None = None,
    **kwargs: str,
) -> None:
    """Print a structured error to the Rich console and exit."""
    console.print()
    console.print(format_error(error_key, debug=debug, exc=exc, **kwargs))
    console.print()

    err = ERRORS.get(error_key)
    if err and err.severity == Severity.FATAL:
        sys.exit(1)


def print_error_generic(
    console: Console,
    message: str,
    *,
    debug: bool = False,
    exc: BaseException | None = None,
) -> None:
    """Print an unrecognized error and exit."""
    console.print()
    console.print(f"[red bold]Error:[/red bold] {message}")
    if debug and exc is not None:
        console.print()
        console.print("[dim]─── Debug traceback ───[/dim]")
        tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
        console.print(f"[dim]{tb}[/dim]")
    console.print()
    sys.exit(1)
