from __future__ import annotations

import errno
from functools import partial
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import shutil
import subprocess
import sys
import tarfile
import tempfile
from urllib.parse import parse_qs, unquote, urlsplit
from uuid import uuid4
import webbrowser

from .payload.types import AgentReviewPayload

LOCAL_SERVER_HOST = "127.0.0.1"
LOCAL_SERVER_START_PORT = 44101
LOCAL_SERVER_POLL_INTERVAL_SECONDS = 0.5
LOCAL_REVIEW_PATH = "/review/local"
LOCAL_PAYLOAD_ENDPOINT = "/__agentreview__/payload"
LOCAL_UI_ARCHIVE_NAME = "local_ui_assets.tar.gz"


class LocalUiError(RuntimeError):
    pass


class _LocalUiRequestHandler(SimpleHTTPRequestHandler):
    def __init__(
        self,
        *args,
        directory: str,
        payload_response: bytes,
        **kwargs,
    ) -> None:
        self._payload_response = payload_response
        super().__init__(*args, directory=directory, **kwargs)

    def do_GET(self) -> None:
        if self._maybe_serve_payload(send_body=True):
            return
        self._rewrite_static_path()
        super().do_GET()

    def do_HEAD(self) -> None:
        if self._maybe_serve_payload(send_body=False):
            return
        self._rewrite_static_path()
        super().do_HEAD()

    def log_message(self, format: str, *args) -> None:
        return

    def _maybe_serve_payload(self, *, send_body: bool) -> bool:
        if urlsplit(self.path).path != LOCAL_PAYLOAD_ENDPOINT:
            return False

        self.send_response(HTTPStatus.OK)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Length", str(len(self._payload_response)))
        self.end_headers()
        if send_body:
            self.wfile.write(self._payload_response)
        return True

    def _rewrite_static_path(self) -> None:
        directory = Path(self.directory)
        split = urlsplit(self.path)
        request_path = unquote(split.path)
        resolved = _resolve_static_request_path(
            directory,
            request_path,
            prefer_flight_data="_rsc" in parse_qs(split.query, keep_blank_values=True),
        )
        if resolved is not None:
            self.path = f"/{resolved}"


def serve_local_review(payload: AgentReviewPayload) -> None:
    archive_path = _find_packaged_site_archive()
    if archive_path is not None:
        with tempfile.TemporaryDirectory(prefix="agentreview-local-") as temp_dir:
            root_dir = Path(temp_dir)
            _extract_site_archive(archive_path, root_dir)
            _serve_static_site(payload, root_dir / "site")
        return

    workspace_root = _find_workspace_root()
    if workspace_root is None:
        raise LocalUiError(
            "Unable to locate bundled local UI assets or an agentreview repository checkout."
        )

    site_dir = _build_workspace_site(workspace_root)
    _serve_static_site(payload, site_dir)


def _serve_static_site(payload: AgentReviewPayload, site_dir: Path) -> None:
    if not site_dir.is_dir():
        raise LocalUiError(f"Unable to locate local UI files at {site_dir}.")

    session_id = f"local-{uuid4().hex}"
    payload_response = json.dumps(
        {
            "payload": payload.to_dict(),
            "sessionId": session_id,
        },
        separators=(",", ":"),
    ).encode("utf-8")

    handler = partial(
        _LocalUiRequestHandler,
        directory=str(site_dir),
        payload_response=payload_response,
    )
    server = _start_http_server(handler)

    try:
        url = f"http://{LOCAL_SERVER_HOST}:{server.server_address[1]}{LOCAL_REVIEW_PATH}"
        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)
        server.serve_forever(poll_interval=LOCAL_SERVER_POLL_INTERVAL_SECONDS)
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()


def _start_http_server(handler: type[SimpleHTTPRequestHandler] | partial) -> ThreadingHTTPServer:
    for port in range(LOCAL_SERVER_START_PORT, 65536):
        try:
            return ThreadingHTTPServer((LOCAL_SERVER_HOST, port), handler)
        except OSError as exc:
            if exc.errno == errno.EADDRINUSE:
                continue
            raise

    raise LocalUiError(
        f"Unable to find an open local port starting from {LOCAL_SERVER_START_PORT}."
    )


def _find_packaged_site_archive() -> Path | None:
    archive_path = Path(__file__).with_name(LOCAL_UI_ARCHIVE_NAME)
    return archive_path if archive_path.is_file() else None


def _extract_site_archive(archive_path: Path, destination: Path) -> None:
    with tarfile.open(archive_path, "r:gz") as archive:
        archive.extractall(destination)


def _resolve_static_request_path(
    directory: Path,
    request_path: str,
    *,
    prefer_flight_data: bool = False,
) -> str | None:
    normalized = request_path.rstrip("/") or "/"
    relative = normalized.lstrip("/")
    candidates: list[str] = []

    if not relative:
        if prefer_flight_data:
            candidates.append("index.txt")
        candidates.append("index.html")
    else:
        candidates.append(relative)
        if "." not in Path(relative).name:
            route_candidates: list[str] = []
            if prefer_flight_data:
                route_candidates.extend([f"{relative}.txt", f"{relative}/index.txt"])
            route_candidates.extend([f"{relative}.html", f"{relative}/index.html"])
            candidates = [*route_candidates, *candidates]

    for candidate in candidates:
        if (directory / candidate).is_file():
            return candidate

    return None


def _build_workspace_site(workspace_root: Path) -> Path:
    if shutil.which("pnpm") is None:
        raise LocalUiError("Unable to find `pnpm` in PATH. Install pnpm to use --local from a checkout.")

    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}.")

    shutil.rmtree(web_dir / ".next", ignore_errors=True)
    shutil.rmtree(web_dir / "out", ignore_errors=True)
    subprocess.run(
        ["pnpm", "--dir", str(workspace_root), "--filter", "@agentreview/web", "build"],
        check=True,
    )
    site_dir = web_dir / "out"
    if not site_dir.is_dir():
        raise LocalUiError(f"Expected static local UI output at {site_dir}.")
    return site_dir


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
