"""Terminal UI helpers for the interactive CLI."""

from __future__ import annotations

from dataclasses import dataclass
import json
from typing import Callable

from prompt_toolkit import PromptSession
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.history import FileHistory, InMemoryHistory
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.shortcuts import clear
from prompt_toolkit.styles import Style
from prompt_toolkit.utils import get_cwidth
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.rule import Rule
from rich.status import Status
from rich.table import Table
from rich.text import Text

from memnixa.config import AppConfig, get_cli_history_path
from memnixa.i18n import t


PROMPT_STYLE = Style.from_dict(
    {
        "prompt": "bold #7aa2f7",
        "prompt.label": "bold #c0caf5",
        "prompt.border": "#7aa2f7",
        "bottom": "fg:#9aa5ce bg:#1a1b26",
    }
)


@dataclass(slots=True)
class TerminalUI:
    """Rich + prompt_toolkit based terminal interface."""

    via: str
    config: AppConfig | None
    gateway_url: str | None
    console: Console
    session: PromptSession
    active_provider_id: str
    active_provider_label: str
    active_model_name: str

    @classmethod
    def create(
        cls,
        *,
        via: str,
        config: AppConfig | None,
        gateway_url: str | None,
    ) -> "TerminalUI":
        history_path = get_cli_history_path()
        try:
            history_path.parent.mkdir(parents=True, exist_ok=True)
            history = FileHistory(str(history_path))
        except OSError:
            history = InMemoryHistory()
        session = PromptSession(
            multiline=True,
            wrap_lines=True,
            history=history,
            key_bindings=_build_key_bindings(),
            style=PROMPT_STYLE,
            reserve_space_for_menu=0,
        )
        if config is not None:
            default_provider = config.get_provider_config()
            provider_id = default_provider.id
            provider_label = default_provider.label or default_provider.preset
            model_name = default_provider.model or "(default)"
        else:
            provider_id = "?"
            provider_label = "gateway"
            model_name = "(remote)"
        return cls(
            via=via,
            config=config,
            gateway_url=gateway_url,
            console=Console(),
            session=session,
            active_provider_id=provider_id,
            active_provider_label=provider_label,
            active_model_name=model_name,
        )

    def print_welcome(self) -> None:
        """Render the CLI header."""
        width = self._terminal_width()
        self.console.print(
            Rule(
                title=self._build_header_title(width),
                style="#414868",
            )
        )
        details = [
            t("ui.mode", value=self.via),
            t(
                "ui.provider",
                id=self.active_provider_id,
                label=self.active_provider_label,
            )
            if self.config
            else t("ui.provider_gateway"),
            t("ui.model", value=self.active_model_name),
        ]
        if self.gateway_url:
            details.append(t("ui.gateway", value=self.gateway_url))
        self.console.print(
            Panel(
                "\n".join(details) + f"\n{t('ui.welcome')}" + f"\n{t('ui.model_help')}",
                title=f"[bold #c0caf5]{t('ui.session')}[/bold #c0caf5]",
                border_style="#7aa2f7",
                padding=(0, 1),
                expand=True,
            )
        )

    def prompt(self) -> str:
        """Read one user message."""
        return self.session.prompt(
            HTML(
                "<prompt.border>┃</prompt.border> "
                f"<prompt.label>{t('ui.user')}</prompt.label> "
                f"<prompt>{t('ui.prompt.input')}</prompt>\n"
                "<prompt.border>╰─➤ </prompt.border>"
            ),
            bottom_toolbar=self._bottom_toolbar,
            prompt_continuation=lambda width, line_number, is_soft_wrap: (
                " " * max(width - 3, 0) + "│ "
            ),
        )

    def busy(self) -> Status:
        """Create the transient busy indicator."""
        spinner = "dots" if self.via != "gateway" else "line"
        label = t("ui.busy.local") if self.via != "gateway" else t("ui.busy.gateway")
        return self.console.status(f"[#9ece6a]{label}[/#9ece6a]", spinner=spinner)

    def render_user_message(self, user_message: str) -> None:
        """Render the user message panel."""
        self.console.print()
        self.console.print(
            Panel(
                Text(user_message.strip() or "(empty)", style="#c0caf5"),
                title=f"[bold #f7768e]{t('ui.user')}[/bold #f7768e]",
                border_style="#414868",
                padding=(0, 1),
            )
        )

    def render_stream_event(self, event: dict) -> None:
        """Render one runtime event immediately."""
        rendered = _render_runtime_event(
            event,
            width=self._terminal_width(),
            height=self._terminal_height(),
        )
        if rendered is not None:
            self.console.print(rendered)

    def render_turn(
        self,
        user_message: str,
        result: dict,
        *,
        render_user_message: bool = True,
        render_tool_events: bool = True,
        render_runtime_events: bool = True,
    ) -> None:
        """Render one full turn."""
        self._apply_result_state(result)
        if render_user_message:
            self.render_user_message(user_message)

        if render_tool_events:
            for index, event in enumerate(result.get("tool_events") or [], start=1):
                self.console.print(
                    _render_tool_event(
                        event,
                        index,
                        width=self._terminal_width(),
                        height=self._terminal_height(),
                    )
                )

        if render_runtime_events:
            for event in result.get("runtime_events") or []:
                rendered = _render_runtime_event(
                    event,
                    width=self._terminal_width(),
                    height=self._terminal_height(),
                )
                if rendered is not None:
                    self.console.print(rendered)

        reply_text = str(result.get("reply") or "")
        self.console.print(
            Panel(
                Markdown(reply_text or "(no reply)"),
                title=f"[bold #7aa2f7]{t('ui.assistant')}[/bold #7aa2f7]",
                subtitle=_build_reply_subtitle(
                    result,
                    width=self._terminal_width(),
                ),
                subtitle_align="right",
                border_style="#7aa2f7",
                padding=(0, 1),
            )
        )

        meta = result.get("meta") or {}
        if meta.get("fallback") == "local":
            self.console.print(
                Text(
                    t("ui.fallback", error=meta.get("gateway_error")),
                    style="#e0af68",
                )
            )

    def _bottom_toolbar(self) -> HTML:
        return HTML(f"<bottom>{self._toolbar_text()}</bottom>")

    def _apply_result_state(self, result: dict) -> None:
        provider_id = result.get("provider_id")
        provider_label = result.get("provider_label")
        model_name = result.get("model_name")
        if provider_id:
            self.active_provider_id = str(provider_id)
        if provider_label:
            self.active_provider_label = str(provider_label)
        if model_name:
            self.active_model_name = str(model_name)

    def _terminal_width(self) -> int:
        return max(self.console.size.width, 20)

    def _terminal_height(self) -> int:
        return max(self.console.size.height, 8)

    def _build_header_title(self, width: int | None = None) -> Text:
        resolved_width = width or self._terminal_width()
        title = Text("Memnixa", style="bold #7aa2f7")
        subtitle = t("ui.title")
        if resolved_width >= 40:
            title.append(f" {subtitle}", style="#9aa5ce")
        return title

    def _toolbar_text(self, width: int | None = None) -> str:
        resolved_width = max(width or self._terminal_width(), 20)
        gateway = self.gateway_url or "-"
        brand = "Memnixa"
        mode = t("ui.mode", value=self.via)
        provider = t(
            "ui.provider",
            id=self.active_provider_id,
            label=self.active_provider_label,
        )
        model = t("ui.model", value=self.active_model_name)
        gateway_text = t("ui.gateway", value=gateway)
        exit_hint = t("ui.exit_hint_short")
        candidates = [
            [brand, mode, provider, model, gateway_text, exit_hint],
            [brand, mode, provider, model, exit_hint],
            [brand, mode, model, exit_hint],
            [brand, model, exit_hint],
            [brand, exit_hint],
        ]
        for parts in candidates:
            line = f" {' • '.join(parts)} "
            if get_cwidth(line) <= resolved_width:
                return line
        return _truncate_display_width(f" {brand} • {exit_hint} ", resolved_width)


