from __future__ import annotations

import json
import os
import platform
import secrets
import shlex
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any

import httpx
from InquirerPy import inquirer


DEFAULT_SERVER_URL = "https://alsoml.com"
STATE_DIR = Path.home() / ".alsogc"
STATE_FILE = STATE_DIR / "state.json"


def _load_state() -> dict[str, Any]:
    if STATE_FILE.exists():
        return json.loads(STATE_FILE.read_text())
    return {
        "server_url": DEFAULT_SERVER_URL,
        "peer_id": _generate_peer_id(),
        "groups": {},
        "children": {},
    }


def _save_state(state: dict[str, Any]) -> None:
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2))


def _generate_peer_id() -> str:
    hostname = platform.node().split(".")[0].lower()
    suffix = secrets.token_hex(3)
    return f"{hostname}-{suffix}"


def _has_claude_descendant(pid: str) -> bool:
    try:
        ps_result = subprocess.run(
            ["ps", "-eo", "pid,ppid,command"],
            capture_output=True,
            text=True,
        )
    except FileNotFoundError:
        return False

    children_of: dict[str, list[tuple[str, str]]] = {}
    for line in ps_result.stdout.strip().splitlines()[1:]:
        parts = line.strip().split(None, 2)
        if len(parts) < 3:
            continue
        child_pid, parent_pid, cmd = parts
        children_of.setdefault(parent_pid, []).append((child_pid, cmd))

    stack = [pid]
    while stack:
        current = stack.pop()
        for child_pid, cmd in children_of.get(current, []):
            if "claude" in cmd.lower():
                return True
            stack.append(child_pid)
    return False


