"""Status bar widget."""

from __future__ import annotations

import logging
from contextlib import suppress
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, get_args

from textual.containers import Horizontal
from textual.content import Content
from textual.css.query import NoMatches
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Static

from deepagents_code._env_vars import HIDE_CWD, HIDE_GIT_BRANCH, is_env_truthy
from deepagents_code.config import get_glyphs
from deepagents_code.widgets.loading import Spinner

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from textual import events
    from textual.app import ComposeResult, RenderResult
    from textual.geometry import Size
    from textual.timer import Timer

PROVIDER_PREFIX_STRIPS: dict[str, tuple[str, ...]] = {
    "fireworks": ("accounts/fireworks/models/",),
}
"""Some providers (e.g. Fireworks) require fully-qualified IDs like
`accounts/fireworks/models/...` that crowd out the rest of the status bar;
strip the registered prefixes before display."""

ConnectionState = Literal["", "connecting", "reconnecting", "resuming"]
"""Connection states the status bar can display (`''` means cleared)."""

CONNECTION_STATES = frozenset(get_args(ConnectionState))
"""Runtime view of `ConnectionState` for `set_connection`'s defensive guard.

Derived from the `Literal` so the two can never drift."""


class ModelLabel(Widget):
    """A label that displays a model name, right-aligned with smart truncation.

    When the full `provider:model` text doesn't fit, the provider is dropped
    first. If the bare model name still doesn't fit, it is left-truncated
    with a leading ellipsis so the most distinctive tail stays visible.
    """

    provider: reactive[str] = reactive("", layout=True)
    model: reactive[str] = reactive("", layout=True)

    def _clean_model(self) -> str:
        """Strip the provider's registered prefix so the status bar stays compact.

        Returns:
            Model name with the provider's registered prefix removed if present,
                otherwise the original name.
        """
        name = self.model
        if not name or not self.provider:
            return name
        for prefix in PROVIDER_PREFIX_STRIPS.get(self.provider, ()):
            if name.startswith(prefix):
                return name[len(prefix) :]
        return name

    def get_content_width(self, container: Size, viewport: Size) -> int:  # noqa: ARG002
        """Return the intrinsic width so `width: auto` works.

        Args:
            container: Size of the container.
            viewport: Size of the viewport.

        Returns:
            Character length of the full provider:model string.
        """
        if not self.model:
            return 0
        model = self._clean_model()
        full = f"{self.provider}:{model}" if self.provider else model
        return len(full)

    def render(self) -> RenderResult:
        """Render the model label with width-aware truncation.

        Returns:
            Text content, truncated from the left when necessary.
        """
        width = self.content_size.width
        if not self.model or width <= 0:
            return ""
        model = self._clean_model()
        full = f"{self.provider}:{model}" if self.provider else model
        if len(full) <= width:
            return Content(full)
        if len(model) <= width:
            return Content(model)
        if width > 1:
            return Content("\u2026" + model[-(width - 1) :])
        return Content("\u2026")


