"""CLI entry point for ClipRoom."""

from __future__ import annotations

import argparse
import asyncio
import logging
import os
from datetime import datetime, timezone
from contextlib import suppress

from . import __version__
from .client import ClipRoomClient
from .cli_state import LocalCLIState, is_pid_running
from .discovery import discover_sessions
from .protocol import DEFAULT_DISCOVERY_PORT, DEFAULT_TCP_PORT, hash_passphrase
from .server import ClipRoomServer


def main() -> None:
    """Run ClipRoom CLI."""
    parser = _build_parser()
    args = parser.parse_args()

    if args.command is None:
        print(f"ClipRoom {__version__}")
        parser.print_help()
        return

    logging.basicConfig(
        level=logging.DEBUG if getattr(args, "verbose", False) else logging.INFO,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    )

    try:
        if args.command == "server" and args.server_command == "start":
            auth_hash = hash_passphrase(args.passphrase or "")
            asyncio.run(
                _run_server_start(
                    host=args.host,
                    port=args.port,
                    fixed_session=args.session,
                    fixed_auth_hash=auth_hash if args.session else None,
                    discovery_port=args.discovery_port,
                    enable_discovery=not args.no_discovery,
                )
            )
            return

        if args.command == "server" and args.server_command == "stop":
            _request_server_stop(wait_seconds=args.wait)
            return

        if args.command == "join":
            asyncio.run(
                _run_join(
                    server_host=args.host,
                    server_port=args.port,
                    session=args.session,
                    client_name=args.name,
                    passphrase=args.passphrase,
                    send_text=args.send,
                    sync_clipboard=not args.no_clipboard,
                    poll_interval=args.poll_interval,
                    heartbeat_interval=args.heartbeat_interval,
                    reconnect=not args.no_reconnect,
                    reconnect_delay=args.reconnect_delay,
                )
            )
            return

        if args.command == "host":
            asyncio.run(
                _run_host(
                    host=args.host,
                    port=args.port,
                    session=args.session,
                    passphrase=args.passphrase,
                    client_name=args.name,
                    send_text=args.send,
                    discovery_port=args.discovery_port,
                    no_discovery=args.no_discovery,
                    sync_clipboard=not args.no_clipboard,
                    poll_interval=args.poll_interval,
                    heartbeat_interval=args.heartbeat_interval,
                    reconnect=not args.no_reconnect,
                    reconnect_delay=args.reconnect_delay,
                )
            )
            return

        if args.command == "discover":
            sessions = asyncio.run(
                discover_sessions(
                    broadcast_host=args.broadcast,
                    discovery_port=args.port,
                    timeout_seconds=args.timeout,
                )
            )
            if not sessions:
                print("No sessions found.")
                return
            for item in sessions:
                locked = " [passphrase]" if item.requires_passphrase else ""
                print(f"{item.session} ({item.host}:{item.port}){locked}")
            return

        if args.command == "status":
            _print_status()
            return

        if args.command == "leave":
            _request_leave(wait_seconds=args.wait)
            return
    except KeyboardInterrupt:
        raise SystemExit(130)

    raise SystemExit("Unsupported command.")


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="cliproom", description="ClipRoom CLI")
    parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")

    subparsers = parser.add_subparsers(dest="command")

    server_parser = subparsers.add_parser("server", help="Server control")
    server_sub = server_parser.add_subparsers(dest="server_command", required=True)

    start = server_sub.add_parser("start", help="Start CRP server")
    start.add_argument("--host", default="0.0.0.0", help="TCP bind host")
    start.add_argument("--port", type=int, default=DEFAULT_TCP_PORT, help="TCP port")
    start.add_argument(
        "--discovery-port",
        type=int,
        default=DEFAULT_DISCOVERY_PORT,
        help="UDP discovery port",
    )
    start.add_argument(
        "--no-discovery",
        action="store_true",
        help="Disable UDP discovery",
    )
    start.add_argument(
        "--session",
        help="Fixed session to allow. If omitted, server accepts multiple sessions.",
    )
    start.add_argument(
        "-p",
        "--passphrase",
        default="",
        help="Fixed session passphrase (only applies with --session).",
    )
    start.add_argument("-v", "--verbose", action="store_true", help="Verbose logs")

    stop = server_sub.add_parser("stop", help="Request local server stop")
    stop.add_argument(
        "--wait",
        type=float,
        default=5.0,
        help="Wait up to N seconds for server shutdown",
    )
    stop.add_argument("-v", "--verbose", action="store_true", help="Verbose logs")

    join = subparsers.add_parser("join", help="Join an existing session")
    join.add_argument("session", help="Session name")
    join.add_argument("--host", default="127.0.0.1", help="Server host")
    join.add_argument("--port", type=int, default=DEFAULT_TCP_PORT, help="TCP port")
    join.add_argument("--name", default="cliproom-client", help="Client name")
    join.add_argument("-p", "--passphrase", default="", help="Session passphrase")
    join.add_argument("--send", help="Send one initial text and then listen")
    join.add_argument(
        "--no-clipboard",
        action="store_true",
        help="Disable local clipboard monitoring",
    )
    join.add_argument(
        "--poll-interval",
        type=float,
        default=0.4,
        help="Clipboard polling interval in seconds",
    )
    join.add_argument(
        "--heartbeat-interval",
        type=float,
        default=10.0,
        help="Heartbeat interval in seconds",
    )
    join.add_argument(
        "--no-reconnect",
        action="store_true",
        help="Disable automatic reconnect",
    )
    join.add_argument(
        "--reconnect-delay",
        type=float,
        default=2.0,
        help="Reconnect delay in seconds",
    )
    join.add_argument("-v", "--verbose", action="store_true", help="Verbose logs")

    host = subparsers.add_parser("host", help="Start local server and join")
    host.add_argument("session", help="Session name")
    host.add_argument("--host", default="0.0.0.0", help="Server bind host")
    host.add_argument("--port", type=int, default=DEFAULT_TCP_PORT, help="TCP port")
    host.add_argument(
        "--discovery-port",
        type=int,
        default=DEFAULT_DISCOVERY_PORT,
        help="UDP discovery port",
    )
    host.add_argument(
        "--no-discovery",
        action="store_true",
        help="Disable UDP discovery",
    )
    host.add_argument("--name", default="cliproom-host", help="Local client name")
    host.add_argument("-p", "--passphrase", default="", help="Session passphrase")
    host.add_argument("--send", help="Send one initial text and then listen")
    host.add_argument(
        "--no-clipboard",
        action="store_true",
        help="Disable local clipboard monitoring",
    )
    host.add_argument(
        "--poll-interval",
        type=float,
        default=0.4,
        help="Clipboard polling interval in seconds",
    )
    host.add_argument(
        "--heartbeat-interval",
        type=float,
        default=10.0,
        help="Heartbeat interval in seconds",
    )
    host.add_argument(
        "--no-reconnect",
        action="store_true",
        help="Disable automatic reconnect",
    )
    host.add_argument(
        "--reconnect-delay",
        type=float,
        default=2.0,
        help="Reconnect delay in seconds",
    )
    host.add_argument("-v", "--verbose", action="store_true", help="Verbose logs")

    discover = subparsers.add_parser("discover", help="Discover LAN sessions")
    discover.add_argument(
        "--broadcast",
        default="255.255.255.255",
        help="Broadcast address",
    )
    discover.add_argument(
        "--port",
        type=int,
        default=DEFAULT_DISCOVERY_PORT,
        help="UDP discovery port",
    )
    discover.add_argument(
        "--timeout",
        type=float,
        default=2.0,
        help="Response timeout in seconds",
    )
    discover.add_argument("-v", "--verbose", action="store_true", help="Verbose logs")

    subparsers.add_parser("status", help="Show local client status")

    leave = subparsers.add_parser("leave", help="Request local client to disconnect")
    leave.add_argument(
        "--wait",
        type=float,
        default=5.0,
        help="Wait up to N seconds for client shutdown",
    )

    return parser


