"""Kiosk auth: password gate for the Cortana voice kiosk (WAN-ready).

Password NEVER stored in plaintext here: kiosk-pass.json holds PBKDF2-HMAC-SHA256
(salt+hash). Sessions are stateless HMAC-signed cookies (secret in kiosk-secret.bin,
created on first use) so family logins survive kiosk restarts for 90 days.
Rate limiter is per-IP in memory (reset on server restart, fine).

Files (C:\\oracle, NEVER included in share snapshots / docs / zips):
  kiosk-pass.json   - salt+hash of the password (pbkdf2)
  kiosk-secret.bin  - 32B random HMAC key for session cookies
  kiosk-pass.txt    - PLAINTEXT copy, read ONLY by e2e_test.py (local automation)
"""
import base64, hashlib, hmac, json, os, secrets, time

_DIR = os.path.dirname(os.path.abspath(__file__))
HASH_FILE = os.path.join(_DIR, "kiosk-pass.json")
SECRET_FILE = os.path.join(_DIR, "kiosk-secret.bin")
COOKIE = "oracle_auth"
_ITER = 240_000
MAX_AGE = 90 * 86400          # 90-day sliding-ish login (fixed from issue time)

# ---------------------------------------------------------------- password ---
def setup_password(plain: str):
    """One-time: write kiosk-pass.json from the plaintext password."""
    salt = secrets.token_bytes(16)
    h = hashlib.pbkdf2_hmac("sha256", plain.encode(), salt, _ITER)
    with open(HASH_FILE, "w") as f:
        json.dump({"salt": salt.hex(), "hash": h.hex(), "iter": _ITER}, f)
    return True

def _load_hash():
    try:
        with open(HASH_FILE) as f:
            d = json.load(f)
        return bytes.fromhex(d["salt"]), bytes.fromhex(d["hash"]), int(d["iter"])
    except Exception:
        return None

def check_password(plain: str) -> bool:
    d = _load_hash()
    if d is None:
        return False                       # fail closed if not configured
    salt, want, iters = d
    got = hashlib.pbkdf2_hmac("sha256", plain.encode(), salt, iters)
    return hmac.compare_digest(got, want)

# ---------------------------------------------------------------- sessions ----
def _secret() -> bytes:
    if os.path.exists(SECRET_FILE):
        with open(SECRET_FILE, "rb") as f:
            return f.read()
    s = secrets.token_bytes(32)
    with open(SECRET_FILE, "wb") as f:
        f.write(s)
    return s

def issue_cookie() -> str:
    exp = int(time.time()) + MAX_AGE
    msg = f"{exp}:{secrets.token_hex(8)}"
    sig = hmac.new(_secret(), msg.encode(), hashlib.sha256).hexdigest()
    return msg + ":" + sig

def verify_cookie(value) -> bool:
    try:
        msg, sig = value.rsplit(":", 1)
        exp = int(msg.split(":", 1)[0])
        want = hmac.new(_secret(), msg.encode(), hashlib.sha256).hexdigest()
        if not hmac.compare_digest(sig, want):
            return False
        return time.time() < exp
    except Exception:
        return False

# ------------------------------------------------------------ rate limiting ---
class _Rate:
    def __init__(self):
        self.fails = {}        # ip -> consecutive failures
        self.blocked = {}      # ip -> block_until ts

    def ok(self, ip) -> bool:
        if self.blocked.get(ip, 0) > time.time():
            return False
        return True

    def fail(self, ip):
        n = self.fails.get(ip, 0) + 1
        self.fails[ip] = n
        if n >= 5:                              # 5 strikes -> escalating block
            self.blocked[ip] = time.time() + min(600, 30 * (2 ** (n - 5)))

    def reset(self, ip):
        self.fails.pop(ip, None)
        self.blocked.pop(ip, None)

limiter = _Rate()
