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 .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.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": 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)}