def run_terminal_loop(
    *,
    via: str,
    config: AppConfig | None,
    gateway_url: str | None,
    execute: Callable[[str, Callable[[dict], None] | None], dict],
) -> None:
    """Run the interactive terminal loop."""
    ui = TerminalUI.create(via=via, config=config, gateway_url=gateway_url)
    ui.print_welcome()
    while True:
        try:
            user_message = ui.prompt().strip()
        except EOFError:
            ui.console.print()
            break
        except KeyboardInterrupt:
            ui.console.print(Text(t("ui.input_cancelled"), style="#e0af68"))
            continue
        if not user_message:
            continue
        if user_message.lower() in {"exit", "quit"}:
            break
        ui.render_user_message(user_message)
        with ui.busy():
            result = execute(user_message, ui.render_stream_event)
        ui.render_turn(
            user_message,
            result,
            render_user_message=False,
            render_tool_events=False,
            render_runtime_events=False,
        )


def render_single_turn(
    *,
    via: str,
    config: AppConfig | None,
    gateway_url: str | None,
    user_message: str,
    result: dict,
) -> None:
    """Render one non-interactive turn."""
    ui = TerminalUI.create(via=via, config=config, gateway_url=gateway_url)
    ui.render_turn(user_message, result)


def _build_key_bindings() -> KeyBindings:
    bindings = KeyBindings()

    @bindings.add("enter")
    def _(event) -> None:
        event.current_buffer.validate_and_handle()

    @bindings.add("escape", "enter")
    def _(event) -> None:
        event.current_buffer.insert_text("\n")

    @bindings.add("c-l")
    def _(event) -> None:
        clear()

    return bindings


