"""cmux CLI communication layer.

All interaction with cmux goes through CLI commands via subprocess.
"""

from __future__ import annotations

import asyncio
import json
import os
import re
from dataclasses import dataclass

from .models import Notification, Pane, Sidebar, Surface, Workspace

CMUX_BIN = os.environ.get(
    "CMUX_BIN",
    "/Applications/cmux.app/Contents/Resources/bin/cmux",
)
SESSION_MAP_FILE = "/tmp/cmux-session-map.json"
TIMEOUT = 5


async def _run(*args: str) -> tuple[str, bool]:
    """Run a cmux CLI command. Returns (stdout, success)."""
    try:
        proc = await asyncio.create_subprocess_exec(
            CMUX_BIN,
            *args,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=TIMEOUT)
        ok = proc.returncode == 0
        return stdout.decode().strip() if ok else "", ok
    except (asyncio.TimeoutError, FileNotFoundError, OSError):
        return "", False


async def ping() -> bool:
    """Check if cmux is reachable."""
    out, ok = await _run("ping")
    return ok


async def get_tree() -> list[Workspace]:
    """Get full workspace tree via `cmux tree --all --json`."""
    out, ok = await _run("tree", "--all", "--json")
    if not ok or not out:
        return []

    try:
        data = json.loads(out)
    except json.JSONDecodeError:
        return []

    workspaces: list[Workspace] = []
    for win in data.get("windows", []):
        for ws_data in win.get("workspaces", []):
            panes = []
            for p_data in ws_data.get("panes", []):
                surfaces = []
                for s_data in p_data.get("surfaces", []):
                    surfaces.append(
                        Surface(
                            ref=s_data.get("ref", ""),
                            title=s_data.get("title", ""),
                            type=s_data.get("type", "terminal"),
                            selected=s_data.get("selected", False),
                            focused=s_data.get("focused", False),
                            url=s_data.get("url"),
                            pane_ref=p_data.get("ref", ""),
                        )
                    )
                panes.append(
                    Pane(
                        ref=p_data.get("ref", ""),
                        focused=p_data.get("focused", False),
                        surfaces=surfaces,
                    )
                )
            workspaces.append(
                Workspace(
                    ref=ws_data.get("ref", ""),
                    title=ws_data.get("title", ""),
                    selected=ws_data.get("selected", False),
                    active=ws_data.get("active", False),
                    pinned=ws_data.get("pinned", False),
                    panes=panes,
                )
            )
    return workspaces


async def get_sidebar(workspace_ref: str) -> Sidebar:
    """Get sidebar metadata for a workspace."""
    out, ok = await _run("sidebar-state", "--workspace", workspace_ref)
    if not ok:
        return Sidebar()

    sb = Sidebar()
    lower_out = out.lower()
    # Detect claude_code state from raw output
    if "needs input" in lower_out:
        sb.claude_code = "Needs input"
    elif "claude_code=running" in lower_out:
        sb.claude_code = "Running"
    elif "claude_code=idle" in lower_out:
        sb.claude_code = "Idle"

    for line in out.splitlines():
        m = re.match(r"^\s*(\w+)=(.*)", line)
        if not m:
            continue
        key, val = m.group(1), m.group(2)
        if key == "tab":
            sb.tab_id = val
        elif key == "color":
            sb.color = val
        elif key == "cwd":
            sb.cwd = val
        elif key == "focused_cwd":
            sb.focused_cwd = val
        elif key == "git_branch":
            sb.git_branch = val
        elif key == "pr":
            sb.pr = val
        elif key == "pr_label":
            sb.pr_label = val
        elif key == "progress":
            sb.progress = val
        elif key == "ports":
            sb.ports = val
        elif key == "status_count":
            sb.status_count = int(val) if val.isdigit() else 0
        elif key == "claude_color":
            sb.claude_color = val
    return sb


