#!/usr/bin/env python3
"""Serve the design folder for the clickable prototype.

Two things `python3 -m http.server` lacks: it sends no cache headers (browsers keep stale
CSS and JS between edits) and it ignores byte ranges (Safari will not play video without
them). This handler revalidates every response and answers single-range requests.

Usage:  python3 tools/serve.py [--port 8765] [--host 127.0.0.1]
Then open http://127.0.0.1:8765/prototype/
"""
from __future__ import annotations

import argparse
import functools
import re
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import BinaryIO

ROOT = Path(__file__).resolve().parent.parent
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8765
RANGE_RE = re.compile(r"bytes=(\d*)-(\d*)$")


class RangeSlice:
    """File object limited to one byte range, read by SimpleHTTPRequestHandler.copyfile."""

    def __init__(self, handle: BinaryIO, length: int) -> None:
        self._handle = handle
        self._left = length

    def read(self, size: int = -1) -> bytes:
        if self._left <= 0:
            return b""
        chunk = self._handle.read(self._left if size < 0 else min(size, self._left))
        self._left -= len(chunk)
        return chunk

    def close(self) -> None:
        self._handle.close()


class PrototypeHandler(SimpleHTTPRequestHandler):
    def end_headers(self) -> None:
        self.send_header("Cache-Control", "no-cache")
        self.send_header("Accept-Ranges", "bytes")
        super().end_headers()

    def send_head(self):  # noqa: ANN201 (same contract as the base class)
        match = RANGE_RE.match(self.headers.get("Range", "").strip())
        path = Path(self.translate_path(self.path))
        if not match or not path.is_file() or not any(match.groups()):
            return super().send_head()

        size = path.stat().st_size
        first, last = match.groups()
        if first:
            start, end = int(first), min(int(last) if last else size - 1, size - 1)
        else:
            start, end = max(size - int(last), 0), size - 1
        if start > end:
            self.send_response(HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
            self.send_header("Content-Range", f"bytes */{size}")
            self.end_headers()
            return None

        handle = path.open("rb")
        handle.seek(start)
        self.send_response(HTTPStatus.PARTIAL_CONTENT)
        self.send_header("Content-Type", self.guess_type(str(path)))
        self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
        self.send_header("Content-Length", str(end - start + 1))
        self.end_headers()
        return RangeSlice(handle, end - start + 1)


def main() -> None:
    parser = argparse.ArgumentParser(description="Serve the SKAP design folder.")
    parser.add_argument("--host", default=DEFAULT_HOST, help="use 0.0.0.0 to test from a phone on the same network")
    parser.add_argument("--port", type=int, default=DEFAULT_PORT)
    args = parser.parse_args()
    handler = functools.partial(PrototypeHandler, directory=str(ROOT))
    with ThreadingHTTPServer((args.host, args.port), handler) as server:
        print(f"SKAP prototype: http://{args.host}:{args.port}/prototype/  (Ctrl+C to stop)", flush=True)
        try:
            server.serve_forever()
        except KeyboardInterrupt:
            pass


if __name__ == "__main__":
    main()
