"""Typer entry point for the Context Platform CLI."""

from __future__ import annotations

import os
import sys
from importlib import metadata
from io import StringIO

import click
import httpx
import typer
from rich.console import Console

try:
    from shared import (
        AuthenticationError,
        BackendRequestError,
        ConfigError,
        ConflictError,
        NotFoundError,
        PreconditionFailedError,
        SharedError,
        ValidationFailure,
    )
except ImportError:
    from cli._vendor.shared import (
        AuthenticationError,
        BackendRequestError,
        ConfigError,
        ConflictError,
        NotFoundError,
        PreconditionFailedError,
        SharedError,
        ValidationFailure,
    )

from .clients import ClientFactory
from .commands import auth, config, items, projects, update
from .config import ConfigStore
from .context import CLIContext
from .exceptions import CLIError, ConfigurationError, KeyringFailure
from .options import OutputMode
from .output import (
    ERROR_ABORTED,
    ERROR_AUTHENTICATION_ERROR,
    ERROR_BACKEND_ERROR,
    ERROR_CLI_ERROR,
    ERROR_CLICK_ERROR,
    ERROR_CONFIG_ERROR,
    ERROR_CONFIGURATION_ERROR,
    ERROR_CONFLICT_ERROR,
    ERROR_CONNECTION_ERROR,
    ERROR_HTTP_ERROR,
    ERROR_KEYRING_ERROR,
    ERROR_NOT_FOUND,
    ERROR_PRECONDITION_FAILED,
    ERROR_TIMEOUT_ERROR,
    ERROR_UNEXPECTED_ERROR,
    ERROR_USAGE_ERROR,
    ERROR_VALIDATION_ERROR,
    format_json_error,
    print_json_error,
)
from .styles import apply_help_styles
from .update_checker import run_startup_update_check

# Global output mode state - set by callback, read by error handlers
_output_mode: OutputMode = OutputMode.TEXT

# Apply before app creation so Typer picks up updated Rich help styles.
apply_help_styles()

app = typer.Typer(help="Interact with the Context Platform.", no_args_is_help=True)
console = Console()
_context = CLIContext(console=console, config_store=ConfigStore(), client_factory=ClientFactory())


def _get_version() -> str:
    """Return the installed CLI package version or dev fallback."""
    try:
        return metadata.version("ctxme")
    except metadata.PackageNotFoundError:
        return "dev"


def _version_callback(value: bool) -> None:
    if value:
        console.print(f"ctxme {_get_version()}")
        raise typer.Exit()


def _output_callback(value: OutputMode) -> OutputMode:
    """Eager callback to set output mode before command parsing.

    This is critical for handling JSON output mode for UsageErrors that occur
    during argument parsing (before the main callback runs).
    """
    global _output_mode  # noqa: PLW0603
    _output_mode = value
    return value


def _is_debug() -> bool:
    """Return True when debug output should be printed."""
    value = os.getenv("CTXME_DEBUG", "").strip().lower()
    return value in {"1", "true", "yes"}


def _shared_error_message(exc: SharedError) -> str:
    """Return a user-friendly message for shared exceptions."""
    if isinstance(exc, AuthenticationError):
        return "Authentication failed. Run 'ctxme auth login'."
    if isinstance(exc, NotFoundError):
        return f"Item/Project not found: {exc}"
    if isinstance(exc, ValidationFailure):
        return f"Validation error: {exc}"
    if isinstance(exc, BackendRequestError):
        return f"API error: {exc}"
    if isinstance(exc, ConfigError):
        return f"Configuration error: {exc}"
    return str(exc)


def _http_error_message(exc: httpx.HTTPError) -> str:
    """Return a user-friendly message for HTTP exceptions."""
    if isinstance(exc, httpx.ConnectError):
        return "Unable to connect to API server. Check your network connection and API URL."
    if isinstance(exc, httpx.TimeoutException):
        return "Request timed out. Try again."
    if isinstance(exc, httpx.HTTPStatusError):
        status = exc.response.status_code if exc.response else "unknown"
        return f"Server error ({status}). Try again later."
    return "Network error. Try again."


