"""CLI scaffold for managing AI agent configuration files."""

from __future__ import annotations

import json
import logging
import sys
import traceback
from dataclasses import dataclass
from enum import Enum
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as package_version
from pathlib import Path
from typing import Annotated

import typer
from pydantic import BaseModel, Field
from pydantic import ValidationError as PydanticValidationError

from dotai.core.errors import (
    ConfigNotFoundError,
    DotAgentError,
    StorageError,
    UsageError,
    ValidationFailureError,
)

logger = logging.getLogger("dotai.cli")


class OutputFormat(str, Enum):  # noqa: UP042
    """Supported structured output formats."""

    json = "json"
    table = "table"


class AgentConfig(BaseModel):
    """A single AI agent configuration entry."""

    provider: str
    model: str
    system_prompt: str | None = None
    temperature: float = Field(default=0.2, ge=0.0, le=2.0)
    enabled: bool = True
    tags: list[str] = Field(default_factory=list)


class AgentRegistry(BaseModel):
    """Container for all configured agents."""

    agents: dict[str, AgentConfig] = Field(default_factory=dict)


@dataclass(slots=True)
class AppState:
    """Values shared across commands via Typer context."""

    config_path: Path
    debug: bool


app = typer.Typer(
    name="dotag",
    help="Manage AI agent configs from one CLI.",
    invoke_without_command=True,
    no_args_is_help=True,
    add_completion=True,
)
agent_app = typer.Typer(help="Create, inspect, and update agent configs.")
app.add_typer(agent_app, name="agent")


def _set_logging(verbose: int) -> None:
    logging.basicConfig(format="%(levelname)s: %(message)s")
    level_by_count = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG, 3: logging.DEBUG}
    logger.setLevel(level_by_count.get(verbose, logging.DEBUG))


def _state_from_ctx(ctx: typer.Context) -> AppState:
    obj = ctx.obj
    if not isinstance(obj, AppState):
        message = "CLI state was not initialized; run command through main app callback."
        raise UsageError(message)
    return obj


def _resolve_config_path(path: Path) -> Path:
    resolved = path.expanduser()
    if resolved.exists() and resolved.is_dir():
        message = f"--config must be a file path, not a directory: {resolved}"
        raise UsageError(message)
    return resolved


def _load_registry(path: Path) -> AgentRegistry:
    if not path.exists():
        return AgentRegistry()

    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except OSError as exc:
        message = f"Unable to read config file: {path}"
        raise StorageError(message) from exc
    except json.JSONDecodeError as exc:
        message = f"Config file is not valid JSON: {path}"
        raise ValidationFailureError(message) from exc

    try:
        return AgentRegistry.model_validate(payload)
    except PydanticValidationError as exc:
        message = f"Config file has invalid schema: {path}"
        raise ValidationFailureError(message) from exc


def _save_registry(path: Path, registry: AgentRegistry) -> None:
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(registry.model_dump_json(indent=2) + "\n", encoding="utf-8")
    except OSError as exc:
        message = f"Unable to write config file: {path}"
        raise StorageError(message) from exc


def _emit_error(exc: BaseException, *, debug: bool) -> None:
    typer.echo(f"Error: {exc}", err=True)
    if debug:
        traceback.print_exc(file=sys.stderr)


def _package_version() -> str:
    try:
        return package_version("dotai")
    except PackageNotFoundError:
        return "0.0.0-dev"


@app.callback()
def cli_callback(
    ctx: typer.Context,
    verbose: Annotated[
        int,
        typer.Option(
            "--verbose", "-v", count=True, min=0, max=3, help="Increase logging verbosity."
        ),
    ] = 0,
    config: Annotated[
        Path,
        typer.Option(
            "--config",
            "-c",
            envvar="dotai_CONFIG",
            help="Path to dotai JSON config file.",
        ),
    ] = Path(".dotai/agents.json"),
    version: Annotated[
        bool,
        typer.Option("--version", "-V", help="Print version and exit."),
    ] = False,
    debug: Annotated[
        bool,
        typer.Option("--debug", help="Show traceback for handled errors."),
    ] = False,
) -> None:
    _set_logging(verbose)

    if version:
        typer.echo(_package_version())
        raise typer.Exit(code=0)

    ctx.obj = AppState(config_path=_resolve_config_path(config), debug=debug)


