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


# Failed logins, per client. Six digits is a million possibilities, which
# is plenty against someone typing and nothing at all against someone
# scripting — and the app ships its own source, so the scheme is not a
# secret. The code stays short enough to read aloud; guessing gets slow
# instead.
_fails: dict = {}
FREE_TRIES = 5
MAX_DELAY_S = 30.0
FORGET_AFTER_S = 3600.0


def login_delay(client: str) -> float:
    """How long this client waits before its next guess is even considered."""
    import time
    rec = _fails.get(client)
    if not rec:
        return 0.0
    count, last = rec
    if time.time() - last > FORGET_AFTER_S:
        _fails.pop(client, None)
        return 0.0
    if count <= FREE_TRIES:
        return 0.0
    return min(MAX_DELAY_S, 0.5 * (2 ** (count - FREE_TRIES - 1)))


def note_failure(client: str) -> int:
    import time
    count, _ = _fails.get(client, (0, 0.0))
    _fails[client] = (count + 1, time.time())
    return count + 1


def note_success(client: str):
    _fails.pop(client, None)


def lan_code(secret: str) -> str:
    """Six digits — long enough to not be guessed casually over a LAN,
    short enough to read aloud. Brute force is handled by login_delay,
    not by length."""
    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>"""
