"""Output formatters for CLI plugin responses.

Provides a protocol-based formatter system with entry-point discovery,
allowing third-party packages to register custom output formatters via
the ``graftpunk.formatters`` entry-point group.
"""

import csv
import importlib.metadata
import io
import json
from typing import Any, Protocol, runtime_checkable

from rich.console import Console
from rich.json import JSON
from rich.table import Table

from graftpunk import console as gp_console
from graftpunk.logging import get_logger
from graftpunk.plugins.cli_plugin import CommandResult
from graftpunk.plugins.output_config import OutputConfig, apply_column_filter, extract_view_data

LOG = get_logger(__name__)


@runtime_checkable
class OutputFormatter(Protocol):
    """Protocol for custom output formatters.

    Implementations must expose a ``name`` property (used as the ``--format``
    flag value) and a ``format`` method that renders data to a Rich console.
    """

    @property
    def name(self) -> str:
        """Formatter name used in --format flag."""
        ...

    def format(
        self,
        data: Any,
        console: Console,
        output_config: OutputConfig | None = None,
    ) -> None:
        """Format and print data to the console."""
        ...


# ---------------------------------------------------------------------------
# Built-in formatters
# ---------------------------------------------------------------------------


class JsonFormatter:
    """Output as formatted JSON with syntax highlighting."""

    name = "json"

    def format(
        self,
        data: Any,
        console: Console,
        output_config: OutputConfig | None = None,
    ) -> None:
        # JSON formatter ignores output_config - shows full data
        json_str = json.dumps(data, indent=2, default=str)
        console.print(JSON(json_str))


class TableFormatter:
    """Output as a rich table (for lists of dicts or single dicts)."""

    name = "table"

    def format(
        self,
        data: Any,
        console: Console,
        output_config: OutputConfig | None = None,
    ) -> None:
        # Apply output config if provided
        if output_config:
            view = output_config.get_default_view()
            if view:
                if view.path:
                    data = extract_view_data(data, view.path)
                if isinstance(data, list) and data and isinstance(data[0], dict):
                    data = apply_column_filter(data, view.columns)

        if isinstance(data, list) and data and isinstance(data[0], dict):
            # List of dicts - display as table with columns
            headers = list(data[0].keys())

            table = Table(header_style="bold cyan", border_style="dim")
            for header in headers:
                table.add_column(header)

            for row in data:
                table.add_row(*[str(row.get(h, "")) for h in headers])

            console.print(table)
        elif isinstance(data, dict):
            # Single dict - display as key/value pairs
            table = Table(show_header=False, border_style="dim")
            table.add_column("Key", style="cyan")
            table.add_column("Value")

            for key, value in data.items():
                if isinstance(value, (dict, list)):
                    value = json.dumps(value, default=str)
                table.add_row(str(key), str(value))

            console.print(table)
        else:
            # Fallback to JSON for other types
            JsonFormatter().format(data, console)


class RawFormatter:
    """Output raw string representation."""

    name = "raw"

    def format(
        self,
        data: Any,
        console: Console,
        output_config: OutputConfig | None = None,
    ) -> None:
        # Raw formatter ignores output_config
        if isinstance(data, str):
            console.print(data)
        else:
            console.print(json.dumps(data, default=str))


class CsvFormatter:
    """Output as CSV (comma-separated values)."""

    name = "csv"

    def format(
        self,
        data: Any,
        console: Console,
        output_config: OutputConfig | None = None,
    ) -> None:
        if isinstance(data, str):
            LOG.debug("csv_format_string_passthrough", length=len(data))
            console.print(data)
            return

        # Apply output config path extraction BEFORE type conversion
        if output_config:
            view = output_config.get_default_view()
            if view and view.path:
                extracted = extract_view_data(data, view.path)
                if extracted is not None:
                    data = extracted

        if isinstance(data, dict):
            data = [data]
        if not isinstance(data, list):
            LOG.warning(
                "csv_format_unsupported_type",
                data_type=type(data).__name__,
                fallback="raw",
            )
            RawFormatter().format(data, console)
            return
        if not data:
            LOG.debug("csv_format_empty_list")
            return
        if not all(isinstance(item, dict) for item in data):
            LOG.warning(
                "csv_format_unsupported_type",
                data_type="list[mixed]",
                fallback="raw",
            )
            RawFormatter().format(data, console)
            return

        # Apply column filter from output config
        if output_config:
            view = output_config.get_default_view()
            if view and view.columns:
                data = apply_column_filter(data, view.columns)

        if not data:
            LOG.debug("csv_format_empty_after_config")
            return

        # Collect headers as union of all row keys, preserving insertion order
        all_keys: dict[str, None] = {}
        for row in data:
            for k in row:
                all_keys.setdefault(k, None)
        headers = list(all_keys)
        buf = io.StringIO()
        writer = csv.writer(buf)
        writer.writerow(headers)
        for row in data:
            writer.writerow(
                [
                    json.dumps(v, default=str) if isinstance(v, (dict, list)) else str(v)
                    for v in (row.get(h, "") for h in headers)
                ]
            )
        console.print(buf.getvalue(), end="")


BUILTIN_FORMATTERS: dict[str, OutputFormatter] = {
    "json": JsonFormatter(),
    "table": TableFormatter(),
    "raw": RawFormatter(),
    "csv": CsvFormatter(),
}


# ---------------------------------------------------------------------------
# Discovery
# ---------------------------------------------------------------------------

FORMATTERS_GROUP = "graftpunk.formatters"


def discover_formatters() -> dict[str, OutputFormatter]:
    """Discover all formatters (built-in + entry points).

    Returns:
        Dictionary mapping formatter name to formatter instance.
        Built-in formatters are always present; entry-point formatters
        are merged on top (and may override built-ins).
    """
    formatters: dict[str, OutputFormatter] = dict(BUILTIN_FORMATTERS)
    for ep in importlib.metadata.entry_points(group=FORMATTERS_GROUP):
        try:
            cls = ep.load()
            instance = cls()
            if not isinstance(instance, OutputFormatter):
                LOG.warning(
                    "formatter_protocol_mismatch",
                    entry_point=ep.name,
                    type=type(instance).__name__,
                )
                continue
            formatters[instance.name] = instance
        except Exception:  # noqa: BLE001
            LOG.warning("formatter_load_failed", entry_point=ep.name, exc_info=True)
    return formatters


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def format_output(data: Any, format_type: str, console: Console) -> None:
    """Format and print command output.

    Args:
        data: Response data to format. May be a CommandResult (unwrapped
            automatically) or raw data.
        format_type: Formatter name (e.g. ``"json"``, ``"table"``, ``"raw"``).
        console: Rich console for output.
    """
    formatters = discover_formatters()
    formatter = formatters.get(format_type)
    if formatter is None:
        LOG.warning("unknown_format", format=format_type)
        gp_console.warn(
            f"Unknown format '{format_type}'. Falling back to JSON. "
            f"Available formats: {', '.join(sorted(formatters.keys()))}"
        )
        formatter = formatters["json"]  # fallback

    output_config = None

    # Unwrap CommandResult
    if isinstance(data, CommandResult):
        output_config = data.output_config  # Extract config
        if data.format_hint and data.format_hint in formatters and format_type == "json":
            # When format_type is the default ("json"), the plugin's format_hint
            # overrides it. Note: we cannot distinguish "user explicitly passed
            # --format json" from "default json" — in both cases the hint wins.
            formatter = formatters[data.format_hint]
        data = data.data

    formatter.format(data, console, output_config=output_config)
