"""
apikeys.py
Shared API-key store for the Status Checker proxy.

Keys live in one JSON file (default: /etc/status-checker/api_keys.json for the
cloud deployment; falls back to ./api_keys.json for local dev so `launch.py`
works out of the box without root). Override with STATUS_CHECKER_KEYSTORE.

We store only the SHA-256 hash of each key, never the raw key — a leaked
store file doesn't leak usable credentials. The raw key is shown once, at
generation time, and cannot be recovered afterward (only revoked + reissued).
"""

import json
import os
import secrets
import hashlib
import tempfile
from datetime import datetime, timezone
from threading import Lock

def _default_store_path():
    preferred = "/etc/status-checker/api_keys.json"
    if os.environ.get("STATUS_CHECKER_KEYSTORE"):
        return os.environ["STATUS_CHECKER_KEYSTORE"]
    # Local/dev fallback when /etc isn't writable (e.g. running launch.py as a normal user)
    parent = os.path.dirname(preferred)
    if os.access(parent, os.W_OK) if os.path.isdir(parent) else False:
        return preferred
    return os.path.join(os.path.dirname(os.path.abspath(__file__)), "api_keys.json")

DEFAULT_STORE_PATH = _default_store_path()

_lock = Lock()


def _now_iso():
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _hash_key(raw_key: str) -> str:
    return hashlib.sha256(raw_key.encode("utf-8")).hexdigest()


def _load(store_path):
    if not os.path.exists(store_path):
        return {"keys": {}}
    with open(store_path, "r") as f:
        return json.load(f)


def _save(data, store_path):
    dir_name = os.path.dirname(store_path) or "."
    os.makedirs(dir_name, exist_ok=True)
    fd, tmp_path = tempfile.mkstemp(dir=dir_name)
    try:
        with os.fdopen(fd, "w") as f:
            json.dump(data, f, indent=2)
        os.chmod(tmp_path, 0o600)
        os.replace(tmp_path, store_path)
    finally:
        if os.path.exists(tmp_path):
            try:
                os.remove(tmp_path)
            except OSError:
                pass


def generate_key(label: str, store_path=None) -> str:
    """Create a new key, persist its hash, return the RAW key (shown once)."""
    store_path = store_path or DEFAULT_STORE_PATH
    raw_key = "sc_" + secrets.token_urlsafe(32)
    key_hash = _hash_key(raw_key)
    with _lock:
        data = _load(store_path)
        data["keys"][key_hash] = {
            "label": label,
            "created": _now_iso(),
            "revoked": False,
            "revoked_at": None,
            "last_used": None,
            "use_count": 0,
        }
        _save(data, store_path)
    return raw_key


def revoke_key(label: str, store_path=None) -> list:
    """Revoke all active keys with this exact label. Returns matched labels."""
    store_path = store_path or DEFAULT_STORE_PATH
    matched = []
    with _lock:
        data = _load(store_path)
        for meta in data["keys"].values():
            if meta["revoked"]:
                continue
            if meta["label"] == label:
                meta["revoked"] = True
                meta["revoked_at"] = _now_iso()
                matched.append(meta["label"])
        _save(data, store_path)
    return matched


def list_keys(store_path=None) -> list:
    """Return metadata for all keys (never raw keys — we don't store them)."""
    store_path = store_path or DEFAULT_STORE_PATH
    data = _load(store_path)
    out = [{"key_id": h[:12], **meta} for h, meta in data["keys"].items()]
    out.sort(key=lambda m: m["created"])
    return out


def validate_key(raw_key: str, store_path=None) -> str | None:
    """Return the label if the key is valid and active, else None.
    Updates last_used / use_count on success (best-effort)."""
    store_path = store_path or DEFAULT_STORE_PATH
    if not raw_key:
        return None
    key_hash = _hash_key(raw_key)
    with _lock:
        data = _load(store_path)
        meta = data["keys"].get(key_hash)
        if not meta or meta["revoked"]:
            return None
        meta["last_used"] = _now_iso()
        meta["use_count"] = meta.get("use_count", 0) + 1
        try:
            _save(data, store_path)
        except OSError:
            pass
        return meta["label"]


def has_any_active_key(store_path=None) -> bool:
    store_path = store_path or DEFAULT_STORE_PATH
    return any(not m["revoked"] for m in list_keys(store_path))
