"""
Module: output
Description: Dual-mode output context for human (Rich) and machine (JSON) CLI output.

Implements:
    - docs/cli/reference.md#global-options
    - docs/cli/agent-integration.md#json-envelope

See Also:
    - docs/cli/agent-integration.md (for agent integration patterns)
"""

from __future__ import annotations

import json
import sys
from contextlib import contextmanager
from typing import Any, Iterator, Literal

from rich.console import Console
from rich.status import Status


class _NoOpStatus:
    """No-op replacement for Rich Status in JSON mode."""

    def __enter__(self) -> _NoOpStatus:
        return self

    def __exit__(self, *args: object) -> None:
        pass

    def update(self, status: str) -> None:  # noqa: ARG002
        pass


class OutputContext:
    """Dual-mode output: Rich formatting for humans, structured JSON for agents.

    In JSON mode, ``print()`` calls are silently dropped. Only ``success()``
    and ``error()`` produce output, using the standard envelope:

        {"ok": true, "data": {...}}
        {"ok": false, "error": {"type": "...", "message": "...", "code": N}}
    """

    def __init__(self, mode: Literal["human", "json"], console: Console) -> None:
        self.mode = mode
        self.console = console

    @property
    def is_json(self) -> bool:
        return self.mode == "json"

    def success(self, data: dict[str, Any]) -> None:
        """Emit a success response.

        JSON mode: prints the envelope to stdout.
        Human mode: no-op (human output was already printed via ``print()``).
        """
        if self.is_json:
            self._write_json({"ok": True, "data": data})

    def error(self, error_type: str, message: str, code: int) -> None:
        """Emit an error response.

        JSON mode: prints the envelope to stderr.
        Human mode: prints a red error message.
        """
        if self.is_json:
            self._write_json(
                {"ok": False, "error": {"type": error_type, "message": message, "code": code}},
                file=sys.stderr,
            )
        else:
            self.console.print(f"[red]{message}[/red]")

    def print(self, *args: Any, **kwargs: Any) -> None:
        """Rich console passthrough. Suppressed in JSON mode."""
        if not self.is_json:
            self.console.print(*args, **kwargs)

    def print_json(self, data: Any) -> None:
        """Print raw JSON data. Works in both modes."""
        if self.is_json:
            self._write_json(data)
        else:
            self.console.print_json(json.dumps(data) if not isinstance(data, str) else data)

    @contextmanager
    def status(self, message: str) -> Iterator[Status | _NoOpStatus]:
        """Rich spinner in human mode; no-op in JSON mode."""
        if self.is_json:
            yield _NoOpStatus()
        else:
            with self.console.status(message) as s:
                yield s

    @staticmethod
    def _write_json(data: Any, file: Any = None) -> None:
        """Write compact JSON to stdout (or specified file)."""
        file = file or sys.stdout
        print(json.dumps(data, default=str), file=file)


def get_output(ctx: Any) -> OutputContext:
    """Extract OutputContext from a Typer context, with safe fallback.

    Works with both ``typer.Context`` and plain dicts. Returns a default
    human-mode context if the output object isn't available (e.g., in tests
    that don't set up the full callback chain).
    """
    from rich.console import Console as _Console

    obj: dict[str, Any] = {}
    if hasattr(ctx, "obj") and isinstance(ctx.obj, dict):
        obj = ctx.obj
    elif isinstance(ctx, dict):
        obj = ctx

    out = obj.get("output")
    if isinstance(out, OutputContext):
        return out
    return OutputContext(mode="human", console=_Console())
