from __future__ import annotations

import asyncio
import math
import webbrowser

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Grid, Vertical
from textual.reactive import reactive
from textual.timer import Timer
from textual.widgets import Footer, Header, Static

from . import cmux
from .models import Workspace
from .screens.launch_modal import LaunchAgentModal
from .screens.send_input_modal import SendInputModal
from .widgets.activity_feed import ActivityFeed
from .widgets.workspace_card import WorkspaceCard


CSS = """
Screen {
    layout: vertical;
}

#hud-bar {
    dock: top;
    height: 1;
    background: $boost;
    color: $text-muted;
    padding: 0 1;
}

#main-area {
    height: 1fr;
}

#dashboard-grid {
    height: 1fr;
    padding: 0;
}

#activity-strip {
    height: 5;
    border-top: solid $secondary;
}

ActivityFeed {
    height: 100%;
}

/* --- Workspace Cards --- */
WorkspaceCard {
    border: solid $surface;
    padding: 0;
}

WorkspaceCard.card-focused {
    border: heavy cyan;
}

WorkspaceCard.state-running {
    border: solid $success;
}
WorkspaceCard.state-running.card-focused {
    border: heavy $success;
}

WorkspaceCard.state-input {
    border: solid $warning;
}
WorkspaceCard.state-input.card-focused {
    border: heavy $warning;
}

#empty-dashboard {
    height: 1fr;
    content-align: center middle;
    text-align: center;
    color: $text-muted;
}
"""