def _render_tool_event(
    event: dict,
    index: int,
    *,
    width: int | None = None,
    height: int | None = None,
) -> Panel:
    arguments = _stringify_tool_payload(event.get("arguments") or {})
    result = _stringify_tool_payload(event.get("result") or {})
    max_chars = _tool_payload_limit(width=width, height=height)
    if len(result) > max_chars:
        result = result[:max_chars] + "\n...[truncated]"

    table = Table.grid(padding=(0, 1))
    table.add_row(Text("arguments", style="#9aa5ce"), Text(arguments, style="#c0caf5"))
    table.add_row(Text("result", style="#9aa5ce"), Text(result, style="#a9b1d6"))
    return Panel(
        table,
        title=f"[bold #9ece6a]tool #{index} · {event.get('tool_name')}[/bold #9ece6a]",
        border_style="#2ac3de",
        padding=(0, 1),
    )


def _stringify_tool_payload(value: object) -> str:
    if isinstance(value, (dict, list)):
        return json.dumps(value, ensure_ascii=False, indent=2)
    return str(value)


def _build_reply_subtitle(result: dict, *, width: int | None = None) -> str:
    resolved_width = max(width or 80, 20)
    tool_count = len(result.get("tool_events") or [])
    candidates = [
        [
            t("reply.mode", value=result.get("mode")),
            t("reply.iter", value=result.get("iterations")),
            t("reply.tools", value=tool_count),
            t("reply.provider", value=result.get("provider_id") or "?"),
            t("reply.model", value=result.get("model_name") or "(unknown)"),
            t("reply.run", value=result.get("run_id")),
        ],
        [
            t("reply.mode", value=result.get("mode")),
            t("reply.iter", value=result.get("iterations")),
            t("reply.tools", value=tool_count),
            t("reply.model", value=result.get("model_name") or "(unknown)"),
        ],
        [
            t("reply.mode", value=result.get("mode")),
            t("reply.tools", value=tool_count),
            t("reply.model", value=result.get("model_name") or "(unknown)"),
        ],
        [t("reply.mode", value=result.get("mode"))],
    ]
    for parts in candidates:
        line = " · ".join(parts)
        if get_cwidth(line) <= resolved_width:
            return line
    return _truncate_display_width(candidates[-1][0], resolved_width)


def _render_runtime_event(
    event: dict,
    *,
    width: int | None = None,
    height: int | None = None,
) -> Panel | Text | None:
    event_type = str(event.get("type") or "").strip().lower()
    if event_type == "tool":
        return _render_tool_event(
            event,
            int(event.get("tool_index") or 1),
            width=width,
            height=height,
        )
    if event_type != "memory":
        return None
    phase = str(event.get("phase") or "info").strip().lower()
    style_map = {
        "start": "#7aa2f7",
        "complete": "#9ece6a",
        "skipped": "#e0af68",
        "error": "#f7768e",
    }
    icon_map = {
        "start": "memory:start",
        "complete": "memory:done",
        "skipped": "memory:skip",
        "error": "memory:error",
    }
    prefix = icon_map.get(phase, "memory")
    message = str(event.get("message") or "memory event")
    return Text(f"[{prefix}] {message}", style=style_map.get(phase, "#9aa5ce"))


def _tool_payload_limit(*, width: int | None = None, height: int | None = None) -> int:
    resolved_width = max(width or 80, 20)
    resolved_height = max(height or 24, 8)
    return max(240, min(4000, int(resolved_width * max(resolved_height - 8, 6) * 1.2)))


def _truncate_display_width(text: str, max_width: int) -> str:
    resolved_width = max(max_width, 1)
    if get_cwidth(text) <= resolved_width:
        return text
    ellipsis = "..."
    if resolved_width <= len(ellipsis):
        return ellipsis[:resolved_width]
    target_width = resolved_width - len(ellipsis)
    rendered: list[str] = []
    used_width = 0
    for char in text:
        char_width = max(get_cwidth(char), 0)
        if used_width + char_width > target_width:
            break
        rendered.append(char)
        used_width += char_width
    return "".join(rendered).rstrip() + ellipsis
