#!/usr/bin/env python3
"""原型评审静态服务：多线程 + gzip。

替代原来的 `python3 -m http.server 3000`。那个是单线程的，浏览器并发拉
几个大文件时会互相排队，页面表现为「打不开」；而且不压缩，一个 4MB 的
data.js 在 300KB/s 的出口带宽下要跑十几秒。

行为与 http.server 完全兼容：同一个目录根、同样的 URL 结构、同样的目录索引，
只是加了线程和 gzip，对目录下已有的原型只增不减。

用法：static_server.py [端口] [根目录]
"""

from __future__ import annotations

import gzip
import os
import sys
import threading
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer

PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 3000
ROOT = sys.argv[2] if len(sys.argv) > 2 else "/opt/prototype"

# 值得压的类型：文本类压缩比 3~5 倍；图片视频已经是压缩格式，再压纯属浪费 CPU
ZIP_EXT = {".html", ".htm", ".js", ".mjs", ".css", ".json", ".svg", ".txt", ".map", ".xml"}

_cache: dict[str, tuple[float, bytes]] = {}
_lock = threading.Lock()


def gzipped(path: str, mtime: float, raw: bytes) -> bytes:
    """按 (路径, mtime) 缓存压缩结果，避免每次请求都重压 4MB。"""
    key = f"{path}:{mtime}"
    with _lock:
        hit = _cache.get(key)
        if hit and hit[0] == mtime:
            return hit[1]
    blob = gzip.compress(raw, 6)
    with _lock:
        if len(_cache) > 64:
            _cache.clear()
        _cache[key] = (mtime, blob)
    return blob


class Handler(SimpleHTTPRequestHandler):
    server_version = "PrototypeStatic/1.0"

    def __init__(self, *a, **kw):
        super().__init__(*a, directory=ROOT, **kw)

    def _serve(self, body: bool) -> None:
        path = self.translate_path(self.path)
        if os.path.isdir(path):
            index = os.path.join(path, "index.html")
            if os.path.isfile(index):
                if not self.path.endswith("/"):
                    # 目录必须带尾斜杠，否则页面里的相对路径会解析到上一层
                    self.send_response(301)
                    self.send_header("Location", self.path + "/")
                    self.end_headers()
                    return
                path = index
            else:
                super().do_GET() if body else super().do_HEAD()
                return
        if not os.path.isfile(path):
            self.send_error(404, "File not found")
            return

        try:
            st = os.stat(path)
            with open(path, "rb") as f:
                raw = f.read()
        except OSError:
            self.send_error(404, "File not found")
            return

        ctype = self.guess_type(path)
        payload = raw
        encoding = None
        if (
            os.path.splitext(path)[1].lower() in ZIP_EXT
            and "gzip" in self.headers.get("Accept-Encoding", "")
            and len(raw) > 1024
        ):
            payload = gzipped(path, st.st_mtime, raw)
            encoding = "gzip"

        self.send_response(200)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(payload)))
        if encoding:
            self.send_header("Content-Encoding", encoding)
            self.send_header("Vary", "Accept-Encoding")
        self.send_header("Last-Modified", self.date_time_string(int(st.st_mtime)))
        self.send_header("Cache-Control", "no-cache")
        self.end_headers()
        if body:
            self.wfile.write(payload)

    def do_GET(self) -> None:
        self._serve(True)

    def do_HEAD(self) -> None:
        self._serve(False)

    def log_message(self, fmt, *args):  # 静音常规访问日志，只保留错误
        if args and str(args[0]).startswith(("4", "5")):
            sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args))


class Server(ThreadingHTTPServer):
    daemon_threads = True
    allow_reuse_address = True


if __name__ == "__main__":
    if not os.path.isdir(ROOT):
        sys.stderr.write(f"root missing: {ROOT}\n")
        sys.exit(1)
    sys.stderr.write(f"serving {ROOT} on 0.0.0.0:{PORT} (threaded, gzip)\n")
    Server(("0.0.0.0", PORT), Handler).serve_forever()