class CmuxCtlApp(App):
    """cmux-ctl: TUI dashboard for cmux workspaces and Claude Code agents."""

    TITLE = "cmux-ctl"
    SUB_TITLE = "Agent Dashboard"
    CSS = CSS

    BINDINGS = [
        Binding("q", "quit", "Quit"),
        Binding("r", "refresh", "Refresh"),
        Binding("l", "launch_agent", "Launch Agent"),
        Binding("s", "stop_agent", "Stop Agent"),
        Binding("i", "send_input", "Send Input"),
        Binding("enter", "select_workspace", "Switch WS"),
        Binding("tab", "focus_next_card", "Next Card"),
        Binding("shift+tab", "focus_prev_card", "Prev Card"),
        Binding("right", "focus_right", "Right", show=False, priority=True),
        Binding("left", "focus_left", "Left", show=False, priority=True),
        Binding("down", "focus_down", "Down", show=False, priority=True),
        Binding("up", "focus_up", "Up", show=False, priority=True),
    ]

    workspaces: reactive[list[Workspace]] = reactive(list, always_update=True)
    _refresh_timer: Timer | None = None
    _focused_ws_ref: str | None = None
    _prev_agent_states: dict[str, str] = {}
    _cards: dict[str, WorkspaceCard] = {}

    def compose(self) -> ComposeResult:
        yield Header()
        yield Static("", id="hud-bar")
        with Vertical(id="main-area"):
            yield Grid(id="dashboard-grid")
            with Vertical(id="activity-strip"):
                yield ActivityFeed()
        yield Footer()

    async def on_mount(self) -> None:
        self.query_one(ActivityFeed).add_system("Starting cmux-ctl...")

        ok = False
        for attempt in range(3):
            ok = await cmux.ping()
            if ok:
                break
            await asyncio.sleep(1)
        if not ok:
            self.query_one(ActivityFeed).add_system(
                "[red]Cannot reach cmux. Is it running?[/]"
            )
        else:
            self.query_one(ActivityFeed).add_system("[green]Connected to cmux[/]")

        await self.action_refresh()
        self._refresh_timer = self.set_interval(3.0, self._auto_refresh)
        self._terminal_timer = self.set_interval(2.0, self._auto_refresh_terminals)

    async def _auto_refresh(self) -> None:
        await self.action_refresh()

    async def _auto_refresh_terminals(self) -> None:
        if self.workspaces:
            await self._refresh_terminals(self.workspaces)

    async def action_refresh(self) -> None:
        new_state = await cmux.get_full_state()
        if not new_state:
            return

        # Detect state changes for activity feed
        feed = self.query_one(ActivityFeed)
        for ws in new_state:
            old_state = self._prev_agent_states.get(ws.ref)
            new_agent_state = ws.agent_state
            if old_state and old_state != new_agent_state:
                if new_agent_state == "running":
                    feed.add_entry(ws.title, "Agent started", "green")
                elif new_agent_state == "input":
                    feed.add_entry(ws.title, "Waiting for input", "yellow")
                elif new_agent_state == "idle":
                    feed.add_entry(ws.title, "Agent idle", "dim")
            self._prev_agent_states[ws.ref] = new_agent_state

        self.workspaces = new_state

        # Update HUD
        total = len(new_state)
        running = sum(1 for ws in new_state if ws.agent_state == "running")
        waiting = sum(1 for ws in new_state if ws.agent_state == "input")
        agents = sum(len(ws.agents) for ws in new_state)
        notifs = sum(
            sum(1 for n in ws.notifications if not n.is_read) for ws in new_state
        )
        hud_parts = [
            f"WS: {total}",
            f"AGENTS: {agents}",
            f"RUNNING: {running}",
            f"INPUT: {waiting}",
        ]
        if notifs:
            hud_parts.append(f"NOTIFS: {notifs}")
        focused_name = ""
        if self._focused_ws_ref:
            focused = next((w for w in new_state if w.ref == self._focused_ws_ref), None)
            if focused:
                focused_name = f"  ▸ {focused.title}"
        self.query_one("#hud-bar", Static).update(
            "  │  ".join(hud_parts) + focused_name
        )

        # Rebuild grid if workspace count changed or refs changed
        current_refs = {ws.ref for ws in new_state}
        existing_refs = set(self._cards.keys())
        if current_refs != existing_refs:
            await self._rebuild_grid(new_state)
        else:
            # Update existing cards
            for ws in new_state:
                card = self._cards.get(ws.ref)
                if card:
                    card.workspace = ws
                    self._apply_state_class(card, ws)

        # Auto-focus first card if none focused
        if not self._focused_ws_ref and new_state:
            self._focused_ws_ref = new_state[0].ref
            card = self._cards.get(new_state[0].ref)
            if card:
                card.focused_card = True

        # Terminal content is refreshed by its own timer (_auto_refresh_terminals)

    async def _rebuild_grid(self, workspaces: list[Workspace]) -> None:
        grid = self.query_one("#dashboard-grid", Grid)
        await grid.remove_children()
        self._cards.clear()

        if not workspaces:
            await grid.mount(
                Static("[dim]No workspaces. Press [bold]l[/] to launch an agent.[/]", id="empty-dashboard")
            )
            return

        # Calculate grid dimensions
        n = len(workspaces)
        cols = math.ceil(math.sqrt(n))
        rows = math.ceil(n / cols)

        grid.styles.grid_size_columns = cols
        grid.styles.grid_size_rows = rows
        grid.styles.grid_columns = " ".join(["1fr"] * cols)
        grid.styles.grid_rows = " ".join(["1fr"] * rows)

        for ws in workspaces:
            card = WorkspaceCard(ws, id=f"card-{ws.ref.replace(':', '-')}")
            self._apply_state_class(card, ws)
            if ws.ref == self._focused_ws_ref:
                card.focused_card = True
            self._cards[ws.ref] = card
            await grid.mount(card)

    def action_open_link(self, url: str) -> None:
        webbrowser.open(url)

    def _apply_state_class(self, card: WorkspaceCard, ws: Workspace) -> None:
        card.remove_class("state-running", "state-input")
        state = ws.agent_state
        if card.is_stale and state == "running":
            return
        if state == "running":
            card.add_class("state-running")
        elif state == "input":
            card.add_class("state-input")

    async def _refresh_terminals(self, workspaces: list[Workspace]) -> None:
        """Fetch terminal output for all workspaces in parallel."""
        async def fetch_one(ws: Workspace) -> tuple[str, str]:
            try:
                agents = ws.agents
                surface_ref = agents[0].ref if agents else None
                text = await cmux.read_screen(ws.ref, surface_ref=surface_ref, lines=15)
                return ws.ref, text
            except Exception:
                return ws.ref, ""

        results = await asyncio.gather(
            *(fetch_one(ws) for ws in workspaces),
            return_exceptions=True,
        )
        for r in results:
            if isinstance(r, Exception):
                continue
            ref, text = r
            card = self._cards.get(ref)
            if not card:
                continue

            # Detect Claude Code from screen content when not detected by title
            ws = next((w for w in workspaces if w.ref == ref), None)
            if ws and not ws.agents:
                detected = "hold Space to speak" in text or "hold space to speak" in text
                if detected:
                    ws._screen_has_agent = True
                    if "⏺" in text:
                        ws._screen_agent_state = "running"
                    else:
                        ws._screen_agent_state = "input"
                    card.workspace = ws
                    self._apply_state_class(card, ws)
                else:
                    ws._screen_has_agent = False

            card.terminal_text = text

    def on_workspace_card_card_focused(self, event: WorkspaceCard.CardFocused) -> None:
        # Unfocus all, focus the clicked one
        for card in self._cards.values():
            card.focused_card = False
        self._focused_ws_ref = event.workspace.ref
        card = self._cards.get(event.workspace.ref)
        if card:
            card.focused_card = True

    def _get_focused_workspace(self) -> Workspace | None:
        if not self._focused_ws_ref:
            return None
        return next((w for w in self.workspaces if w.ref == self._focused_ws_ref), None)

    async def action_select_workspace(self) -> None:
        ws = self._get_focused_workspace()
        if ws:
            ok = await cmux.select_workspace(ws.ref)
            if ok:
                self.query_one(ActivityFeed).add_entry(
                    ws.title, "Switched to workspace", "cyan"
                )

    def action_focus_next_card(self) -> None:
        if not self.workspaces:
            return
        refs = [ws.ref for ws in self.workspaces]
        if not self._focused_ws_ref or self._focused_ws_ref not in refs:
            idx = 0
        else:
            idx = (refs.index(self._focused_ws_ref) + 1) % len(refs)
        self._set_focus_by_index(idx, refs)

    def action_focus_prev_card(self) -> None:
        if not self.workspaces:
            return
        refs = [ws.ref for ws in self.workspaces]
        if not self._focused_ws_ref or self._focused_ws_ref not in refs:
            idx = 0
        else:
            idx = (refs.index(self._focused_ws_ref) - 1) % len(refs)
        self._set_focus_by_index(idx, refs)

    def _set_focus_by_index(self, idx: int, refs: list[str]) -> None:
        for card in self._cards.values():
            card.focused_card = False
        self._focused_ws_ref = refs[idx]
        card = self._cards.get(refs[idx])
        if card:
            card.focused_card = True

    def _grid_cols(self) -> int:
        n = len(self.workspaces)
        return math.ceil(math.sqrt(n)) if n else 1

    def action_focus_right(self) -> None:
        if not self.workspaces:
            return
        refs = [ws.ref for ws in self.workspaces]
        if not self._focused_ws_ref or self._focused_ws_ref not in refs:
            idx = 0
        else:
            idx = refs.index(self._focused_ws_ref)
            cols = self._grid_cols()
            row = idx // cols
            col = (idx % cols + 1) % cols
            new_idx = row * cols + col
            idx = new_idx if new_idx < len(refs) else idx
        self._set_focus_by_index(idx, refs)

    def action_focus_left(self) -> None:
        if not self.workspaces:
            return
        refs = [ws.ref for ws in self.workspaces]
        if not self._focused_ws_ref or self._focused_ws_ref not in refs:
            idx = 0
        else:
            idx = refs.index(self._focused_ws_ref)
            cols = self._grid_cols()
            row = idx // cols
            col = (idx % cols - 1) % cols
            new_idx = row * cols + col
            idx = new_idx if new_idx < len(refs) else idx
        self._set_focus_by_index(idx, refs)

    def action_focus_down(self) -> None:
        if not self.workspaces:
            return
        refs = [ws.ref for ws in self.workspaces]
        if not self._focused_ws_ref or self._focused_ws_ref not in refs:
            idx = 0
        else:
            idx = refs.index(self._focused_ws_ref)
            cols = self._grid_cols()
            new_idx = idx + cols
            idx = new_idx if new_idx < len(refs) else idx
        self._set_focus_by_index(idx, refs)

    def action_focus_up(self) -> None:
        if not self.workspaces:
            return
        refs = [ws.ref for ws in self.workspaces]
        if not self._focused_ws_ref or self._focused_ws_ref not in refs:
            idx = 0
        else:
            idx = refs.index(self._focused_ws_ref)
            cols = self._grid_cols()
            new_idx = idx - cols
            idx = new_idx if new_idx >= 0 else idx
        self._set_focus_by_index(idx, refs)

    async def action_launch_agent(self) -> None:
        names = [ws.title for ws in self.workspaces]
        # Collect unique cwds from all workspaces for directory picker
        cwds: list[str] = []
        seen: set[str] = set()
        for ws in self.workspaces:
            cwd = ws.sidebar.cwd
            if cwd and cwd != "none" and cwd not in seen:
                cwds.append(cwd)
                seen.add(cwd)

        def on_result(result: dict | None) -> None:
            if result:
                self.run_worker(self._do_launch(result))

        self.push_screen(LaunchAgentModal(names, cwds=cwds), on_result)

    async def _do_launch(self, config: dict) -> None:
        feed = self.query_one(ActivityFeed)
        prompt = config.get("prompt", "")
        workspace_name = config.get("workspace")
        cwd = config.get("cwd", "")

        if workspace_name:
            ws = next((w for w in self.workspaces if w.title == workspace_name), None)
            if ws:
                agents = ws.agents
                if agents:
                    ok = await cmux.send_text(
                        prompt + "\n", ws.ref, agents[0].ref
                    )
                    if ok:
                        feed.add_entry(ws.title, f"Sent: {prompt[:50]}", "green")
                    else:
                        feed.add_entry(ws.title, "Failed to send", "red")
                else:
                    cmd = f"claude '{prompt}'" if prompt else "claude"
                    ok = await cmux.send_text(cmd + "\n", ws.ref)
                    if ok:
                        feed.add_entry(ws.title, f"Launching agent: {prompt[:40]}", "green")
                    else:
                        feed.add_entry(ws.title, "Failed to launch", "red")
        else:
            effective_cwd = cwd or "~"
            cmd = f"claude '{prompt}'" if prompt else "claude"
            ok = await cmux.new_workspace(cwd=effective_cwd, command=cmd)
            if ok:
                feed.add_system(f"New workspace with agent: {prompt[:40]}")
            else:
                feed.add_system("[red]Failed to create workspace[/]")

    async def action_stop_agent(self) -> None:
        ws = self._get_focused_workspace()
        if not ws:
            return
        agents = ws.agents
        if agents:
            ok = await cmux.send_key("C-c", ws.ref, agents[0].ref)
            feed = self.query_one(ActivityFeed)
            if ok:
                feed.add_entry(ws.title, "Sent Ctrl+C to agent", "yellow")
            else:
                feed.add_entry(ws.title, "Failed to stop agent", "red")

    async def action_send_input(self) -> None:
        ws = self._get_focused_workspace()
        if not ws:
            return

        def on_result(text: str | None) -> None:
            if text:
                self.run_worker(self._do_send_input(ws, text))

        self.push_screen(SendInputModal(ws.title), on_result)

    async def _do_send_input(self, ws: Workspace, text: str) -> None:
        agents = ws.agents
        feed = self.query_one(ActivityFeed)
        if agents:
            ok = await cmux.send_text(text + "\n", ws.ref, agents[0].ref)
        else:
            ok = await cmux.send_text(text + "\n", ws.ref)
        if ok:
            feed.add_entry(ws.title, f"Sent: {text[:50]}", "green")
        else:
            feed.add_entry(ws.title, "Failed to send input", "red")