async def _run_join(
    *,
    server_host: str,
    server_port: int,
    session: str,
    client_name: str,
    passphrase: str,
    send_text: str | None,
    sync_clipboard: bool,
    poll_interval: float,
    heartbeat_interval: float,
    reconnect: bool,
    reconnect_delay: float,
) -> None:
    cli_state = LocalCLIState()
    cli_state.clear_leave_requested()
    sent_initial = False
    while True:
        if cli_state.is_leave_requested():
            break
        client = ClipRoomClient(
            server_host=server_host,
            server_port=server_port,
            session=session,
            client_name=client_name,
            passphrase=passphrase,
        )
        try:
            cli_state.write_state(
                _build_state_payload(
                    mode="join",
                    status="connecting",
                    session=session,
                    server_host=server_host,
                    server_port=server_port,
                    client_name=client_name,
                    connected=False,
                    last_error=None,
                )
            )
            await client.connect()
            cli_state.write_state(
                _build_state_payload(
                    mode="join",
                    status="connected",
                    session=session,
                    server_host=server_host,
                    server_port=server_port,
                    client_name=client_name,
                    connected=True,
                    last_error=None,
                )
            )
            if send_text and not sent_initial:
                await client.send_clip_text(send_text)
                sent_initial = True
            run_task = asyncio.create_task(
                client.run_forever(
                    sync_clipboard=sync_clipboard,
                    poll_interval=poll_interval,
                    heartbeat_interval=heartbeat_interval,
                )
            )
            leave_task = asyncio.create_task(_wait_for_leave_request(cli_state))
            done, pending = await asyncio.wait(
                [run_task, leave_task],
                return_when=asyncio.FIRST_COMPLETED,
            )
            for task in pending:
                task.cancel()
            for task in pending:
                with suppress(asyncio.CancelledError):
                    await task
            if leave_task in done:
                logging.getLogger("cliproom.cli").info("Leave requested, disconnecting...")
                await client.close()
                with suppress(asyncio.CancelledError, Exception):
                    await run_task
                break
            exc = run_task.exception()
            if exc is not None:
                raise exc
            if not reconnect:
                break
            cli_state.write_state(
                _build_state_payload(
                    mode="join",
                    status="disconnected",
                    session=session,
                    server_host=server_host,
                    server_port=server_port,
                    client_name=client_name,
                    connected=False,
                    last_error=None,
                )
            )
            logging.getLogger("cliproom.cli").warning(
                "Disconnected from server. Reconnecting in %.1fs...",
                reconnect_delay,
            )
            await asyncio.sleep(reconnect_delay)
        except KeyboardInterrupt:
            raise
        except Exception as exc:
            cli_state.write_state(
                _build_state_payload(
                    mode="join",
                    status="error",
                    session=session,
                    server_host=server_host,
                    server_port=server_port,
                    client_name=client_name,
                    connected=False,
                    last_error=str(exc),
                )
            )
            if not reconnect:
                raise
            logging.getLogger("cliproom.cli").warning(
                "Connection error: %s. Reconnecting in %.1fs...",
                exc,
                reconnect_delay,
            )
            await asyncio.sleep(reconnect_delay)
        finally:
            await client.close()
    cli_state.write_state(
        _build_state_payload(
            mode="join",
            status="stopped",
            session=session,
            server_host=server_host,
            server_port=server_port,
            client_name=client_name,
            connected=False,
            last_error=None,
        )
    )
    cli_state.clear_leave_requested()


