from __future__ import annotations

import shutil
import subprocess
import sys
from enum import Enum
from typing import Annotated

import typer

from cli_demo import DEFAULT_MODEL, PACKAGE_NAME, __version__
from cli_demo.config import (
    ConfigError,
    get_config_path,
    get_effective_model,
    load_config,
    mask_secret,
    normalize_optional_value,
    save_config,
)
from cli_demo.openai_service import (
    EmptyPromptError,
    EmptyResponseError,
    MissingAPIKeyError,
    OpenAIRequestError,
    ask_model,
)

CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}

app = typer.Typer(
    help="An OpenAI-powered AI CLI built with Typer.",
    no_args_is_help=True,
    add_completion=False,
    context_settings=CONTEXT_SETTINGS,
)
config_app = typer.Typer(
    help="View and modify local CLI config.",
    context_settings=CONTEXT_SETTINGS,
)
app.add_typer(config_app, name="config")
CLEAR_SENTINEL = "__CLI_DEMO_CLEAR__"


class ConfigField(str, Enum):
    API_KEY = "api-key"
    BASE_URL = "base-url"
    MODEL = "model"


def _version_callback(value: bool) -> None:
    if value:
        try:
            config = load_config()
            effective_model = get_effective_model(config)
        except ConfigError:
            effective_model = DEFAULT_MODEL

        typer.echo(f"cli-demo {__version__} (model: {effective_model})")
        raise typer.Exit()


@app.callback()
def app_callback(
    version: Annotated[
        bool,
        typer.Option(
            "--version",
            "-v",
            help="Show the CLI version and effective model.",
            callback=lambda value: _version_callback(value),
            is_eager=True,
        ),
    ] = False,
) -> None:
    """Root callback for global CLI options."""


@app.command()
def ask(
    prompt: Annotated[
        str,
        typer.Argument(help="Prompt sent to the model."),
    ],
    model: Annotated[
        str | None,
        typer.Option(
            "--model",
            help="Optional model override for this request.",
        ),
    ] = None,
    system: Annotated[
        str | None,
        typer.Option(
            "--system",
            "-s",
            help="Optional system instruction for the model.",
        ),
    ] = None,
) -> None:
    """Send one prompt and print one model response."""

    try:
        output_text = ask_model(
            prompt=prompt,
            model=model,
            system_prompt=system,
        )
    except ConfigError as exc:
        typer.secho(f"Error: {exc}", err=True, fg=typer.colors.RED)
        raise typer.Exit(code=1) from exc
    except EmptyPromptError as exc:
        typer.secho(f"Error: {exc}", err=True, fg=typer.colors.RED)
        raise typer.Exit(code=1) from exc
    except MissingAPIKeyError as exc:
        typer.secho(f"Error: {exc}", err=True, fg=typer.colors.RED)
        raise typer.Exit(code=1) from exc
    except EmptyResponseError as exc:
        typer.secho(f"Error: {exc}", err=True, fg=typer.colors.RED)
        raise typer.Exit(code=1) from exc
    except OpenAIRequestError as exc:
        typer.secho(f"Error: {exc}", err=True, fg=typer.colors.RED)
        raise typer.Exit(code=1) from exc
    except Exception as exc:  # pragma: no cover - defensive fallback
        typer.secho(f"Internal error: {exc}", err=True, fg=typer.colors.RED)
        raise typer.Exit(code=1) from exc

    typer.echo(output_text)


@app.command()
def update(
    dry_run: Annotated[
        bool,
        typer.Option(
            "--dry-run",
            "-n",
            help="Show the selected update command without running it.",
        ),
    ] = False,
) -> None:
    """Update this CLI with uv tool."""

    command = ["uv", "tool", "upgrade", PACKAGE_NAME]
    if not shutil.which(command[0]):
        typer.secho(
            f"Error: '{command[0]}' is not available on PATH.",
            err=True,
            fg=typer.colors.RED,
        )
        raise typer.Exit(code=1)

    typer.echo(f"Current version: {__version__}")
    typer.echo(f"Selected command: {' '.join(command)}")
    if dry_run:
        return

    completed = subprocess.run(command, check=False)
    if completed.returncode != 0:
        raise typer.Exit(code=completed.returncode)