def _print_shared_debug(exc: SharedError, *, console: Console) -> None:
    """Print extra request details for shared errors when available."""
    request_url = getattr(exc, "request_url", None)
    status_code = getattr(exc, "status_code", None)
    request_id = getattr(exc, "request_id", None)
    response_body = getattr(exc, "response_body", None)
    if request_url:
        console.print(f"[yellow]Request URL:[/yellow] {request_url}")
    if status_code is not None:
        console.print(f"[yellow]Status:[/yellow] {status_code}")
    if request_id:
        console.print(f"[yellow]Request ID:[/yellow] {request_id}")
    if response_body:
        console.print("[yellow]Response body:[/yellow]")
        console.print(response_body)


def _print_http_debug(exc: httpx.HTTPError, *, console: Console) -> None:
    """Print HTTP request/response details for debugging."""
    request = getattr(exc, "request", None)
    response = getattr(exc, "response", None)
    if request:
        console.print(f"[yellow]Request:[/yellow] {request.method} {request.url}")
    if response:
        console.print(f"[yellow]Status:[/yellow] {response.status_code}")
        request_id = response.headers.get("x-request-id")
        if request_id:
            console.print(f"[yellow]Request ID:[/yellow] {request_id}")
        if response.text:
            console.print("[yellow]Response body:[/yellow]")
            console.print(response.text)


@app.callback(invoke_without_command=True)
def _inject_context(
    ctx: typer.Context,
    version: bool = typer.Option(
        False,
        "--version",
        is_eager=True,
        callback=_version_callback,
        help="Show CLI version and exit.",
    ),
    debug: bool = typer.Option(False, "--debug", hidden=True),
    output: OutputMode = typer.Option(
        OutputMode.TEXT,
        "--output",
        "-o",
        is_eager=True,
        callback=_output_callback,
        help="Output format: 'text' (default) or 'json' for machine-parseable output.",
    ),
) -> None:
    """Attach shared context to Typer's state."""
    _ = version
    _ = output  # Already processed by eager callback
    if debug:
        os.environ["CTXME_DEBUG"] = "1"
    ctx.obj = _context

    # Run periodic update check (silent on errors)
    try:
        invoked_command = ctx.invoked_subcommand
        run_startup_update_check(_get_version(), invoked_command)
    except Exception:  # noqa: S110 - intentionally silent, update check must not crash CLI
        pass


def _is_json_mode() -> bool:
    """Return True when JSON output mode is active."""
    return _output_mode == OutputMode.JSON


def _json_error_from_shared(exc: SharedError) -> None:
    """Emit JSON error for SharedError exceptions and exit.

    Maps exception types to error codes per issue #260 specification.
    """
    # Map exception type to error code per issue #260
    if isinstance(exc, AuthenticationError):
        error_code = ERROR_AUTHENTICATION_ERROR
        hint = "Run 'ctx auth login' to authenticate."
    elif isinstance(exc, NotFoundError):
        error_code = ERROR_NOT_FOUND
        # Use explicit resource_type context when available (per issue #260)
        resource_type = getattr(exc, "resource_type", None)
        if resource_type == "project":
            hint = "Run 'ctx projects list' to see available projects."
        elif resource_type in ("item", "file"):
            hint = "Run 'ctx items list --project <project>' to see available items."
        else:
            # Fallback for HTTP 404 without resource_type context
            hint = "Run 'ctx projects list' or 'ctx items list' to see available resources."
    elif isinstance(exc, ValidationFailure):
        error_code = ERROR_VALIDATION_ERROR
        hint = None
    elif isinstance(exc, ConflictError):
        error_code = ERROR_CONFLICT_ERROR
        hint = None
    elif isinstance(exc, PreconditionFailedError):
        error_code = ERROR_PRECONDITION_FAILED
        hint = "The resource was modified. Fetch the latest version and retry."
    elif isinstance(exc, ConfigError):
        error_code = ERROR_CONFIG_ERROR
        hint = "Run 'ctx config show' to check configuration."
    elif isinstance(exc, BackendRequestError):
        # Generic BackendRequestError (not a subclass)
        error_code = ERROR_BACKEND_ERROR
        hint = None
    else:
        error_code = ERROR_UNEXPECTED_ERROR
        hint = None

    # Extract optional fields from BackendRequestError
    request_id = getattr(exc, "request_id", None)
    status_code = getattr(exc, "status_code", None)

    # Build context with resource info for NotFoundError
    context: dict[str, str] = {}
    resource_type = getattr(exc, "resource_type", None)
    resource_key = getattr(exc, "resource_key", None)
    if resource_type:
        context["resource_type"] = resource_type
    if resource_key:
        context["resource_key"] = resource_key

    json_err = format_json_error(
        error=error_code,
        message=str(exc),
        hint=hint,
        request_id=request_id,
        status_code=status_code,
        context=context if context else None,
    )
    print_json_error(json_err)