async def _run_host(
    *,
    host: str,
    port: int,
    session: str,
    passphrase: str,
    client_name: str,
    send_text: str | None,
    discovery_port: int,
    no_discovery: bool,
    sync_clipboard: bool,
    poll_interval: float,
    heartbeat_interval: float,
    reconnect: bool,
    reconnect_delay: float,
) -> None:
    auth_hash = hash_passphrase(passphrase)
    server = ClipRoomServer(
        host=host,
        port=port,
        fixed_session=session,
        fixed_auth_hash=auth_hash,
        discovery_port=discovery_port,
        enable_discovery=not no_discovery,
    )
    await server.start()

    connect_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
    server_task = asyncio.create_task(server.serve_forever())
    try:
        state = LocalCLIState()
        state.write_state(
            _build_state_payload(
                mode="host",
                status="hosting",
                session=session,
                server_host=connect_host,
                server_port=port,
                client_name=client_name,
                connected=False,
                last_error=None,
            )
        )
        await _run_join(
            server_host=connect_host,
            server_port=port,
            session=session,
            client_name=client_name,
            passphrase=passphrase,
            send_text=send_text,
            sync_clipboard=sync_clipboard,
            poll_interval=poll_interval,
            heartbeat_interval=heartbeat_interval,
            reconnect=reconnect,
            reconnect_delay=reconnect_delay,
        )
    finally:
        server_task.cancel()
        with suppress(asyncio.CancelledError):
            await server_task
        await server.stop()
        state = LocalCLIState()
        state.write_state(
            _build_state_payload(
                mode="host",
                status="stopped",
                session=session,
                server_host=connect_host,
                server_port=port,
                client_name=client_name,
                connected=False,
                last_error=None,
            )
        )