@config_app.command("show")
def config_show() -> None:
    """Show current config values."""

    try:
        config = load_config()
        typer.echo(f"path: {get_config_path()}")
        typer.echo(f"api_key: {mask_secret(config.api_key)}")
        typer.echo(f"base_url: {config.base_url or '<not set>'}")
        typer.echo(f"model: {config.model or '<not set>'}")
        typer.echo(f"effective_model: {get_effective_model(config)}")
    except ConfigError as exc:
        typer.secho(f"Error: {exc}", err=True, fg=typer.colors.RED)
        raise typer.Exit(code=1) from exc


@config_app.command("path")
def config_path() -> None:
    """Print the config file path."""

    typer.echo(get_config_path())


@config_app.command("set")
def config_set(
    api_key: Annotated[
        str | None,
        typer.Option("--api-key", help="Set the OpenAI API key."),
    ] = None,
    base_url: Annotated[
        str | None,
        typer.Option("--base-url", help="Set the OpenAI base URL."),
    ] = None,
    model: Annotated[
        str | None,
        typer.Option("--model", help="Set the default model."),
    ] = None,
) -> None:
    """Update config values."""

    try:
        config = load_config()
        if api_key is None and base_url is None and model is None:
            typer.echo("Interactive config setup")
            typer.echo(f"path: {get_config_path()}")
            typer.echo("Leave blank to keep the current value. Type 'none' to clear a value.")

            api_key = _prompt_config_value(
                label="api_key",
                current_value=config.api_key,
                hide_input=True,
            )
            base_url = _prompt_config_value(
                label="base_url",
                current_value=config.base_url,
            )
            model = _prompt_config_value(
                label="model",
                current_value=config.model,
            )

        if api_key is not None:
            if api_key == CLEAR_SENTINEL:
                config.api_key = None
            else:
                config.api_key = normalize_optional_value(api_key, field_name="api_key")
        if base_url is not None:
            if base_url == CLEAR_SENTINEL:
                config.base_url = None
            else:
                config.base_url = normalize_optional_value(base_url, field_name="base_url")
        if model is not None:
            if model == CLEAR_SENTINEL:
                config.model = None
            else:
                config.model = normalize_optional_value(model, field_name="model")

        path = save_config(config)
        typer.echo(f"Updated config: {path}")
    except ConfigError as exc:
        typer.secho(f"Error: {exc}", err=True, fg=typer.colors.RED)
        raise typer.Exit(code=1) from exc


@config_app.command("unset")
def config_unset(
    field: Annotated[
        ConfigField,
        typer.Argument(help="Config field to clear."),
    ],
) -> None:
    """Clear one config value."""

    try:
        config = load_config()
        if field is ConfigField.API_KEY:
            config.api_key = None
        elif field is ConfigField.BASE_URL:
            config.base_url = None
        else:
            config.model = None

        path = save_config(config)
        typer.echo(f"Updated config: {path}")
    except ConfigError as exc:
        typer.secho(f"Error: {exc}", err=True, fg=typer.colors.RED)
        raise typer.Exit(code=1) from exc


def main() -> None:
    app()


def _prompt_config_value(
    *,
    label: str,
    current_value: str | None,
    hide_input: bool = False,
) -> str | None:
    display_current = mask_secret(current_value) if hide_input else (current_value or "<not set>")
    typer.echo(f"Current {label}: {display_current}")
    raw_value = typer.prompt(
        f"New {label}",
        default="",
        show_default=False,
        hide_input=hide_input and sys.stdin.isatty(),
    ).strip()

    if raw_value == "":
        return None
    if raw_value.lower() == "none":
        return CLEAR_SENTINEL
    return raw_value