def _json_error_from_http(exc: httpx.HTTPError) -> None:
    """Emit JSON error for httpx.HTTPError exceptions and exit.

    Maps httpx exception types to error codes per issue #260:
    - httpx.ConnectError -> connection_error
    - httpx.TimeoutException -> timeout_error
    - httpx.HTTPStatusError -> http_error
    """
    # Extract context from the exception
    status_code: int | None = None
    request_id: str | None = None
    context: dict[str, str] = {}

    # Map specific httpx exceptions to error codes per issue #260
    if isinstance(exc, httpx.ConnectError):
        error_code = ERROR_CONNECTION_ERROR
        hint = "Check your network connection and API URL."
    elif isinstance(exc, httpx.TimeoutException):
        error_code = ERROR_TIMEOUT_ERROR
        hint = None
    elif isinstance(exc, httpx.HTTPStatusError):
        error_code = ERROR_HTTP_ERROR
        hint = None
        if exc.response:
            status_code = exc.response.status_code
            request_id = exc.response.headers.get("x-request-id")
    else:
        # Other httpx errors (e.g., httpx.RequestError subclasses)
        error_code = ERROR_CONNECTION_ERROR
        hint = None

    # Try to get request URL safely - httpx exceptions may raise RuntimeError
    try:
        request = exc.request if hasattr(exc, "request") else None
        if request:
            context["request_url"] = str(request.url)
    except RuntimeError:
        # The .request property may raise if not set
        pass

    json_err = format_json_error(
        error=error_code,
        message=_http_error_message(exc),
        hint=hint,
        request_id=request_id,
        status_code=status_code,
        context=context if context else None,
    )
    print_json_error(json_err)


def _json_error_from_cli(exc: CLIError) -> None:
    """Emit JSON error for CLIError exceptions and exit.

    Maps CLI exception types to error codes per issue #260:
    - ConfigurationError -> configuration_error
    - KeyringFailure -> keyring_error
    - CLIError (base) -> cli_error
    """
    if isinstance(exc, ConfigurationError):
        error_code = ERROR_CONFIGURATION_ERROR
        hint = None
    elif isinstance(exc, KeyringFailure):
        error_code = ERROR_KEYRING_ERROR
        hint = None
    else:
        error_code = ERROR_CLI_ERROR
        hint = None

    json_err = format_json_error(
        error=error_code,
        message=str(exc),
        hint=hint,
    )
    print_json_error(json_err)


def _json_error_from_usage(exc: click.UsageError) -> None:
    """Emit JSON error for click.UsageError exceptions and exit."""
    # Include command context if available
    context: dict[str, str] = {}
    if exc.ctx is not None:
        info_name = exc.ctx.info_name
        if info_name:
            context["command"] = info_name

    json_err = format_json_error(
        error=ERROR_USAGE_ERROR,
        message=exc.format_message(),
        hint="Run with --help for usage information.",
        context=context if context else None,
    )
    print_json_error(json_err)


def _json_error_from_click(exc: click.ClickException) -> None:
    """Emit JSON error for click.ClickException exceptions and exit.

    Per issue #260, click.ClickException maps to 'click_error'.
    """
    json_err = format_json_error(
        error=ERROR_CLICK_ERROR,
        message=exc.format_message(),
    )
    print_json_error(json_err)


