"""Relay HTTP server. Stores opaque (client-side encrypted) chunks on disk.

API (all except /health need  Authorization: Bearer <token>):
  GET    /health
  PUT    /transfers/<id>/chunks/<n>     body = chunk bytes
  PUT    /transfers/<id>/meta           body = JSON (name, size, chunks...)
  GET    /transfers/<id>                -> meta + list of received chunks
  GET    /transfers/<id>/chunks/<n>
  DELETE /transfers/<id>
"""
from __future__ import annotations

import hmac
import json
import logging
import re
import shutil
import ssl
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

from app.config import RelayConfig

log = logging.getLogger("relay")
ID_RE = re.compile(r"^[A-Za-z0-9_-]{8,64}$")
ROUTE = re.compile(r"^/transfers/([^/]+)(?:/(meta)|/chunks/(\d{1,9}))?$")


def make_handler(cfg: RelayConfig, root: Path):
    max_bytes = cfg.max_chunk_mb * 1024 * 1024

    class Handler(BaseHTTPRequestHandler):
        server_version = "AutoTransferRelay"

        def log_message(self, fmt, *args):
            log.info("%s - %s", self.address_string(), fmt % args)

        def _send(self, code, body=b"", ctype="application/json"):
            self.send_response(code)
            self.send_header("Content-Type", ctype)
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)

        def _json(self, code, obj):
            self._send(code, json.dumps(obj).encode())

        def _authed(self) -> bool:
            header = self.headers.get("Authorization", "")
            token = header[7:] if header.startswith("Bearer ") else ""
            return hmac.compare_digest(token.encode(), cfg.auth_token.encode())

        def _route(self):
            if self.path == "/health":
                return "health", None, None
            if not self._authed():
                self._json(401, {"error": "unauthorized"})
                return None
            m = ROUTE.match(self.path.split("?")[0])
            if not m or not ID_RE.match(m.group(1)):
                self._json(404, {"error": "not found"})
                return None
            return "t", m.group(1), (m.group(2), m.group(3))

        def do_GET(self):
            r = self._route()
            if not r:
                return
            if r[0] == "health":
                return self._json(200, {"status": "ok"})
            tid, (meta, chunk) = r[1], r[2]
            d = root / tid
            if not d.is_dir():
                return self._json(404, {"error": "unknown transfer"})
            if chunk is not None:
                f = d / f"{int(chunk):09d}.chunk"
                if not f.exists():
                    return self._json(404, {"error": "chunk missing"})
                return self._send(200, f.read_bytes(), "application/octet-stream")
            m = d / "meta.json"
            info = json.loads(m.read_text()) if m.exists() else {}
            info["received"] = sorted(int(p.stem) for p in d.glob("*.chunk"))
            self._json(200, info)

        def do_PUT(self):
            r = self._route()
            if not r or r[0] == "health":
                return
            tid, (meta, chunk) = r[1], r[2]
            length = int(self.headers.get("Content-Length", "0"))
            if length <= 0 or length > max_bytes:
                return self._json(413, {"error": "bad or too large body"})
            body = self.rfile.read(length)
            d = root / tid
            d.mkdir(parents=True, exist_ok=True)
            if meta:
                try:
                    json.loads(body)
                except ValueError:
                    return self._json(400, {"error": "invalid json"})
                (d / "meta.json").write_bytes(body)
            elif chunk is not None:
                tmp = d / f"{int(chunk):09d}.tmp"
                tmp.write_bytes(body)
                tmp.replace(d / f"{int(chunk):09d}.chunk")
            else:
                return self._json(404, {"error": "not found"})
            self._json(200, {"ok": True})

        def do_DELETE(self):
            r = self._route()
            if not r or r[0] == "health":
                return
            shutil.rmtree(root / r[1], ignore_errors=True)
            self._json(200, {"ok": True})

    return Handler


def cleanup_loop(root: Path, hours: int):
    while True:
        cutoff = time.time() - hours * 3600
        for d in root.iterdir():
            try:
                if d.is_dir() and d.stat().st_mtime < cutoff:
                    shutil.rmtree(d, ignore_errors=True)
                    log.info("expired transfer %s removed", d.name)
            except OSError:
                pass
        time.sleep(3600)


def serve(cfg: RelayConfig) -> int:
    root = Path(cfg.data_dir)
    root.mkdir(parents=True, exist_ok=True)
    httpd = ThreadingHTTPServer((cfg.host, int(cfg.port)), make_handler(cfg, root))
    scheme = "http"
    if cfg.ssl_cert_path:
        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        ctx.load_cert_chain(cfg.ssl_cert_path, cfg.ssl_key_path)
        httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
        scheme = "https"
    threading.Thread(target=cleanup_loop, args=(root, cfg.retention_hours), daemon=True).start()
    log.info("relay listening on %s://%s:%s", scheme, cfg.host, cfg.port)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        log.info("shutting down")
    finally:
        httpd.server_close()
    return 0
