"""Shared CLI output formatting helpers.

Provides ``format_output`` to render data as JSON, YAML, plain text,
ASCII table, Rich console markup, or ANSI colour.  All serialisation
uses stable field names from the domain models' ``as_cli_dict`` methods.

The implementation now delegates to the Output Rendering Framework
(``cleveragents.cli.output``) while preserving the original public API
for backward compatibility.

Based on v3_spec.md implementation plan Stage A4b / G5b.render.
"""

from __future__ import annotations

import json
import time
from datetime import datetime
from enum import Enum, StrEnum
from io import StringIO
from typing import Any

import yaml
from rich.console import Console
from rich.table import Table

from cleveragents.cli.output.materializers import (
    ColorMaterializer,
    JsonMaterializer,
    PlainMaterializer,
    RichMaterializer,
    TableMaterializer,
    YamlMaterializer,
)
from cleveragents.cli.output.session import OutputSession


class OutputFormat(StrEnum):
    """Supported CLI output formats."""

    JSON = "json"
    YAML = "yaml"
    PLAIN = "plain"
    TABLE = "table"
    RICH = "rich"
    COLOR = "color"


def _serialize_value(value: Any) -> Any:
    """Recursively normalise values for JSON/YAML serialisation.

    * ``datetime`` -> ISO-8601 string
    * ``Enum``     -> ``.value``
    * ``dict``     -> recursed
    * ``list``     -> recursed
    * everything else -> unchanged
    """
    if isinstance(value, datetime):
        return value.isoformat()
    if isinstance(value, Enum):
        return value.value
    if isinstance(value, dict):
        return {k: _serialize_value(v) for k, v in value.items()}
    if isinstance(value, list):
        return [_serialize_value(item) for item in value]
    return value


def _format_json(data: dict[str, Any] | list[dict[str, Any]]) -> str:
    """Render data as indented JSON."""
    return json.dumps(_serialize_value(data), indent=2, default=str)


def _format_yaml(data: dict[str, Any] | list[dict[str, Any]]) -> str:
    """Render data as YAML."""
    return yaml.dump(
        _serialize_value(data),
        default_flow_style=False,
        sort_keys=False,
        allow_unicode=True,
    ).rstrip("\n")


def _format_plain(data: dict[str, Any] | list[dict[str, Any]]) -> str:
    """Render data as plain key: value lines (no Rich markup)."""
    if isinstance(data, list):
        parts: list[str] = []
        for idx, item in enumerate(data):
            if idx > 0:
                parts.append("---")
            parts.append(_format_plain_dict(item))
        return "\n".join(parts)
    return _format_plain_dict(data)


def _format_plain_dict(data: dict[str, Any]) -> str:
    """Render a single dict as plain text."""
    lines: list[str] = []
    for key, value in data.items():
        serialised = _serialize_value(value)
        if isinstance(serialised, dict):
            lines.append(f"{key}:")
            for k2, v2 in serialised.items():
                lines.append(f"  {k2}: {v2}")
        elif isinstance(serialised, list):
            lines.append(f"{key}:")
            for item in serialised:
                if isinstance(item, dict):
                    lines.append(f"  - {json.dumps(item, default=str)}")
                else:
                    lines.append(f"  - {item}")
        else:
            lines.append(f"{key}: {serialised}")
    return "\n".join(lines)


def _format_table(data: dict[str, Any] | list[dict[str, Any]]) -> str:
    """Render data as an ASCII table (no Rich styling)."""
    rows: list[dict[str, Any]] = [data] if isinstance(data, dict) else data

    if not rows:
        return "(empty)"

    # Collect all keys preserving order from first row
    columns: list[str] = list(rows[0].keys())
    for row in rows[1:]:
        for key in row:
            if key not in columns:
                columns.append(key)

    # Build Rich table and capture to string
    table = Table(show_header=True, show_edge=True)
    for col in columns:
        table.add_column(col)
    for row in rows:
        cells: list[str] = []
        for col in columns:
            val = row.get(col, "")
            serialised = _serialize_value(val)
            if isinstance(serialised, (dict, list)):
                cells.append(json.dumps(serialised, default=str))
            else:
                cells.append(str(serialised))
        table.add_row(*cells)

    buf = StringIO()
    console = Console(file=buf, width=200, no_color=True)
    console.print(table)
    return buf.getvalue().rstrip("\n")


def _redact_data(
    data: dict[str, Any] | list[dict[str, Any]],
) -> dict[str, Any] | list[dict[str, Any]]:
    """Apply secrets redaction to output data before rendering."""
    from cleveragents.shared.redaction import redact_dict

    if isinstance(data, list):
        return [redact_dict(item) if isinstance(item, dict) else item for item in data]
    return redact_dict(data)


_VALID_STATUSES: frozenset[str] = frozenset({"ok", "warn", "error"})