def _json_error_aborted() -> None:
    """Emit JSON error for click.Abort and exit."""
    json_err = format_json_error(
        error=ERROR_ABORTED,
        message="Operation was aborted.",
    )
    print_json_error(json_err)


def _json_error_unexpected() -> None:
    """Emit JSON error for unexpected exceptions and exit.

    Per issue #260, generic exceptions map to 'unexpected_error'.
    """
    json_err = format_json_error(
        error=ERROR_UNEXPECTED_ERROR,
        message="An unexpected error occurred.",
        hint="Run with CTXME_DEBUG=1 for details.",
    )
    print_json_error(json_err)


def _register_commands() -> None:
    config.register(app)
    auth.register(app)
    items.register(app)
    projects.register(app)
    update.register(app)


_register_commands()


def main() -> None:
    """Console script entry point.

    Error handling in JSON mode (--output json):
    - Errors emit structured JSON to stdout (exit code indicates failure)
    - KeyboardInterrupt exits silently with code 130 (no JSON output)
    - click.Abort emits JSON with error: "aborted" and exits with code 1
    - Click/Typer styled output is suppressed in JSON mode
    """
    err_console = Console(stderr=True)
    try:
        rv = app(prog_name="ctxme", standalone_mode=False)
        # With standalone_mode=False, typer.Exit returns exit code instead of raising
        if isinstance(rv, int):
            raise SystemExit(rv)
    except KeyboardInterrupt:
        # Ctrl+C exits silently with code 130 (no JSON output per issue #260)
        raise SystemExit(130)
    except SystemExit:
        raise
    except typer.Exit as exc:
        raise SystemExit(exc.exit_code)
    except click.Abort:
        # click.Abort emits JSON in JSON mode, text otherwise
        if _is_json_mode():
            _json_error_aborted()
        else:
            click.echo("Aborted!", err=True)
        raise SystemExit(1)
    except click.UsageError as exc:
        if _is_json_mode():
            _json_error_from_usage(exc)
        else:
            # Show original error with styling
            exc.show()
            # If "Missing command.", append help text to guide the user
            if exc.message == "Missing command." and exc.ctx is not None:
                # Typer's get_help() prints to stdout as a side effect.
                # Capture it and redirect to stderr for consistency.
                old_stdout = sys.stdout
                sys.stdout = captured = StringIO()
                try:
                    exc.ctx.get_help()
                finally:
                    sys.stdout = old_stdout
                help_output = captured.getvalue()
                if help_output:
                    err_console.print()  # blank line separator
                    # Print raw captured output to stderr (already formatted by Typer)
                    sys.stderr.write(help_output)
        raise SystemExit(exc.exit_code)
    except click.ClickException as exc:
        if _is_json_mode():
            _json_error_from_click(exc)
        else:
            exc.show()
        raise SystemExit(exc.exit_code)
    except CLIError as exc:
        if _is_json_mode():
            _json_error_from_cli(exc)
        else:
            err_console.print(f"[red]Error:[/red] {exc}")
            if _is_debug():
                err_console.print_exception()
        raise SystemExit(1)
    except SharedError as exc:
        if _is_json_mode():
            _json_error_from_shared(exc)
        else:
            err_console.print(f"[red]Error:[/red] {_shared_error_message(exc)}")
            if _is_debug():
                _print_shared_debug(exc, console=err_console)
                err_console.print_exception()
        raise SystemExit(1)
    except httpx.HTTPError as exc:
        if _is_json_mode():
            _json_error_from_http(exc)
        else:
            err_console.print(f"[red]Error:[/red] {_http_error_message(exc)}")
            if _is_debug():
                _print_http_debug(exc, console=err_console)
                err_console.print_exception()
        raise SystemExit(1)
    except Exception:
        if _is_json_mode():
            _json_error_unexpected()
        else:
            err_console.print("[red]Error:[/red] An unexpected error occurred.")
            if _is_debug():
                err_console.print_exception()
            else:
                err_console.print("Run with CTXME_DEBUG=1 for details.")
        raise SystemExit(1)


if __name__ == "__main__":
    main()
