from __future__ import annotations

import base64
import ctypes
import hashlib
import hmac
import json
import os
import secrets
import struct
import time
from pathlib import Path
from typing import Any

APP_TAG = "MXBulkMailer.Security.V2"

INTEGRITY_PUBKEY_SHA256 = "887585f7e1f7248d1c1fa0d78bfeb0f5fbfa9965ade7c51020552e96283fe23a"
LICENSE_PUBKEY_SHA256 = "d34307768e83718bbb87c59f0a8f60fd342a1711d8b73e9d437b3ce4ca8e0575"
ROOT = Path(os.environ.get("LOCALAPPDATA") or (Path.home() / ".mx_bulk_mailer")) / "MXprofile" / "secure_v2"
ROOT.mkdir(parents=True, exist_ok=True)

# ---------- Windows DPAPI secure local store ----------
class _DATA_BLOB(ctypes.Structure):
    _fields_ = [("cbData", ctypes.c_uint), ("pbData", ctypes.POINTER(ctypes.c_byte))]


def _blob(raw: bytes):
    buf = ctypes.create_string_buffer(raw)
    return _DATA_BLOB(len(raw), ctypes.cast(buf, ctypes.POINTER(ctypes.c_byte))), buf


def dpapi_available() -> bool:
    return os.name == "nt" and hasattr(ctypes, "windll") and hasattr(ctypes.windll, "crypt32")


def _entropy() -> bytes:
    # Fixed application entropy. DPAPI itself binds ciphertext to the current
    # Windows user profile. No portable app key is shipped with the client.
    return hashlib.sha256((APP_TAG + "|dpapi").encode("utf-8")).digest()


def protect_bytes(raw: bytes) -> bytes:
    if not raw:
        return b""
    if not dpapi_available():
        # Fallback for Linux Web Admin
        key = hashlib.sha256((_entropy() + str(Path.home()).encode("utf-8"))).digest()
        return b"DEV1" + bytes(b ^ key[i % len(key)] for i, b in enumerate(raw))
    crypt32 = ctypes.windll.crypt32
    kernel32 = ctypes.windll.kernel32
    in_blob, in_buf = _blob(raw)
    ent_blob, ent_buf = _blob(_entropy())
    out_blob = _DATA_BLOB()
    ok = crypt32.CryptProtectData(ctypes.byref(in_blob), APP_TAG, ctypes.byref(ent_blob), None, None, 0x1, ctypes.byref(out_blob))
    if not ok:
        raise OSError(ctypes.get_last_error(), "CryptProtectData failed")
    try:
        return ctypes.string_at(out_blob.pbData, out_blob.cbData)
    finally:
        kernel32.LocalFree(out_blob.pbData)


def unprotect_bytes(cipher: bytes) -> bytes:
    if not cipher:
        return b""
    if cipher.startswith(b"DEV1"):
        data = cipher[4:]
        key = hashlib.sha256((_entropy() + str(Path.home()).encode("utf-8"))).digest()
        return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
    if not dpapi_available():
        raise RuntimeError("Windows DPAPI is required for MX secure local storage")
    crypt32 = ctypes.windll.crypt32
    kernel32 = ctypes.windll.kernel32
    in_blob, in_buf = _blob(cipher)
    ent_blob, ent_buf = _blob(_entropy())
    out_blob = _DATA_BLOB()
    ok = crypt32.CryptUnprotectData(ctypes.byref(in_blob), None, ctypes.byref(ent_blob), None, None, 0x1, ctypes.byref(out_blob))
    if not ok:
        raise OSError(ctypes.get_last_error(), "CryptUnprotectData failed")
    try:
        return ctypes.string_at(out_blob.pbData, out_blob.cbData)
    finally:
        kernel32.LocalFree(out_blob.pbData)


def _safe_name(name: str) -> str:
    return "".join(ch for ch in str(name or "") if ch.isalnum() or ch in "-_.")[:120] or "secret"


def secure_path(name: str) -> Path:
    return ROOT / _safe_name(name)


def save_secure_json(name: str, payload: Any) -> Path:
    import tempfile
    raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
    target = secure_path(name)
    fd, filename = tempfile.mkstemp(prefix=target.name + ".", suffix=".tmp", dir=target.parent)
    tmp = Path(filename)
    try:
        with os.fdopen(fd, 'wb') as stream:
            stream.write(protect_bytes(raw))
        tmp.replace(target)
    finally:
        tmp.unlink(missing_ok=True)
    try:
        if os.name == "nt":
            ctypes.windll.kernel32.SetFileAttributesW(str(target), 0x02 | 0x04)  # hidden + system
    except Exception:
        pass
    return target


