from __future__ import annotations

import json
import os
from pathlib import Path
import signal
import shutil
import socket
import subprocess
import sys
import tempfile
import time
from uuid import uuid4
import webbrowser

from .payload.types import AgentReviewPayload

LOCAL_SERVER_HOST = "127.0.0.1"
LOCAL_SERVER_STARTUP_TIMEOUT_SECONDS = 45.0
LOCAL_SERVER_POLL_INTERVAL_SECONDS = 0.5
LOCAL_SERVER_SHUTDOWN_TIMEOUT_SECONDS = 5.0
LOCAL_PAYLOAD_FILENAME = "payload.json"
LOCAL_REVIEW_PATH = "/review/local"
LOCAL_PAYLOAD_ENV_VAR = "AGENTREVIEW_LOCAL_PAYLOAD_PATH"
LOCAL_SESSION_ENV_VAR = "AGENTREVIEW_LOCAL_SESSION_ID"


class LocalUiError(RuntimeError):
    pass


def serve_local_review(payload: AgentReviewPayload) -> None:
    workspace_root = _find_workspace_root()
    if workspace_root is None:
        raise LocalUiError(
            "Unable to locate the web app workspace. --local currently requires a checkout of the agentreview repository."
        )

    if shutil.which("pnpm") is None:
        raise LocalUiError("Unable to find `pnpm` in PATH. Install pnpm to use --local.")

    web_dir = workspace_root / "packages" / "web"
    if not (web_dir / "package.json").is_file():
        raise LocalUiError(f"Unable to locate the web app at {web_dir}.")

    port = _reserve_port()
    url = f"http://{LOCAL_SERVER_HOST}:{port}{LOCAL_REVIEW_PATH}"
    session_id = f"local-{uuid4().hex}"

    with tempfile.TemporaryDirectory(prefix="agentreview-local-") as temp_dir:
        payload_path = Path(temp_dir) / LOCAL_PAYLOAD_FILENAME
        payload_path.write_text(
            json.dumps(payload.to_dict(), separators=(",", ":")),
            encoding="utf-8",
        )

        process = subprocess.Popen(
            [
                "pnpm",
                "exec",
                "next",
                "dev",
                "--hostname",
                LOCAL_SERVER_HOST,
                "--port",
                str(port),
            ],
            cwd=web_dir,
            env=_build_local_server_env(payload_path, session_id),
        )

        try:
            _wait_for_local_server(process, port)
            print(f"Local review UI: {url}", file=sys.stderr)
            print("Press Ctrl-C to stop the local server.", file=sys.stderr)
            opened = webbrowser.open(url)
            if not opened:
                print(f"Open this URL in your browser: {url}", file=sys.stderr)
            returncode = process.wait()
            if returncode not in (0, -signal.SIGINT):
                raise LocalUiError(f"The local web server exited with status {returncode}.")
        except KeyboardInterrupt:
            pass
        finally:
            _terminate_process(process)


def _build_local_server_env(payload_path: Path, session_id: str) -> dict[str, str]:
    env = os.environ.copy()
    env[LOCAL_PAYLOAD_ENV_VAR] = str(payload_path)
    env[LOCAL_SESSION_ENV_VAR] = session_id
    return env


def _find_workspace_root() -> Path | None:
    current = Path(__file__).resolve()
    for parent in current.parents:
        if (parent / "pnpm-workspace.yaml").is_file() and (
            parent / "packages" / "web" / "package.json"
        ).is_file():
            return parent
    return None


def _reserve_port() -> int:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.bind((LOCAL_SERVER_HOST, 0))
        return int(sock.getsockname()[1])


def _wait_for_local_server(process: subprocess.Popen[bytes], port: int) -> None:
    deadline = time.monotonic() + LOCAL_SERVER_STARTUP_TIMEOUT_SECONDS
    while time.monotonic() < deadline:
        if process.poll() is not None:
            raise LocalUiError("The local web server exited before it became ready.")

        if _is_server_listening(port):
            return

        time.sleep(LOCAL_SERVER_POLL_INTERVAL_SECONDS)

    _terminate_process(process)
    raise LocalUiError("Timed out waiting for the local web server to start.")


def _is_server_listening(port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.settimeout(1)
        try:
            sock.connect((LOCAL_SERVER_HOST, port))
            return True
        except OSError:
            return False


def _terminate_process(process: subprocess.Popen[bytes]) -> None:
    if process.poll() is not None:
        return

    process.terminate()
    try:
        process.wait(timeout=LOCAL_SERVER_SHUTDOWN_TIMEOUT_SECONDS)
    except subprocess.TimeoutExpired:
        process.kill()
        process.wait()