def _build_envelope(
    data: dict[str, Any] | list[dict[str, Any]],
    command: str,
    status: str,
    exit_code: int,
    duration_ms: int,
    messages: list[dict[str, Any]] | None,
) -> dict[str, Any]:
    """Build the spec-required JSON/YAML output envelope.

    The envelope wraps command-specific payload in the structure required
    by the specification (§Output Rendering Framework):

    .. code-block:: json

        {
          "command": "<command that was run>",
          "status": "ok",
          "exit_code": 0,
          "data": { ... },
          "timing": { "duration_ms": 123 },
          "messages": [{ "level": "ok", "text": "..." }]
        }

    Parameters
    ----------
    data:
        The command-specific payload to wrap.
    command:
        The CLI command string that produced this output.
    status:
        Envelope status: must be one of ``"ok"``, ``"warn"``, or ``"error"``.
    exit_code:
        Envelope exit code; must be a non-negative integer.
    duration_ms:
        Elapsed time in milliseconds.
    messages:
        Optional list of message dicts; each must have ``"level"`` and
        ``"text"`` keys.  If *None*, a default single-entry list is generated.

    Raises
    ------
    ValueError
        If *status* is not one of the allowed values, *exit_code* is negative,
        or any entry in *messages* is missing the required ``"level"`` or
        ``"text"`` keys.
    """
    if status not in _VALID_STATUSES:
        raise ValueError(f"status must be one of {_VALID_STATUSES!r}, got {status!r}")
    if exit_code < 0:
        raise ValueError(f"exit_code must be non-negative, got {exit_code!r}")
    if messages is not None:
        for msg in messages:
            if "level" not in msg or "text" not in msg:
                raise ValueError(
                    f"Each message must have 'level' and 'text' keys, got {msg!r}"
                )
    if messages is None:
        messages = [
            {"level": status, "text": f"{command} completed" if command else "ok"}
        ]
    return {
        "command": command,
        "status": status,
        "exit_code": exit_code,
        "data": data,
        "timing": {"duration_ms": duration_ms},
        "messages": messages,
    }


def format_output(
    data: dict[str, Any] | list[dict[str, Any]],
    format_type: str,
    *,
    command: str = "",
    status: str = "ok",
    exit_code: int = 0,
    messages: list[dict[str, Any]] | None = None,
) -> str:
    """Format *data* according to *format_type*.

    All output is passed through :func:`redact_dict` before
    rendering so that sensitive values are masked unless the
    global ``show_secrets`` flag is enabled.

    For machine-readable formats (json, yaml) the output is wrapped in
    the spec-required envelope (``command``, ``status``, ``exit_code``,
    ``data``, ``timing``, ``messages``) before serialisation.  Plain
    format renders the raw data without an envelope.

    For machine-readable formats (json, yaml, plain) the output is
    written directly to ``sys.stdout`` to avoid Rich ``console.print``
    line-wrapping which can introduce literal newlines inside JSON
    string values.  An empty string is returned so the caller's
    ``console.print(result)`` simply emits a trailing newline.

    Parameters
    ----------
    data:
        A single dict or a list of dicts (from ``model.as_cli_dict()``).
    format_type:
        One of ``json``, ``yaml``, ``plain``, ``table``, ``rich``, ``color``.
    command:
        The CLI command string that produced this output (e.g.
        ``"agents actor list"``).  Used in the JSON/YAML envelope.
    status:
        Envelope status field: ``"ok"``, ``"warn"``, or ``"error"``.
    exit_code:
        Envelope exit_code field (integer, typically 0 for success).
    messages:
        Envelope messages array.  If *None*, a default single-entry list
        is generated from *command* and *status*.

    Returns
    -------
    str
        The rendered string.  For machine-readable formats the output
        is written directly to stdout and an empty string is returned.
    """
    import sys

    t_start = time.monotonic()
    safe_data = _redact_data(data)
    fmt = format_type.lower()

    # Machine-readable formats: write directly to stdout to avoid
    # Rich console.print() line-wrapping artefacts.
    if fmt in (
        OutputFormat.JSON.value,
        OutputFormat.YAML.value,
        OutputFormat.PLAIN.value,
    ):
        if fmt == OutputFormat.JSON.value:
            duration_ms = int((time.monotonic() - t_start) * 1000)
            envelope = _build_envelope(
                safe_data, command, status, exit_code, duration_ms, messages
            )
            rendered = _format_json(envelope)
        elif fmt == OutputFormat.YAML.value:
            duration_ms = int((time.monotonic() - t_start) * 1000)
            envelope = _build_envelope(
                safe_data, command, status, exit_code, duration_ms, messages
            )
            rendered = _format_yaml(envelope)
        else:
            rendered = _format_plain(safe_data)
        sys.stdout.write(rendered + "\n")
        sys.stdout.flush()
        return ""

    if fmt == OutputFormat.TABLE.value:
        return _format_table(safe_data)
    if fmt == OutputFormat.COLOR.value:
        return _format_plain(safe_data)
    # ``rich`` and any unknown value fall back to JSON
    return _format_json(safe_data)


def format_output_session(
    data: dict[str, Any] | list[dict[str, Any]],
    format_type: str,
) -> str:
    """Format *data* via the ``OutputSession`` framework.

    This is the new entry-point that fully utilises the output rendering
    framework.  It converts a dict/list payload into panels in an
    ``OutputSession``, applies the requested materialiser, and returns
    the rendered string.
    """
    safe_data = _redact_data(data)
    strategy_map: dict[str, type] = {
        "rich": RichMaterializer,
        "color": ColorMaterializer,
        "table": TableMaterializer,
        "plain": PlainMaterializer,
        "json": JsonMaterializer,
        "yaml": YamlMaterializer,
    }

    fmt = format_type.lower()
    strategy_cls = strategy_map.get(fmt, PlainMaterializer)
    strategy = strategy_cls()

    with OutputSession(
        format=fmt, command="format_output", strategy=strategy
    ) as session:
        if isinstance(safe_data, list):
            for idx, item in enumerate(safe_data):
                if isinstance(item, dict):
                    panel = session.panel(f"Item {idx + 1}")
                    for key, val in item.items():
                        panel.set_entry(key, str(_serialize_value(val)))
                    panel.close()
        elif isinstance(safe_data, dict):
            panel = session.panel("Output")
            for key, val in safe_data.items():
                panel.set_entry(key, str(_serialize_value(val)))
            panel.close()

    return strategy.get_output()
