from __future__ import annotations

from collections.abc import Callable
from datetime import datetime, timezone
import time
from typing import Any

import orjson
import typer

from .config import OutputMode
from .errors import TerminalError
from .render import COMPACT_SKIP_KEYS, compact_dict
from .serialize import to_jsonable
from .state import TerminalState


def emit_payload(
    state: TerminalState,
    command: str,
    filters: dict[str, Any],
    data: Any,
    rich_renderer: Callable[[TerminalState, Any], None],
    ndjson_items: Callable[[Any], list[Any]] | None = None,
) -> None:
    base = {
        "command": command,
        "fetched_at": datetime.now(timezone.utc).isoformat(),
        "filters": to_jsonable(filters),
    }
    if state.config.output_mode is OutputMode.RICH:
        rich_renderer(state, data)
        return
    payload = _json_payload(base, data)
    if state.config.compact:
        payload = _compact_payload(payload)
    if state.config.output_mode is OutputMode.JSON:
        typer.echo(orjson.dumps(payload, option=orjson.OPT_INDENT_2).decode())
        return
    items = ndjson_items(data) if ndjson_items else _default_ndjson_items(data)
    for item in items:
        line = {
            "command": command,
            "fetched_at": base["fetched_at"],
            "filters": base["filters"],
            "item": _compact_value(to_jsonable(item)) if state.config.compact else to_jsonable(item),
        }
        typer.echo(orjson.dumps(line).decode())


def watch_loop(interval_seconds: float, runner: Callable[[], None], state: TerminalState) -> None:
    while True:
        if state.config.output_mode is OutputMode.RICH:
            state.console.clear()
        runner()
        try:
            time.sleep(interval_seconds)
        except KeyboardInterrupt:
            raise typer.Exit(code=0) from None


def run_command(
    state: TerminalState,
    command: str,
    filters: dict[str, Any],
    fetcher: Callable[[], Any],
    renderer: Callable[[TerminalState, Any], None],
    ndjson_items: Callable[[Any], list[Any]] | None = None,
    watch: float | None = None,
) -> None:
    def once() -> None:
        try:
            data = fetcher()
        except TerminalError as exc:
            state.console.print(f"[red]Error:[/red] {exc}")
            raise typer.Exit(exc.exit_code) from exc
        emit_payload(state, command, filters, data, renderer, ndjson_items)

    if watch and watch > 0:
        watch_loop(watch, once, state)
        return
    once()


def _default_ndjson_items(data: Any) -> list[Any]:
    if isinstance(data, list):
        return data
    if hasattr(data, "data") and hasattr(data, "meta"):
        return _default_ndjson_items(getattr(data, "data"))
    return [data]


def _json_payload(base: dict[str, Any], data: Any) -> dict[str, Any]:
    if hasattr(data, "data") and hasattr(data, "meta") and hasattr(data, "warnings"):
        return {
            **base,
            "data": to_jsonable(getattr(data, "data")),
            "meta": to_jsonable(getattr(data, "meta")),
            "warnings": to_jsonable(getattr(data, "warnings")),
        }
    return {**base, "data": to_jsonable(data)}


def _compact_payload(payload: dict[str, Any]) -> dict[str, Any]:
    return {
        "command": payload["command"],
        "fetched_at": payload["fetched_at"],
        "filters": _compact_value(payload["filters"]),
        "data": _compact_value(payload["data"]),
        **({"meta": _compact_value(payload["meta"])} if "meta" in payload else {}),
        **({"warnings": _compact_value(payload["warnings"])} if "warnings" in payload else {}),
    }


def _compact_value(value: Any) -> Any:
    if isinstance(value, list):
        return [_compact_value(item) for item in value[:6]]
    if not isinstance(value, dict):
        return value
    if set(value).issubset({"command", "fetched_at", "filters", "data", "meta", "warnings", "item"}):
        return {key: _compact_value(item) for key, item in value.items()}
    scalar_values = {
        key: item
        for key, item in value.items()
        if key not in COMPACT_SKIP_KEYS and not isinstance(item, (dict, list))
    }
    nested_values = {
        key: item for key, item in value.items() if key not in COMPACT_SKIP_KEYS and isinstance(item, (dict, list))
    }
    compacted: dict[str, Any] = compact_dict(scalar_values, max_items=6)
    for key, item in nested_values.items():
        if len(compacted) >= 6:
            break
        compacted[key] = _compact_value(item)
    if compacted:
        return compacted
    return compact_dict(value, max_items=6)