def _discover_claude_sessions() -> list[dict[str, str]]:
    result = subprocess.run(
        [
            "tmux", "list-panes", "-a",
            "-F", "#{session_name}:#{window_index}.#{pane_index} #{pane_pid} #{pane_current_command}",
        ],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        return []

    sessions: list[dict[str, str]] = []
    for line in result.stdout.strip().splitlines():
        parts = line.split(" ", 2)
        if len(parts) < 3:
            continue
        pane_id, pid, command = parts
        if "claude" not in command.lower() and not _has_claude_descendant(pid):
            continue
        session_name, rest = pane_id.split(":", 1)
        sessions.append({
            "pane_id": pane_id,
            "session_name": session_name,
            "pid": pid,
            "command": command,
        })
    return sessions


def _fetch_groups(server_url: str) -> list[str]:
    try:
        resp = httpx.get(f"{server_url}/auth/groups", timeout=10)
        resp.raise_for_status()
        return resp.json()
    except (httpx.HTTPError, json.JSONDecodeError):
        return []


def _create_group(server_url: str, group_id: str) -> str | None:
    try:
        resp = httpx.post(
            f"{server_url}/auth/groups",
            json={"group_id": group_id},
            timeout=10,
        )
        if resp.status_code == 409:
            print(f"  Group '{group_id}' already exists.", file=sys.stderr)
            return None
        resp.raise_for_status()
        return resp.json()["password"]
    except httpx.HTTPError as e:
        print(f"  Failed to create group: {e}", file=sys.stderr)
        return None


def _verify_group(server_url: str, group_id: str, password: str) -> bool:
    try:
        resp = httpx.post(
            f"{server_url}/auth/groups/{group_id}/verify",
            json={"password": password},
            timeout=10,
        )
        resp.raise_for_status()
        return resp.json().get("ok", False)
    except httpx.HTTPError:
        return False


def _setup_tui_sidebar(
    target_pane: str,
    server_url: str,
    group_id: str,
    peer_id: str,
    groups_header: str,
) -> str | None:
    split_result = subprocess.run(
        [
            "tmux", "split-window", "-h",
            "-t", target_pane,
            "-l", "40%",
            "-P", "-F", "#{session_name}:#{window_index}.#{pane_index}",
        ],
        capture_output=True,
        text=True,
    )
    if split_result.returncode != 0:
        print(f"  Failed to split tmux window: {split_result.stderr}", file=sys.stderr)
        return None

    new_pane = split_result.stdout.strip()
    subprocess.run(
        ["tmux", "send-keys", "-t", new_pane,
         f"export ALSO_GROUPS_HEADER={shlex.quote(groups_header)}", "Enter"],
        check=True,
    )
    cmd = (
        f"relay-tui "
        f"--server-url {server_url} "
        f"--group-id {group_id} "
        f"--peer-id {peer_id} "
        f"--target-pane {target_pane}"
    )
    subprocess.run(["tmux", "send-keys", "-t", new_pane, cmd, "Enter"], check=True)
    subprocess.run(["tmux", "select-pane", "-t", target_pane], check=True)
    return new_pane


def _build_groups_header(state: dict[str, Any], group_ids: list[str]) -> str:
    pairs: list[str] = []
    for gid in group_ids:
        pw = state["groups"].get(gid, {}).get("password", "")
        if pw:
            pairs.append(f"{gid}:{pw}")
    return ",".join(pairs)


def _sync_mcp_config(state: dict[str, Any]) -> bool:
    all_groups = list(state["groups"].keys())
    if not all_groups:
        return False

    settings_path = Path.home() / ".claude" / "settings.local.json"
    settings_path.parent.mkdir(parents=True, exist_ok=True)

    config: dict[str, Any] = {}
    if settings_path.exists():
        try:
            config = json.loads(settings_path.read_text())
        except json.JSONDecodeError:
            config = {}

    groups_header = _build_groups_header(state, all_groups)
    hostname = platform.node().split(".")[0].lower()

    mcp_servers = config.setdefault("mcpServers", {})
    mcp_servers["also-mcp"] = {
        "type": "http",
        "url": f"{state['server_url']}/mcp",
        "headers": {
            "X-Peer-Id": state["peer_id"],
            "X-Groups": groups_header,
            "X-Hostname": hostname,
            "X-Working-Directory": Path.home().as_posix(),
        },
    }

    fd, tmp_path = tempfile.mkstemp(
        dir=settings_path.parent, suffix=".tmp"
    )
    try:
        with os.fdopen(fd, "w") as tmp_file:
            json.dump(config, tmp_file, indent=2)
        os.replace(tmp_path, str(settings_path))
    except BaseException:
        os.unlink(tmp_path)
        raise
    return True


def _join_or_create_group(state: dict[str, Any], current_session_groups: list[str] | None = None) -> str | None:
    server_url = state["server_url"]
    remote_groups = _fetch_groups(server_url)
    session_set = set(current_session_groups or [])

    choices = []
    for gid in remote_groups:
        label = f"{gid}  (joined)" if gid in session_set else gid
        choices.append({"name": label, "value": gid})
    choices.append({"name": "-- Create new group --", "value": "__create__"})

    selected = inquirer.fuzzy(
        message="Select or search for a group:",
        choices=choices,
    ).execute()

    if selected == "__create__":
        group_id = inquirer.text(message="New group name:").execute()
        if not group_id:
            return None
        password = _create_group(server_url, group_id)
        if password is None:
            return None
        state["groups"][group_id] = {"password": password}
        _save_state(state)
        print(f"\n  Created group '{group_id}'")
        print(f"  Password: {password}")
        print("  Share this password with your teammates.")
        return group_id

    group_id = selected
    if group_id in state["groups"]:
        return group_id

    password = inquirer.secret(message=f"Password for '{group_id}':").execute()
    if not _verify_group(server_url, group_id, password):
        print("  Invalid password.", file=sys.stderr)
        return None

    state["groups"][group_id] = {"password": password}
    _save_state(state)
    print(f"  Joined '{group_id}'.")
    return group_id


def _launch_new_claude_session() -> str | None:
    directory = inquirer.filepath(
        message="Directory to start Claude Code in:",
        default=str(Path.cwd()),
        only_directories=True,
    ).execute()
    if not directory:
        return None

    directory = str(Path(directory).resolve())

    has_tmux = subprocess.run(
        ["tmux", "list-sessions"],
        capture_output=True,
    ).returncode == 0

    if has_tmux:
        result = subprocess.run(
            [
                "tmux", "new-window",
                "-P", "-F", "#{session_name}:#{window_index}.#{pane_index}",
            ],
            capture_output=True,
            text=True,
        )
    else:
        result = subprocess.run(
            [
                "tmux", "new-session", "-d", "-s", "also",
                "-P", "-F", "#{session_name}:#{window_index}.#{pane_index}",
            ],
            capture_output=True,
            text=True,
        )

    if result.returncode != 0:
        print(f"  Failed to create tmux session: {result.stderr.strip()}", file=sys.stderr)
        return None

    pane_id = result.stdout.strip()
    subprocess.run(
        ["tmux", "send-keys", "-t", pane_id, f"cd {shlex.quote(directory)} && claude", "Enter"],
        check=True,
    )
    if not has_tmux:
        print(f"  Created tmux session 'also'. Run 'tmux attach -t also' to view it.")
    print(f"  Launched Claude Code in {directory} (pane {pane_id})")
    return pane_id


def _action_setup_session(state: dict[str, Any]) -> None:
    sessions = _discover_claude_sessions()

    session_choices = [
        {"name": f"{s['pane_id']}  (pid {s['pid']})", "value": s["pane_id"]}
        for s in sessions
    ]
    session_choices.append({"name": "-- Launch new Claude Code session --", "value": "__new__"})

    pane_id = inquirer.fuzzy(
        message="Select a Claude Code session:",
        choices=session_choices,
    ).execute()

    if pane_id == "__new__":
        _launch_new_claude_session()
        return

    child = state["children"].setdefault(pane_id, {"groups": []})
    current_groups = set(child["groups"])
    joined_groups = list(state["groups"].keys())

    while True:
        sub_choices = ["Add to a group"]
        if child["groups"]:
            sub_choices.append("Remove from a group")
        if not child.get("tui_pane") and child["groups"]:
            sub_choices.append("Launch TUI sidebar")
        sub_choices.append("Back")

        action = inquirer.select(
            message=f"Session {pane_id} — groups: {', '.join(child['groups']) or '(none)'}",
            choices=sub_choices,
        ).execute()

        if action == "Back" or not action:
            break

        if action == "Add to a group":
            group_id = _join_or_create_group(state, child["groups"])
            if group_id and group_id not in child["groups"]:
                child["groups"].append(group_id)
                _save_state(state)
                _sync_mcp_config(state)
                print(f"  Added to '{group_id}'. MCP config updated — restart Claude Code.\n")

        elif action.startswith("Remove"):
            if not child["groups"]:
                continue
            to_remove = inquirer.select(
                message="Remove from which group?",
                choices=child["groups"],
            ).execute()
            child["groups"].remove(to_remove)
            _save_state(state)
            _sync_mcp_config(state)
            print(f"  Removed from '{to_remove}'. MCP config updated — restart Claude Code.\n")

        elif action.startswith("Launch"):
            if child["groups"]:
                tui_pane = _setup_tui_sidebar(
                    target_pane=pane_id,
                    server_url=state["server_url"],
                    group_id=child["groups"][0],
                    peer_id=state["peer_id"],
                    groups_header=_build_groups_header(state, child["groups"]),
                )
                if tui_pane:
                    child["tui_pane"] = tui_pane
                    _save_state(state)
                    print(f"  TUI running in pane: {tui_pane}\n")


def _action_show_status(state: dict[str, Any]) -> None:
    print(f"\n  Server:  {state['server_url']}")
    print(f"  Groups:  {', '.join(state['groups'].keys()) or '(none)'}")
    if state["children"]:
        print("  Sessions:")
        for pane_id, child in state["children"].items():
            groups = ", ".join(child["groups"]) or "(none)"
            print(f"    {pane_id} -> {groups}")
    print()


def _get_version() -> str:
    from importlib.metadata import version
    return version("alsogc")


def main() -> None:
    if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-v"):
        print(f"alsogc {_get_version()}")
        return

    state = _load_state()
    if "server_url" not in state:
        state["server_url"] = DEFAULT_SERVER_URL
        _save_state(state)

    if state["groups"] and _sync_mcp_config(state):
        print("  MCP config synced.\n")

    while True:
        action = inquirer.select(
            message="What would you like to do?",
            choices=[
                "Setup a Claude Code session",
                "Status",
                "Exit",
            ],
        ).execute()

        if action.startswith("Setup"):
            _action_setup_session(state)
        elif action.startswith("Status"):
            _action_show_status(state)
        else:
            break


if __name__ == "__main__":
    main()