@app.command("help")
def cli_help(ctx: typer.Context) -> None:
    parent_ctx = ctx.parent
    if parent_ctx is None:
        message = "No parent context available for help output."
        raise UsageError(message)
    typer.echo(parent_ctx.get_help())


@app.command("path")
def config_path(ctx: typer.Context) -> None:
    state = _state_from_ctx(ctx)
    typer.echo(str(state.config_path))


@app.command("validate")
def validate_registry(ctx: typer.Context) -> None:
    state = _state_from_ctx(ctx)
    _load_registry(state.config_path)
    typer.echo(f"Config is valid: {state.config_path}")


@agent_app.command("list")
def agent_list(
    ctx: typer.Context,
    output: OutputFormat = typer.Option(
        OutputFormat.table,
        "--output",
        "-o",
        help="Select output format.",
    ),
) -> None:
    state = _state_from_ctx(ctx)
    registry = _load_registry(state.config_path)

    if output is OutputFormat.json:
        typer.echo(registry.model_dump_json(indent=2))
        return

    if not registry.agents:
        typer.echo("No agents configured.")
        return

    for name in sorted(registry.agents):
        agent = registry.agents[name]
        status = "enabled" if agent.enabled else "disabled"
        typer.echo(f"{name}\t{agent.provider}/{agent.model}\t{status}")


@agent_app.command("show")
def agent_show(
    ctx: typer.Context,
    name: Annotated[str, typer.Argument(help="Agent name.")],
) -> None:
    state = _state_from_ctx(ctx)
    registry = _load_registry(state.config_path)
    agent = registry.agents.get(name)
    if agent is None:
        message = f"Agent not found: {name}"
        raise ConfigNotFoundError(message)
    typer.echo(agent.model_dump_json(indent=2))


@agent_app.command("set")
def agent_set(  # noqa: PLR0913,PLR0917
    ctx: typer.Context,
    name: Annotated[str, typer.Argument(help="Agent name to create or update.")],
    provider: Annotated[str, typer.Option("--provider", help="Provider slug.")],
    model: Annotated[str, typer.Option("--model", help="Model id.")],
    system_prompt: Annotated[
        str | None,
        typer.Option("--system-prompt", help="Optional system prompt."),
    ] = None,
    temperature: Annotated[
        float,
        typer.Option("--temperature", min=0.0, max=2.0, help="Sampling temperature."),
    ] = 0.2,
    enabled: Annotated[
        bool,
        typer.Option("--enabled/--disabled", help="Enable or disable the agent."),
    ] = True,
    tag: Annotated[
        list[str] | None,
        typer.Option("--tag", help="Repeatable tag. Use multiple --tag flags."),
    ] = None,
) -> None:
    state = _state_from_ctx(ctx)
    registry = _load_registry(state.config_path)
    registry.agents[name] = AgentConfig(
        provider=provider,
        model=model,
        system_prompt=system_prompt,
        temperature=temperature,
        enabled=enabled,
        tags=sorted(set(tag or [])),
    )
    _save_registry(state.config_path, registry)
    typer.echo(f"Saved agent '{name}' in {state.config_path}")


@agent_app.command("remove")
def agent_remove(
    ctx: typer.Context,
    name: Annotated[str, typer.Argument(help="Agent name.")],
) -> None:
    state = _state_from_ctx(ctx)
    registry = _load_registry(state.config_path)
    if name not in registry.agents:
        message = f"Agent not found: {name}"
        raise ConfigNotFoundError(message)
    del registry.agents[name]
    _save_registry(state.config_path, registry)
    typer.echo(f"Removed agent '{name}'")


def main() -> None:
    try:
        app()
    except DotAgentError as exc:
        _emit_error(exc, debug="--debug" in sys.argv)
        raise SystemExit(exc.exit_code) from exc


if __name__ == "__main__":
    main()