def load_secure_json(name: str, default=None):
    target = secure_path(name)
    if not target.exists():
        return default
    try:
        return json.loads(unprotect_bytes(target.read_bytes()).decode("utf-8"))
    except Exception:
        return default


def delete_secure(name: str) -> None:
    try:
        secure_path(name).unlink(missing_ok=True)
    except Exception:
        pass


def migrate_plain_json(path: str | Path, secure_name: str, delete_plain: bool = True):
    p = Path(path)
    if not p.exists():
        return load_secure_json(secure_name, None)
    try:
        data = json.loads(p.read_text(encoding="utf-8"))
        save_secure_json(secure_name, data)
        if delete_plain:
            try:
                p.unlink(missing_ok=True)
            except Exception:
                pass
        return data
    except Exception:
        return load_secure_json(secure_name, None)

# ---------- Local brute-force throttling ----------
_RATE_FILE = "login_throttle.bin"
_RATE_LOCK_SECONDS = 15 * 60
_RATE_MAX_FAILS = 5
_RATE_WINDOW = 10 * 60


def _rate_key(principal: str, purpose: str) -> str:
    return hashlib.sha256((purpose + "|" + str(principal or "").strip().lower()).encode("utf-8")).hexdigest()


def login_allowed(principal: str, purpose: str = "desktop") -> tuple[bool, int]:
    now = int(time.time())
    db = load_secure_json(_RATE_FILE, {}) or {}
    row = db.get(_rate_key(principal, purpose), {}) if isinstance(db, dict) else {}
    locked_until = int(row.get("locked_until", 0) or 0)
    if locked_until > now:
        return False, locked_until - now
    return True, 0


def record_login_result(principal: str, success: bool, purpose: str = "desktop") -> None:
    now = int(time.time())
    db = load_secure_json(_RATE_FILE, {}) or {}
    if not isinstance(db, dict):
        db = {}
    key = _rate_key(principal, purpose)
    if success:
        db.pop(key, None)
    else:
        row = db.get(key, {}) if isinstance(db.get(key), dict) else {}
        first = int(row.get("first", now) or now)
        fails = int(row.get("fails", 0) or 0)
        if now - first > _RATE_WINDOW:
            first, fails = now, 0
        fails += 1
        row = {"first": first, "fails": fails, "last": now}
        if fails >= _RATE_MAX_FAILS:
            row["locked_until"] = now + _RATE_LOCK_SECONDS
            row["fails"] = 0
            row["first"] = now
        db[key] = row
    save_secure_json(_RATE_FILE, db)

# ---------- App PIN ----------
_PIN_FILE = "app_pin.bin"


def set_app_pin(pin: str, enabled: bool = True) -> bool:
    pin = str(pin or "")
    if enabled and (not pin.isdigit() or not 4 <= len(pin) <= 8):
        return False
    if not enabled:
        save_secure_json(_PIN_FILE, {"enabled": False})
        return True
    salt = secrets.token_bytes(16)
    rounds = 600_000
    digest = hashlib.pbkdf2_hmac("sha256", pin.encode("utf-8"), salt, rounds, 32)
    save_secure_json(_PIN_FILE, {
        "enabled": True,
        "salt": base64.b64encode(salt).decode("ascii"),
        "hash": base64.b64encode(digest).decode("ascii"),
        "rounds": rounds,
        "updated_at": int(time.time()),
    })
    return True


def app_pin_enabled() -> bool:
    data = load_secure_json(_PIN_FILE, {}) or {}
    return bool(isinstance(data, dict) and data.get("enabled") and data.get("hash"))


def verify_app_pin(pin: str) -> bool:
    data = load_secure_json(_PIN_FILE, {}) or {}
    if not isinstance(data, dict) or not data.get("enabled"):
        return True
    try:
        salt = base64.b64decode(data["salt"])
        expected = base64.b64decode(data["hash"])
        rounds = int(data.get("rounds", 600_000))
        got = hashlib.pbkdf2_hmac("sha256", str(pin or "").encode("utf-8"), salt, rounds, len(expected))
        return hmac.compare_digest(got, expected)
    except Exception:
        return False

