"""Access control for network mode.

Localhost is trusted: the app is a single-user desktop program and the UI
must never nag the person sitting at the machine. But `--lan` puts the same
UI — recordings, clips, settings, share credentials — on the network, so
requests arriving from anywhere else must prove they know the LAN code.

The code is a short, readable per-install secret (derived from the same
secret as the GSI token, so nothing new to store). Entering it once sets a
cookie; the GSI ingest endpoints keep their own token so game configs and
button boxes are unaffected.
"""
import hashlib
import hmac
import ipaddress

from fastapi import Request
from fastapi.responses import HTMLResponse, JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

COOKIE = "lan_ok"
# paths that must stay reachable without the cookie: the game POSTs (token
# authed), the login page itself, and the assets that page needs
OPEN_PREFIXES = ("/api/gsi/", "/api/clipmark", "/api/chaptermark",
                 "/api/lan/login", "/css/", "/fonts/", "/js/", "/favicon")


def lan_code(secret: str) -> str:
    """Six digits — long enough to not be guessed casually over a LAN,
    short enough to read aloud."""
    h = hashlib.sha256(f"lan:{secret}".encode()).hexdigest()
    return str(int(h[:8], 16) % 1_000_000).zfill(6)


def _is_local(host: str) -> bool:
    try:
        return ipaddress.ip_address(host).is_loopback
    except ValueError:
        return False


class LanAuthMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, secret: str):
        super().__init__(app)
        self.code = lan_code(secret)
        self.token = hashlib.sha256(f"lancookie:{secret}".encode()).hexdigest()[:32]

    async def dispatch(self, request: Request, call_next):
        client = request.client.host if request.client else ""
        if _is_local(client):
            return await call_next(request)
        path = request.url.path
        if path.startswith(OPEN_PREFIXES):
            return await call_next(request)
        if hmac.compare_digest(request.cookies.get(COOKIE, ""), self.token):
            return await call_next(request)
        if path.startswith("/api/"):
            return JSONResponse({"detail": "this install requires the LAN code"},
                                status_code=401)
        return HTMLResponse(_LOGIN, status_code=401)


_LOGIN = """<!DOCTYPE html><html><head><meta charset="utf-8">
<title>enter the code</title><link rel="stylesheet" href="/css/core.css">
<style>body{display:flex;align-items:center;justify-content:center;min-height:100vh}
form{background:var(--surface);border:1px solid var(--border);border-radius:16px;
padding:28px 30px;max-width:380px;text-align:center}
h1{font-size:1.1rem;margin-bottom:6px}p{color:var(--text-dim);font-size:.8rem;line-height:1.6}
input{background:var(--card);border:1px solid var(--border);border-radius:10px;color:var(--text);
font:inherit;font-size:1.2rem;letter-spacing:.3em;padding:10px 14px;width:170px;text-align:center;
margin:14px 0}button{background:var(--primary);border:0;border-radius:16px;color:#1a1206;
font-weight:700;padding:9px 20px;cursor:pointer}</style></head><body>
<form onsubmit="go(event)"><h1>enter the code</h1>
<p>This recording library is being shared on your network.<br>
The code is on the machine running it, under Settings.</p>
<input id="c" inputmode="numeric" maxlength="6" placeholder="000000" autofocus>
<div><button>unlock</button></div><p id="e" style="color:var(--accent)"></p></form>
<script>async function go(ev){ev.preventDefault();
const r=await fetch('/api/lan/login',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({code:document.getElementById('c').value.trim()})});
if(r.ok){location.reload();}else{document.getElementById('e').textContent='that code is not right';}}
</script></body></html>"""