async def _run_server_start(
    *,
    host: str,
    port: int,
    fixed_session: str | None,
    fixed_auth_hash: str | None,
    discovery_port: int,
    enable_discovery: bool,
) -> None:
    state = LocalCLIState()
    state.clear_server_stop_requested()
    server = ClipRoomServer(
        host=host,
        port=port,
        fixed_session=fixed_session,
        fixed_auth_hash=fixed_auth_hash,
        discovery_port=discovery_port,
        enable_discovery=enable_discovery,
    )
    await server.start()
    state.write_server_state(
        _build_server_state_payload(
            status="running",
            host=host,
            port=port,
            discovery_port=discovery_port,
            fixed_session=fixed_session,
            last_error=None,
        )
    )

    serve_task = asyncio.create_task(server.serve_forever(), name="server-serve")
    stop_task = asyncio.create_task(
        _wait_for_server_stop_request(state),
        name="server-stop-request",
    )
    try:
        done, pending = await asyncio.wait(
            [serve_task, stop_task],
            return_when=asyncio.FIRST_COMPLETED,
        )
        if stop_task in done:
            logging.getLogger("cliproom.cli").info("Server stop requested.")
            await server.stop()
            serve_task.cancel()
            with suppress(asyncio.CancelledError):
                await serve_task
        else:
            exc = serve_task.exception()
            if exc is not None:
                raise exc
    except Exception as exc:
        state.write_server_state(
            _build_server_state_payload(
                status="error",
                host=host,
                port=port,
                discovery_port=discovery_port,
                fixed_session=fixed_session,
                last_error=str(exc),
            )
        )
        raise
    finally:
        stop_task.cancel()
        with suppress(asyncio.CancelledError):
            await stop_task
        await server.stop()
        state.write_server_state(
            _build_server_state_payload(
                status="stopped",
                host=host,
                port=port,
                discovery_port=discovery_port,
                fixed_session=fixed_session,
                last_error=None,
            )
        )
        state.clear_server_stop_requested()


def _utc_now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _build_state_payload(
    *,
    mode: str,
    status: str,
    session: str,
    server_host: str,
    server_port: int,
    client_name: str,
    connected: bool,
    last_error: str | None,
) -> dict[str, object]:
    return {
        "active": status not in {"stopped"},
        "mode": mode,
        "status": status,
        "session": session,
        "server_host": server_host,
        "server_port": server_port,
        "client_name": client_name,
        "connected": connected,
        "pid": os.getpid(),
        "last_error": last_error,
        "updated_at": _utc_now(),
    }


async def _wait_for_leave_request(state: LocalCLIState, interval: float = 0.5) -> None:
    while True:
        if state.is_leave_requested():
            return
        await asyncio.sleep(interval)


async def _wait_for_server_stop_request(state: LocalCLIState, interval: float = 0.5) -> None:
    while True:
        if state.is_server_stop_requested():
            return
        await asyncio.sleep(interval)