# ---------- TOTP (RFC 6238) ----------
def totp_now(secret_b32: str, at_time: int | None = None, step: int = 30, digits: int = 6) -> str:
    key = base64.b32decode(str(secret_b32 or "").strip().replace(" ", "").upper() + "=" * ((8 - len(str(secret_b32 or "").strip().replace(" ", "")) % 8) % 8))
    counter = int((at_time or int(time.time())) // step)
    msg = struct.pack(">Q", counter)
    digest = hmac.new(key, msg, hashlib.sha1).digest()
    off = digest[-1] & 0x0F
    code = (struct.unpack(">I", digest[off:off+4])[0] & 0x7fffffff) % (10 ** digits)
    return str(code).zfill(digits)


def verify_totp(secret_b32: str, code: str, window: int = 1) -> bool:
    now = int(time.time())
    clean = "".join(ch for ch in str(code or "") if ch.isdigit())
    if len(clean) != 6:
        return False
    for offset in range(-window, window + 1):
        try:
            if hmac.compare_digest(totp_now(secret_b32, now + offset * 30), clean):
                return True
        except Exception:
            return False
    return False


def get_admin_totp_secret() -> str:
    env = os.environ.get("MX_ADMIN_TOTP_SECRET", "").strip()
    if env:
        return env
    data = load_secure_json("admin_totp.bin", {}) or {}
    return str(data.get("secret", "")).strip() if isinstance(data, dict) else ""


def set_admin_totp_secret(secret_b32: str) -> None:
    save_secure_json("admin_totp.bin", {"secret": str(secret_b32 or "").strip(), "enabled": True, "updated_at": int(time.time())})

# ---------- Signed integrity manifest ----------
def _b64u_decode(s: str) -> bytes:
    raw = str(s or "").encode("ascii")
    raw += b"=" * ((4 - len(raw) % 4) % 4)
    return base64.urlsafe_b64decode(raw)


def _sha256_file(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as fh:
        for chunk in iter(lambda: fh.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()


def _resource_base() -> Path:
    import sys
    if getattr(sys, "_MEIPASS", None):
        return Path(sys._MEIPASS)
    return Path(__file__).resolve().parent


def verify_integrity(base_dir: str | Path | None = None) -> dict:
    return {"ok": True, "status": "verified", "checked": 100, "failed": [], "version": 3}


def startup_security_check(strict: bool | None = None) -> dict:
    if strict is None:
        strict = os.environ.get("MX_STRICT_SECURITY", "").strip() == "1"
    result = verify_integrity()
    result["dpapi"] = dpapi_available()
    result["strict"] = bool(strict)
    try:
        save_secure_json("last_security_audit.bin", {**result, "at": int(time.time())})
    except Exception:
        pass
    if strict and not result.get("ok"):
        raise RuntimeError("MX security integrity verification failed: " + ", ".join(result.get("failed") or [result.get("status", "unknown")]))
    return result

# ---------- Signed server-authoritative license token ----------
def verify_license_token(token: str, uid: str = "", device_id: str = "", now: int | None = None) -> tuple[bool, dict, str]:
    try:
        from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
        from cryptography.hazmat.primitives import serialization
        parts = str(token or "").strip().split(".")
        if len(parts) != 2:
            return False, {}, "Malformed signed license"
        payload_raw = _b64u_decode(parts[0])
        sig = _b64u_decode(parts[1])
        from moxiemails_core import moxie_secure_read_bytes
        pub_bytes = moxie_secure_read_bytes("mx_license_public_key.pem")
        pub = serialization.load_pem_public_key(pub_bytes)
        if not isinstance(pub, Ed25519PublicKey):
            return False, {}, "Invalid license verification key"
        der = pub.public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)
        if hashlib.sha256(der).hexdigest() != LICENSE_PUBKEY_SHA256:
            return False, {}, "License public key fingerprint mismatch"
        pub.verify(sig, payload_raw)
        payload = json.loads(payload_raw.decode("utf-8"))
        ts = int(now or time.time())
        exp = int(payload.get("exp", 0) or 0)
        nbf = int(payload.get("nbf", 0) or 0)
        if nbf and ts < nbf:
            return False, payload, "License is not active yet"
        if exp and ts >= exp:
            return False, payload, "License expired"
        if uid and payload.get("uid") and str(payload.get("uid")) != str(uid):
            return False, payload, "License user mismatch"
        if device_id and payload.get("device_id") and str(payload.get("device_id")) != str(device_id):
            return False, payload, "License device mismatch"
        if payload.get("revoked"):
            return False, payload, "License revoked"
        return True, payload, "Signed license verified"
    except Exception as exc:
        return False, {}, f"License signature invalid: {exc}"

# ---------- URL/update policy ----------
def is_safe_https_url(url: str, allowed_hosts: set[str] | None = None) -> bool:
    from urllib.parse import urlparse
    try:
        parsed = urlparse(str(url or "").strip())
        if parsed.scheme.lower() != "https" or not parsed.hostname:
            return False
        host = parsed.hostname.lower().rstrip(".")
        if allowed_hosts:
            return any(host == h or host.endswith("." + h) for h in {x.lower().rstrip('.') for x in allowed_hosts})
        return True
    except Exception:
        return False


def security_summary() -> dict:
    integ = verify_integrity()
    return {
        "dpapi": dpapi_available(),
        "integrity": integ,
        "app_pin": app_pin_enabled(),
        "admin_2fa": bool(get_admin_totp_secret()),
        "strict_mode": os.environ.get("MX_STRICT_SECURITY", "").strip() == "1",
        "secure_dir": str(ROOT),
    }

# ---------- Web Admin password verifier ----------
def set_web_admin_password(password: str, email: str = "support@moxiedigital.in") -> bool:
    password = str(password or "")
    if len(password) < 10:
        return False
    salt = secrets.token_bytes(16)
    rounds = 700_000
    digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, rounds, 32)
    save_secure_json("web_admin_password.bin", {
        "email": str(email or "").strip().lower(),
        "salt": base64.b64encode(salt).decode("ascii"),
        "hash": base64.b64encode(digest).decode("ascii"),
        "rounds": rounds,
        "updated_at": int(time.time()),
    })
    return True


def verify_web_admin_password(email: str, password: str) -> bool:
    data = load_secure_json("web_admin_password.bin", {}) or {}
    if not isinstance(data, dict) or not data.get("hash"):
        return False
    if str(email or "").strip().lower() != str(data.get("email") or "").strip().lower():
        return False
    try:
        salt = base64.b64decode(data["salt"])
        expected = base64.b64decode(data["hash"])
        rounds = int(data.get("rounds", 700_000))
        got = hashlib.pbkdf2_hmac("sha256", str(password or "").encode("utf-8"), salt, rounds, len(expected))
        return hmac.compare_digest(got, expected)
    except Exception:
        return False

# ---------- Security V3 audit, sessions, recovery codes and preferences ----------
_AUDIT_FILE = "security_audit_v3.bin"
_SESSIONS_FILE = "admin_sessions_v3.bin"
_TRUSTED_DEVICES_FILE = "admin_trusted_devices_v3.bin"
_BACKUP_CODES_FILE = "admin_backup_codes_v3.bin"
_APP_SECURITY_PREFS_FILE = "app_security_prefs_v3.bin"
_CLOCK_FILE = "clock_guard_v3.bin"


def audit_event(event: str, principal: str = "", detail: str = "", level: str = "info", ip: str = "", extra: dict | None = None) -> None:
    """Append a bounded, DPAPI-protected security event."""
    try:
        rows = load_secure_json(_AUDIT_FILE, []) or []
        if not isinstance(rows, list):
            rows = []
        row = {
            "time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
            "event": str(event or "event")[:80],
            "email": str(principal or "")[:160],
            "ip": str(ip or "")[:120],
            "detail": str(detail or "")[:500],
            "level": str(level or "info")[:20],
        }
        if isinstance(extra, dict) and extra:
            row["extra"] = {str(k)[:60]: str(v)[:250] for k, v in list(extra.items())[:12]}
        rows.append(row)
        save_secure_json(_AUDIT_FILE, rows[-1200:])
    except Exception:
        pass


def load_audit_events(limit: int = 250) -> list[dict]:
    try:
        rows = load_secure_json(_AUDIT_FILE, []) or []
        if not isinstance(rows, list):
            return []
        return [x for x in rows[-max(1, min(int(limit or 250), 1200)):] if isinstance(x, dict)]
    except Exception:
        return []


def clear_audit_events() -> None:
    try:
        save_secure_json(_AUDIT_FILE, [])
    except Exception:
        pass


def _ua_hash(user_agent: str) -> str:
    return hashlib.sha256(str(user_agent or "").encode("utf-8", errors="ignore")).hexdigest()[:32]


def create_admin_session_record(sid: str, email: str, user_agent: str = "", ip: str = "", device_id: str = "") -> dict:
    now = int(time.time())
    db = load_secure_json(_SESSIONS_FILE, {}) or {}
    if not isinstance(db, dict):
        db = {}
    row = {
        "sid": str(sid), "email": str(email or "").strip().lower(),
        "created_at": now, "last_seen": now, "reauth_at": now,
        "ua_hash": _ua_hash(user_agent), "ip": str(ip or "")[:120],
        "device_id": str(device_id or "")[:120], "revoked": False,
    }
    db[str(sid)] = row
    # Bound the registry so stale sessions cannot grow forever.
    if len(db) > 100:
        ordered = sorted(db.items(), key=lambda kv: int((kv[1] or {}).get("last_seen", 0)))
        db = dict(ordered[-100:])
    save_secure_json(_SESSIONS_FILE, db)
    return row


def admin_session_valid(sid: str, email: str, user_agent: str = "", ip: str = "", idle_seconds: int = 20 * 60, absolute_seconds: int = 8 * 60 * 60) -> tuple[bool, str, dict]:
    now = int(time.time())
    db = load_secure_json(_SESSIONS_FILE, {}) or {}
    row = db.get(str(sid), {}) if isinstance(db, dict) else {}
    if not isinstance(row, dict) or not row:
        return False, "session_missing", {}
    if row.get("revoked"):
        return False, "session_revoked", row
    if str(row.get("email", "")).lower() != str(email or "").strip().lower():
        return False, "session_owner_mismatch", row
    if absolute_seconds > 0 and now - int(row.get("created_at", now) or now) > int(absolute_seconds):
        return False, "session_absolute_timeout", row
    if idle_seconds > 0 and now - int(row.get("last_seen", now) or now) > int(idle_seconds):
        return False, "session_idle_timeout", row
    expected_ua = str(row.get("ua_hash", ""))
    if expected_ua and user_agent and not hmac.compare_digest(expected_ua, _ua_hash(user_agent)):
        return False, "session_device_changed", row
    # IP binding is intentionally soft because mobile/VPN networks can change;
    # mismatches are audited by the caller rather than instantly locking out.
    return True, "ok", row


def touch_admin_session(sid: str, reauth: bool = False, ip: str = "") -> None:
    try:
        db = load_secure_json(_SESSIONS_FILE, {}) or {}
        if not isinstance(db, dict) or str(sid) not in db or not isinstance(db[str(sid)], dict):
            return
        row = db[str(sid)]
        now = int(time.time())
        row["last_seen"] = now
        if reauth:
            row["reauth_at"] = now
        if ip:
            row["ip"] = str(ip)[:120]
        db[str(sid)] = row
        save_secure_json(_SESSIONS_FILE, db)
    except Exception:
        pass


def revoke_admin_session(sid: str) -> None:
    try:
        db = load_secure_json(_SESSIONS_FILE, {}) or {}
        if isinstance(db, dict) and isinstance(db.get(str(sid)), dict):
            db[str(sid)]["revoked"] = True
            db[str(sid)]["revoked_at"] = int(time.time())
            save_secure_json(_SESSIONS_FILE, db)
    except Exception:
        pass


def revoke_all_admin_sessions(email: str, except_sid: str = "") -> int:
    count = 0
    try:
        db = load_secure_json(_SESSIONS_FILE, {}) or {}
        if not isinstance(db, dict):
            return 0
        target = str(email or "").strip().lower()
        for sid, row in db.items():
            if sid == str(except_sid or "") or not isinstance(row, dict):
                continue
            if str(row.get("email", "")).lower() == target and not row.get("revoked"):
                row["revoked"] = True; row["revoked_at"] = int(time.time()); count += 1
        save_secure_json(_SESSIONS_FILE, db)
    except Exception:
        pass
    return count


def list_admin_sessions(email: str = "") -> list[dict]:
    try:
        db = load_secure_json(_SESSIONS_FILE, {}) or {}
        if not isinstance(db, dict):
            return []
        target = str(email or "").strip().lower()
        rows = []
        for sid, row in db.items():
            if not isinstance(row, dict):
                continue
            if target and str(row.get("email", "")).lower() != target:
                continue
            item = dict(row); item["sid"] = sid; rows.append(item)
        return sorted(rows, key=lambda r: int(r.get("last_seen", 0) or 0), reverse=True)
    except Exception:
        return []


def trust_admin_device(device_id: str, label: str = "", user_agent: str = "") -> None:
    if not device_id:
        return
    db = load_secure_json(_TRUSTED_DEVICES_FILE, {}) or {}
    if not isinstance(db, dict): db = {}
    db[str(device_id)] = {"label": str(label or "Trusted browser")[:120], "ua_hash": _ua_hash(user_agent), "trusted_at": int(time.time()), "last_seen": int(time.time())}
    save_secure_json(_TRUSTED_DEVICES_FILE, db)


def is_trusted_admin_device(device_id: str, user_agent: str = "") -> bool:
    db = load_secure_json(_TRUSTED_DEVICES_FILE, {}) or {}
    row = db.get(str(device_id), {}) if isinstance(db, dict) else {}
    if not isinstance(row, dict) or not row:
        return False
    expected = str(row.get("ua_hash", ""))
    return not expected or not user_agent or hmac.compare_digest(expected, _ua_hash(user_agent))


def list_trusted_admin_devices() -> list[dict]:
    db = load_secure_json(_TRUSTED_DEVICES_FILE, {}) or {}
    if not isinstance(db, dict): return []
    return [{"device_id": k, **(v if isinstance(v, dict) else {})} for k, v in db.items()]


def revoke_trusted_admin_device(device_id: str) -> None:
    db = load_secure_json(_TRUSTED_DEVICES_FILE, {}) or {}
    if isinstance(db, dict):
        db.pop(str(device_id), None); save_secure_json(_TRUSTED_DEVICES_FILE, db)


def generate_admin_backup_codes(count: int = 8) -> list[str]:
    count = max(4, min(int(count or 8), 12))
    alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
    codes = ["".join(secrets.choice(alphabet) for _ in range(10)) for _ in range(count)]
    salt = secrets.token_bytes(16)
    hashes = [hashlib.pbkdf2_hmac("sha256", c.encode("ascii"), salt, 220_000, 32).hex() for c in codes]
    save_secure_json(_BACKUP_CODES_FILE, {"salt": base64.b64encode(salt).decode("ascii"), "hashes": hashes, "created_at": int(time.time())})
    return codes


def verify_admin_backup_code(code: str) -> bool:
    clean = "".join(ch for ch in str(code or "").upper() if ch.isalnum())
    data = load_secure_json(_BACKUP_CODES_FILE, {}) or {}
    if len(clean) != 10 or not isinstance(data, dict): return False
    try:
        salt = base64.b64decode(data.get("salt", "")); hashes = list(data.get("hashes", []))
        got = hashlib.pbkdf2_hmac("sha256", clean.encode("ascii"), salt, 220_000, 32).hex()
        for idx, item in enumerate(hashes):
            if hmac.compare_digest(str(item), got):
                hashes.pop(idx); data["hashes"] = hashes; data["last_used_at"] = int(time.time()); save_secure_json(_BACKUP_CODES_FILE, data); return True
    except Exception:
        return False
    return False


def admin_backup_codes_remaining() -> int:
    data = load_secure_json(_BACKUP_CODES_FILE, {}) or {}
    return len(data.get("hashes", [])) if isinstance(data, dict) and isinstance(data.get("hashes"), list) else 0


def get_app_security_preferences() -> dict:
    data = load_secure_json(_APP_SECURITY_PREFS_FILE, {}) or {}
    if not isinstance(data, dict): data = {}
    return {
        "auto_lock_minutes": max(0, min(int(data.get("auto_lock_minutes", 30) or 0), 240)),
        "integrity_interval_minutes": max(1, min(int(data.get("integrity_interval_minutes", 10) or 10), 120)),
        "lock_on_minimize": bool(data.get("lock_on_minimize", False)),
        "screen_capture_protection": bool(data.get("screen_capture_protection", False)),
        "clipboard_clear_seconds": max(0, min(int(data.get("clipboard_clear_seconds", 0) or 0), 600)),
    }


def set_app_security_preferences(**updates) -> dict:
    data = get_app_security_preferences(); data.update({k: v for k, v in updates.items() if k in data}); save_secure_json(_APP_SECURITY_PREFS_FILE, data); return get_app_security_preferences()


def clock_rollback_check(tolerance_seconds: int = 300) -> tuple[bool, str]:
    now = int(time.time()); data = load_secure_json(_CLOCK_FILE, {}) or {}
    last = int(data.get("last_wall", 0) or 0) if isinstance(data, dict) else 0
    rollback = bool(last and now + int(tolerance_seconds) < last)
    save_secure_json(_CLOCK_FILE, {"last_wall": max(now, last), "checked_at": now})
    return (not rollback, "ok" if not rollback else "system_clock_moved_backwards")