async def get_notifications() -> list[Notification]:
    """Get all notifications."""
    out, ok = await _run("list-notifications")
    if not ok or not out:
        return []

    notifications: list[Notification] = []
    for line in out.splitlines():
        # Format: idx:id|workspace_id|surface_id|read_status|title|subtitle|body
        parts = line.split("|", 6)
        if len(parts) < 7:
            continue
        idx_id = parts[0].split(":", 1)
        notif_id = idx_id[1] if len(idx_id) > 1 else idx_id[0]
        notifications.append(
            Notification(
                id=notif_id,
                workspace_id=parts[1],
                surface_id=parts[2],
                is_read=parts[3] == "read",
                title=parts[4],
                subtitle=parts[5],
                body=parts[6],
            )
        )
    return notifications


async def read_screen(
    workspace_ref: str,
    surface_ref: str | None = None,
    lines: int = 20,
) -> str:
    """Read terminal screen text."""
    args = ["read-screen", "--workspace", workspace_ref, "--lines", str(lines)]
    if surface_ref:
        args.extend(["--surface", surface_ref])
    out, ok = await _run(*args)
    return out if ok else ""


async def select_workspace(workspace_ref: str) -> bool:
    """Switch to a workspace."""
    _, ok = await _run("select-workspace", "--workspace", workspace_ref)
    return ok


async def send_text(
    text: str,
    workspace_ref: str | None = None,
    surface_ref: str | None = None,
) -> bool:
    """Send text to a surface (e.g. type a command or prompt)."""
    args = ["send"]
    if workspace_ref:
        args.extend(["--workspace", workspace_ref])
    if surface_ref:
        args.extend(["--surface", surface_ref])
    args.append(text)
    _, ok = await _run(*args)
    return ok


async def send_key(
    key: str,
    workspace_ref: str | None = None,
    surface_ref: str | None = None,
) -> bool:
    """Send a key to a surface (e.g. 'Enter', 'C-c')."""
    args = ["send-key"]
    if workspace_ref:
        args.extend(["--workspace", workspace_ref])
    if surface_ref:
        args.extend(["--surface", surface_ref])
    args.append(key)
    _, ok = await _run(*args)
    return ok


async def new_workspace(cwd: str | None = None, command: str | None = None) -> bool:
    """Create a new workspace, optionally with a cwd and command."""
    args = ["new-workspace"]
    if cwd:
        args.extend(["--cwd", cwd])
    if command:
        args.extend(["--command", command])
    _, ok = await _run(*args)
    return ok


async def close_workspace(workspace_ref: str) -> bool:
    """Close a workspace."""
    _, ok = await _run("close-workspace", "--workspace", workspace_ref)
    return ok


async def list_log(workspace_ref: str, limit: int = 20) -> str:
    """Get log entries for a workspace."""
    out, ok = await _run("list-log", "--workspace", workspace_ref, "--limit", str(limit))
    return out if ok else ""


def load_session_map() -> dict:
    """Load the Claude Code session map from /tmp/cmux-session-map.json."""
    try:
        with open(SESSION_MAP_FILE) as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return {}


async def get_full_state() -> list[Workspace]:
    """Get complete state: tree + sidebar + session map for each workspace."""
    workspaces = await get_tree()
    if not workspaces:
        return []

    # Fetch sidebar state for all workspaces in parallel
    sidebars = await asyncio.gather(
        *(get_sidebar(ws.ref) for ws in workspaces)
    )
    for ws, sb in zip(workspaces, sidebars):
        ws.sidebar = sb

    # Enrich with session map — mark workspaces that have active Claude sessions
    # The session map keys are surface UUIDs, and each entry has a workspace UUID.
    # The sidebar `tab` field is the workspace UUID we can match against.
    session_map = load_session_map()
    if session_map:
        # Build set of workspace UUIDs that have active sessions
        active_ws_uuids = {
            info.get("workspace_id") for info in session_map.values()
        }
        for ws in workspaces:
            # sidebar.tab (parsed below) would be the UUID, but we store it as
            # part of the sidebar. Match via the tab UUID from sidebar-state.
            if ws.sidebar.tab_id and ws.sidebar.tab_id in active_ws_uuids:
                ws._session_map_has_agent = True

    # Fetch notifications
    notifications = await get_notifications()
    # Group by workspace_id
    for notif in notifications:
        for ws in workspaces:
            # Match by workspace title or id in notification
            if notif.workspace_id in (ws.ref, ws.title):
                ws.notifications.append(notif)

    return workspaces
