"""Admin gate — per-member TOTP auth for the hidden /admin zone.

Each project member enrolls their own authenticator (own secret, own QR) under
a named slot. Login accepts a code matching ANY active member, so offboarding
is a per-member revoke — no shared secret, no re-enrolling everyone when one
person leaves. The first visit bootstraps an "owner" slot; further members are
added from inside the authenticated admin panel. State persists under DATA_DIR
(gitignored).
"""

from __future__ import annotations

import base64
import io
import json
import secrets
from datetime import datetime, timezone
from typing import List, Optional, Tuple

import pyotp
import qrcode

from psyc import DATA_DIR, log


_log = log.get(__name__)

_STATE_PATH = DATA_DIR / "admin_auth.json"
_ISSUER = "psyc"


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _load() -> dict:
    if _STATE_PATH.exists():
        data = json.loads(_STATE_PATH.read_text())
        # Migrate the old single-secret format → member list.
        if "members" not in data:
            members = []
            pending = data.get("totp_secret")
            if data.get("provisioned") and data.get("totp_secret"):
                members = [_new_member("owner", data["totp_secret"])]
                pending = None
            data = {
                "session_secret": data.get("session_secret", secrets.token_urlsafe(32)),
                "pending_secret": pending,
                "members": members,
            }
            _save(data)
        return data
    data = {
        "session_secret": secrets.token_urlsafe(32),
        "pending_secret": pyotp.random_base32(),
        "members": [],
    }
    _save(data)
    _log.info("adminauth.initialized", path=str(_STATE_PATH))
    return data


def _save(state: dict) -> None:
    _STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
    _STATE_PATH.write_text(json.dumps(state, indent=2))


def _new_member(label: str, secret: str) -> dict:
    return {
        "id": secrets.token_hex(4),
        "label": label,
        "secret": secret,
        "created_at": _now(),
        "active": True,
        "last_used": None,
    }


def session_secret() -> str:
    return _load()["session_secret"]


def members() -> List[dict]:
    """Active members, without exposing their secrets."""
    return [
        {k: m[k] for k in ("id", "label", "created_at", "last_used")}
        for m in _load()["members"] if m.get("active")
    ]


def is_bootstrapped() -> bool:
    return any(m.get("active") for m in _load()["members"])


def verify(code: str) -> Optional[str]:
    """Check a code against every active member (and the bootstrap slot).

    Returns the matched member label, or None. Updates last_used; promotes the
    pending bootstrap secret into the first 'owner' member on first success.
    """
    code = code.strip()
    state = _load()
    for m in state["members"]:
        if m.get("active") and pyotp.TOTP(m["secret"]).verify(code, valid_window=1):
            m["last_used"] = _now()
            _save(state)
            _log.info("adminauth.verify.ok", member=m["label"])
            return m["label"]
    # Bootstrap: no active members yet → accept the pending secret as owner.
    if not any(m.get("active") for m in state["members"]) and state.get("pending_secret"):
        if pyotp.TOTP(state["pending_secret"]).verify(code, valid_window=1):
            owner = _new_member("owner", state["pending_secret"])
            owner["last_used"] = _now()
            state["members"].append(owner)
            state["pending_secret"] = None
            _save(state)
            _log.info("adminauth.bootstrapped")
            return "owner"
    _log.info("adminauth.verify.fail")
    return None


def add_member(label: str) -> Tuple[str, str]:
    """Enroll a new member. Returns (member_id, QR data-uri) to hand to them."""
    label = (label or "member").strip()[:40]
    secret = pyotp.random_base32()
    state = _load()
    m = _new_member(label, secret)
    state["members"].append(m)
    _save(state)
    _log.info("adminauth.member.added", member=label, id=m["id"])
    return m["id"], _qr_for(secret, label)


def revoke_member(member_id: str) -> bool:
    state = _load()
    for m in state["members"]:
        if m["id"] == member_id and m.get("active"):
            m["active"] = False
            m["revoked_at"] = _now()
            _save(state)
            _log.info("adminauth.member.revoked", id=member_id, label=m["label"])
            return True
    return False


def member_qr(member_id: str) -> Optional[str]:
    """One-time QR for a just-created member (admin-only surface)."""
    for m in _load()["members"]:
        if m["id"] == member_id and m.get("active"):
            return _qr_for(m["secret"], m["label"])
    return None


def bootstrap_qr() -> str:
    """QR for the initial owner enrollment (only meaningful before bootstrap)."""
    state = _load()
    if not state.get("pending_secret"):
        state["pending_secret"] = pyotp.random_base32()
        _save(state)
    return _qr_for(state["pending_secret"], "owner")


def _qr_for(secret: str, label: str) -> str:
    uri = pyotp.TOTP(secret).provisioning_uri(name=label, issuer_name=_ISSUER)
    img = qrcode.make(uri)
    buf = io.BytesIO()
    img.save(buf, format="PNG")
    return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()


def current_code(member_id: Optional[str] = None) -> str:
    """Live code for tests/local verification — never shown in the UI."""
    state = _load()
    if member_id:
        for m in state["members"]:
            if m["id"] == member_id:
                return pyotp.TOTP(m["secret"]).now()
    return pyotp.TOTP(state["pending_secret"]).now()
