"""CLI error handling — maps SDK exceptions to exit codes.

The :func:`handle_errors` decorator wraps every CLI command so that
``SikaError`` subclasses are caught and translated into user-friendly
messages and correct exit codes.

Exit code mapping:

=====  ========================  ================================
Code   Meaning                   SDK Exception
=====  ========================  ================================
0      Success                   —
1      General error             ``SikaError`` (base)
2      Authentication error      ``AuthenticationError``
3      Forbidden / quota error   ``ForbiddenError``, ``QuotaExceededError``
4      Resource not found        ``NotFoundError``
5      Validation error          ``ValidationError``
130    Interrupted (Ctrl+C)      ``KeyboardInterrupt``
=====  ========================  ================================
"""

from __future__ import annotations

import functools
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from collections.abc import Callable

import typer

from sika._errors import (
    AuthenticationError,
    ForbiddenError,
    NotFoundError,
    QuotaExceededError,
    SikaError,
    ValidationError,
)
from sika_cli.output import get_error_console, print_json


def _exit_code_for(error: SikaError) -> int:
    """Return the CLI exit code for an SDK exception."""
    if isinstance(error, AuthenticationError):
        return 2
    if isinstance(error, (ForbiddenError, QuotaExceededError)):
        return 3
    if isinstance(error, NotFoundError):
        return 4
    if isinstance(error, ValidationError):
        return 5
    return 1


def _is_json_mode(ctx: typer.Context | None) -> bool:
    """Check whether ``--json`` mode is active."""
    if ctx is None:
        return False
    obj: dict[str, Any] = ctx.ensure_object(dict)
    return bool(obj.get("json", False))


def _print_error(
    error: SikaError,
    *,
    ctx: typer.Context | None = None,
) -> None:
    """Print an error message (text or JSON) to stderr."""
    if _is_json_mode(ctx):
        payload: dict[str, Any] = {
            "error": error.detail,
            "code": error.code or None,
        }
        if isinstance(error, QuotaExceededError) and error.upgrade_url:
            payload["upgrade_url"] = error.upgrade_url
        print_json(payload)
        return

    if ctx is not None:
        console = get_error_console(ctx)
    else:
        from rich.console import Console

        console = Console(stderr=True)

    message = f"Error: {error.detail}"
    if isinstance(error, QuotaExceededError) and error.upgrade_url:
        message += f"\nUpgrade your plan: {error.upgrade_url}"
    elif isinstance(error, AuthenticationError):
        message += "\nSet SIKA_API_KEY or run 'sika auth login'."

    console.print(message, style="red")


def handle_errors(
    func: Callable[..., Any],
) -> Callable[..., Any]:
    """Decorator that catches ``SikaError`` and exits with the right code.

    Wraps a Typer command function so that SDK exceptions are translated
    into user-friendly stderr messages and the process exits with the
    correct code.
    """

    @functools.wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        # Try to find the typer.Context from the arguments so we can
        # respect --json mode when formatting the error.
        ctx: typer.Context | None = kwargs.get("ctx")
        if ctx is None:
            for arg in args:
                if isinstance(arg, typer.Context):
                    ctx = arg
                    break

        try:
            return func(*args, **kwargs)
        except KeyboardInterrupt:
            raise typer.Exit(code=130) from None
        except SikaError as exc:
            _print_error(exc, ctx=ctx)
            raise typer.Exit(code=_exit_code_for(exc)) from None

    return wrapper