class StatusBar(Horizontal):
    """Status bar showing mode, auto-approve, cwd, git branch, tokens, and model."""

    DEFAULT_CSS = """
    StatusBar {
        height: 1;
        dock: bottom;
        background: $surface;
    }

    StatusBar .status-mode {
        width: auto;
        padding: 0 1;
    }

    StatusBar .status-mode.normal {
        display: none;
    }

    StatusBar .status-mode.shell {
        background: $mode-bash;
        color: white;
        text-style: bold;
    }

    StatusBar .status-mode.command {
        background: $mode-command;
        color: white;
    }

    StatusBar .status-mode.shell-incognito {
        background: $mode-incognito;
        color: $background;
        text-style: bold;
    }

    StatusBar .status-auto-approve {
        width: auto;
        padding: 0 1;
    }

    StatusBar .status-auto-approve.on {
        background: $success;
        color: $background;
    }

    StatusBar .status-auto-approve.off {
        background: $warning;
        color: $background;
    }

    StatusBar .status-connection {
        width: auto;
        padding: 0 1;
        color: $warning;
        text-style: bold;
    }

    StatusBar .status-message {
        width: auto;
        padding: 0 1;
        color: $text-muted;
    }

    StatusBar .status-message.thinking {
        color: $warning;
    }

    StatusBar .status-cwd {
        width: auto;
        text-align: right;
        color: $text-muted;
    }

    StatusBar .status-branch {
        width: auto;
        color: $text-muted;
        padding: 0 1;
    }

    StatusBar .status-left-collapsible {
        width: 1fr;
        min-width: 0;
        height: 1;
        overflow-x: hidden;
    }

    StatusBar .status-tokens {
        width: auto;
        padding: 0 1;
        color: $text-muted;
    }

    StatusBar ModelLabel {
        width: auto;
        padding: 0 2;
        color: $text-muted;
        text-align: right;
    }
    """
    """Mode badges and auto-approve pills use distinct colors for at-a-glance status."""

    mode: reactive[str] = reactive("normal", init=False)
    status_message: reactive[str] = reactive("", init=False)
    connection_state: reactive[ConnectionState] = reactive("", init=False)
    queued_count: reactive[int] = reactive(0, init=False)
    auto_approve: reactive[bool] = reactive(default=False, init=False)
    cwd: reactive[str] = reactive("", init=False)
    branch: reactive[str] = reactive("", init=False)
    tokens: reactive[int] = reactive(0, init=False)

    def __init__(self, cwd: str | Path | None = None, **kwargs: Any) -> None:
        """Initialize the status bar.

        Args:
            cwd: Current working directory to display
            **kwargs: Additional arguments passed to parent
        """
        super().__init__(**kwargs)
        # Store initial cwd - will be used in compose()
        self._initial_cwd = str(cwd) if cwd else str(Path.cwd())
        self._hide_cwd = is_env_truthy(HIDE_CWD)
        self._hide_git_branch = is_env_truthy(HIDE_GIT_BRANCH)
        self._spinner = Spinner()
        self._spinner_timer: Timer | None = None

    def compose(self) -> ComposeResult:  # noqa: PLR6301 — Textual widget method
        """Compose the status bar layout.

        Yields:
            Widgets for mode, auto-approve, message, cwd, branch, tokens, and
                model display.
        """
        yield Static("", classes="status-mode normal", id="mode-indicator")
        yield Static(
            "manual",
            classes="status-auto-approve off",
            id="auto-approve-indicator",
        )
        with Horizontal(classes="status-left-collapsible"):
            yield Static("", classes="status-connection", id="connection-indicator")
            yield Static("", classes="status-message", id="status-message")
            yield Static("", classes="status-cwd", id="cwd-display")
            yield Static("", classes="status-branch", id="branch-display")
        yield Static("", classes="status-tokens", id="tokens-display")
        yield ModelLabel(id="model-display")

    _BRANCH_WIDTH_THRESHOLD = 100
    """Hide git branch display below this terminal width."""
    _CWD_WIDTH_THRESHOLD = 70
    """Hide cwd display below this terminal width."""

    def on_resize(self, event: events.Resize) -> None:
        """Manage visibility of status items based on terminal width.

        Priority (highest first): model, cwd, git branch.
        """
        width = event.size.width
        branch_threshold = (
            self._CWD_WIDTH_THRESHOLD
            if self._hide_cwd
            else self._BRANCH_WIDTH_THRESHOLD
        )
        with suppress(NoMatches):
            self.query_one("#branch-display", Static).display = (
                not self._hide_git_branch and width >= branch_threshold
            )
        with suppress(NoMatches):
            self.query_one("#cwd-display", Static).display = (
                not self._hide_cwd and width >= self._CWD_WIDTH_THRESHOLD
            )

    def on_unmount(self) -> None:
        """Stop the spinner timer so it can't tick on a detached widget."""
        self._stop_spinner()

    def on_mount(self) -> None:
        """Set reactive values after mount to trigger watchers safely."""
        from deepagents_code.config import settings

        self.cwd = self._initial_cwd
        if self._hide_cwd:
            with suppress(NoMatches):
                self.query_one("#cwd-display", Static).display = False
        if self._hide_git_branch:
            with suppress(NoMatches):
                self.query_one("#branch-display", Static).display = False
        # Set initial model display
        label = self.query_one("#model-display", ModelLabel)
        label.provider = settings.model_provider or ""
        label.model = settings.model_name or ""
        # Reactives are `init=False`, so the connection watcher never fires on
        # mount; render once to hide the empty indicator (and its padding).
        self._render_connection()

    def watch_mode(self, mode: str) -> None:
        """Update mode indicator when mode changes."""
        try:
            indicator = self.query_one("#mode-indicator", Static)
        except NoMatches:
            return
        indicator.remove_class("normal", "shell", "command", "shell-incognito")

        if mode == "shell":
            indicator.update("SHELL")
            indicator.add_class("shell")
        elif mode == "shell_incognito":
            indicator.update("SHELL")
            indicator.add_class("shell-incognito")
        elif mode == "command":
            indicator.update("CMD")
            indicator.add_class("command")
        else:
            indicator.update("")
            indicator.add_class("normal")

    def watch_auto_approve(self, new_value: bool) -> None:
        """Update auto-approve indicator when state changes."""
        try:
            indicator = self.query_one("#auto-approve-indicator", Static)
        except NoMatches:
            return
        indicator.remove_class("on", "off")

        if new_value:
            indicator.update("auto")
            indicator.add_class("on")
        else:
            indicator.update("manual")
            indicator.add_class("off")

    def watch_cwd(self, new_value: str) -> None:
        """Update cwd display when it changes."""
        try:
            display = self.query_one("#cwd-display", Static)
        except NoMatches:
            return
        display.update(self._format_cwd(new_value))

    def watch_branch(self, new_value: str) -> None:
        """Update branch display when it changes."""
        try:
            display = self.query_one("#branch-display", Static)
        except NoMatches:
            return
        icon = get_glyphs().git_branch
        display.update(f"{icon} {new_value}" if new_value else "")

    def watch_status_message(self, new_value: str) -> None:
        """Update status message display."""
        try:
            msg_widget = self.query_one("#status-message", Static)
        except NoMatches:
            return

        msg_widget.remove_class("thinking")
        if new_value:
            msg_widget.update(new_value)
            if "thinking" in new_value.lower() or "executing" in new_value.lower():
                msg_widget.add_class("thinking")
        else:
            msg_widget.update("")

    def watch_connection_state(self, new_value: ConnectionState) -> None:
        """Start or stop the spinner and re-render when connection state changes."""
        if new_value in {"connecting", "reconnecting", "resuming"}:
            self._start_spinner()
        else:
            self._stop_spinner()
        self._render_connection()

    def watch_queued_count(self, _new_value: int) -> None:
        """Re-render the connection indicator when the queued count changes."""
        self._render_connection()

    def _start_spinner(self) -> None:
        """Begin cycling the connection-indicator spinner frames.

        No-op when not yet running (e.g. before mount) since `set_interval`
        requires a live event loop, or when an animation is already active.
        """
        if self._spinner_timer is not None or not self._running:
            return
        # 0.1s mirrors LoadingWidget so this spinner ticks in step with the
        # in-thread "Thinking" spinner.
        self._spinner_timer = self.set_interval(0.1, self._tick_spinner)

    def _stop_spinner(self) -> None:
        """Stop the spinner animation and reset to the first frame."""
        if self._spinner_timer is not None:
            self._spinner_timer.stop()
            self._spinner_timer = None
        self._spinner = Spinner()

    def _tick_spinner(self) -> None:
        """Advance the spinner frame and re-render the connection indicator."""
        self._spinner.next_frame()
        self._render_connection()

    def _render_connection(self) -> None:
        """Render the combined connection + queued-count indicator text."""
        try:
            widget = self.query_one("#connection-indicator", Static)
        except NoMatches:
            return

        parts: list[str] = []
        if self.connection_state == "reconnecting":
            parts.append(f"{self._spinner.current_frame()} Reconnecting")
        elif self.connection_state == "resuming":
            parts.append(f"{self._spinner.current_frame()} Resuming")
        elif self.connection_state == "connecting":
            parts.append(f"{self._spinner.current_frame()} Connecting")
        if self.queued_count > 0:
            label = "message" if self.queued_count == 1 else "messages"
            parts.append(f"{self.queued_count} {label} queued")
        separator = f" {get_glyphs().bullet} "
        text = separator.join(parts)
        # Hide the widget entirely when empty so its `padding: 0 1` doesn't
        # leave a 2-column gap between the auto-approve pill and the cwd.
        widget.display = bool(text)
        widget.update(text)

    def set_connection(self, state: ConnectionState) -> None:
        """Set the connection indicator state.

        Args:
            state: One of `''` (clear), `'connecting'`, `'reconnecting'`, or
                `'resuming'`.

        Raises:
            ValueError: If `state` is not a recognized connection state.
        """
        if state not in CONNECTION_STATES:
            msg = f"Unknown connection state: {state!r}"
            raise ValueError(msg)
        self.connection_state = state

    def set_queued(self, count: int) -> None:
        """Set the number of messages waiting in the queue.

        Args:
            count: Count of queued messages (negative values clamp to `0`).
        """
        self.queued_count = max(count, 0)

    def _format_cwd(self, cwd_path: str = "") -> str:
        """Format the current working directory for display.

        Returns:
            Formatted path string, using ~ for home directory when possible.
        """
        path = Path(cwd_path or self.cwd or self._initial_cwd)
        try:
            # Try to use ~ for home directory
            home = Path.home()
            if path.is_relative_to(home):
                return "~/" + path.relative_to(home).as_posix()
        except (ValueError, RuntimeError):
            pass
        return str(path)

    def set_mode(self, mode: str) -> None:
        """Set the current input mode.

        Args:
            mode: One of "normal", "shell", or "command"
        """
        self.mode = mode

    def set_auto_approve(self, *, enabled: bool) -> None:
        """Set the auto-approve state.

        Args:
            enabled: Whether auto-approve is enabled
        """
        self.auto_approve = enabled

    def set_status_message(self, message: str) -> None:
        """Set the status message.

        Args:
            message: Status message to display (empty string to clear)
        """
        self.status_message = message

    _approximate: bool = False
    """Append "+" to the token count to signal that the displayed value is stale.

    (The actual context is larger because the generation was interrupted before
    the model reported final usage.)
    """
    _has_token_count: bool = False
    """Whether the status bar has displayed a real token count this session."""

    def watch_tokens(self, new_value: int) -> None:
        """Update token display when count changes."""
        self._render_tokens(new_value, approximate=self._approximate)

    def _render_tokens(self, count: int, *, approximate: bool = False) -> None:
        """Render the token count into the display widget.

        Args:
            count: Total context token count.
            approximate: Append "+" suffix to indicate the count is stale
                (e.g. after an interrupted generation).
        """
        try:
            display = self.query_one("#tokens-display", Static)
        except NoMatches:
            return

        if count > 0:
            suffix = "+" if approximate else ""
            # Format with K suffix for thousands
            if count >= 1000:  # noqa: PLR2004  # Count formatting threshold
                display.update(f"{count / 1000:.1f}K{suffix} tokens")
            else:
                display.update(f"{count}{suffix} tokens")
        else:
            display.update("")

    def set_tokens(self, count: int, *, approximate: bool = False) -> None:
        """Set the token count.

        Forces a display refresh even when the value is unchanged. During
        streaming, `show_pending_tokens` replaces the widget text without
        changing the reactive token value, so a later update with the same
        count still needs to re-render the exact count.

        Args:
            count: Current context token count.
            approximate: Append "+" to indicate the count is stale.
        """
        self._approximate = approximate
        self._has_token_count = count > 0
        if self.tokens == count:
            # Reactive dedup would skip the watcher — call render directly.
            self._render_tokens(count, approximate=approximate)
        else:
            # Reactive assignment triggers watch_tokens, which reads
            # self._approximate for the suffix.
            self.tokens = count

    def show_pending_tokens(self) -> None:
        """Show an unknown token count placeholder during streaming."""
        if not self._has_token_count:
            return
        try:
            self.query_one("#tokens-display", Static).update("... tokens")
        except NoMatches:
            return

    def set_model(self, *, provider: str, model: str) -> None:
        """Update the model display text.

        Args:
            provider: Model provider name (e.g., `'anthropic'`).
            model: Model name (e.g., `'claude-sonnet-4-5'`).
        """
        label = self.query_one("#model-display", ModelLabel)
        label.provider = provider
        label.model = model