def _print_status() -> None:
    state = LocalCLIState()
    client = state.read_state()
    server = state.read_server_state()

    if client is None and server is None:
        print("No local runtime state found.")
        return

    print("[client]")
    if client is None:
        print("state: missing")
    else:
        _print_state_block(
            {
                "active": client.get("active", False),
                "mode": client.get("mode", "unknown"),
                "status": client.get("status", "unknown"),
                "session": client.get("session", "-"),
                "endpoint": f"{client.get('server_host', '-')}:{client.get('server_port', '-')}",
                "name": client.get("client_name", "-"),
                "pid": client.get("pid", 0),
                "updated_at": client.get("updated_at", "-"),
                "last_error": client.get("last_error"),
            }
        )

    print("[server]")
    if server is None:
        print("state: missing")
    else:
        _print_state_block(
            {
                "active": server.get("active", False),
                "mode": "server",
                "status": server.get("status", "unknown"),
                "session": server.get("fixed_session") or "-",
                "endpoint": f"{server.get('host', '-')}:{server.get('port', '-')}",
                "name": "cliproom-server",
                "pid": server.get("pid", 0),
                "updated_at": server.get("updated_at", "-"),
                "last_error": server.get("last_error"),
            }
        )


def _print_state_block(payload: dict[str, object]) -> None:
    pid = int(payload.get("pid", 0) or 0)
    running = is_pid_running(pid)
    print(f"active: {payload.get('active', False)}")
    print(f"mode: {payload.get('mode', 'unknown')}")
    print(f"status: {payload.get('status', 'unknown')}")
    print(f"session: {payload.get('session', '-')}")
    print(f"endpoint: {payload.get('endpoint', '-')}")
    print(f"name: {payload.get('name', '-')}")
    print(f"pid: {pid} ({'running' if running else 'not running'})")
    print(f"updated_at: {payload.get('updated_at', '-')}")
    last_error = payload.get("last_error")
    if last_error:
        print(f"last_error: {last_error}")


def _request_leave(*, wait_seconds: float) -> None:
    state = LocalCLIState()
    payload = state.read_state()
    if payload is None:
        print("No local client state found.")
        return
    if not payload.get("active", False):
        print("Local client is not active.")
        return

    state.set_leave_requested()
    print("Leave requested.")
    if wait_seconds <= 0:
        return

    loop = asyncio.new_event_loop()
    try:
        loop.run_until_complete(_wait_for_client_stop(timeout=wait_seconds))
    finally:
        loop.close()


def _request_server_stop(*, wait_seconds: float) -> None:
    state = LocalCLIState()
    payload = state.read_server_state()
    if payload is None:
        print("No local server state found.")
        return
    if payload.get("status") != "running":
        print("Local server is not running.")
        return

    state.set_server_stop_requested()
    print("Server stop requested.")
    if wait_seconds <= 0:
        return

    loop = asyncio.new_event_loop()
    try:
        loop.run_until_complete(_wait_for_server_shutdown(timeout=wait_seconds))
    finally:
        loop.close()


async def _wait_for_client_stop(*, timeout: float) -> None:
    state = LocalCLIState()
    deadline = asyncio.get_running_loop().time() + timeout
    while asyncio.get_running_loop().time() < deadline:
        payload = state.read_state() or {}
        pid = int(payload.get("pid", 0) or 0)
        active = bool(payload.get("active", False))
        if (not active) or (pid and not is_pid_running(pid)):
            print("Client stopped.")
            return
        await asyncio.sleep(0.2)
    print("Timed out waiting for client shutdown.")


async def _wait_for_server_shutdown(*, timeout: float) -> None:
    state = LocalCLIState()
    deadline = asyncio.get_running_loop().time() + timeout
    while asyncio.get_running_loop().time() < deadline:
        payload = state.read_server_state() or {}
        pid = int(payload.get("pid", 0) or 0)
        status = payload.get("status")
        if status in {"stopped", "error"} or (pid and not is_pid_running(pid)):
            print("Server stopped.")
            return
        await asyncio.sleep(0.2)
    print("Timed out waiting for server shutdown.")


def _build_server_state_payload(
    *,
    status: str,
    host: str,
    port: int,
    discovery_port: int,
    fixed_session: str | None,
    last_error: str | None,
) -> dict[str, object]:
    return {
        "active": status == "running",
        "status": status,
        "host": host,
        "port": port,
        "discovery_port": discovery_port,
        "fixed_session": fixed_session,
        "pid": os.getpid(),
        "last_error": last_error,
        "updated_at": _utc_now(),
    }


if __name__ == "__main__":
    main()
