"""
Sécurité transverse : secrets persistants, mots de passe, URLs signées,
IP client derrière un reverse proxy, validation de la config de production.
"""
import base64
import hashlib
import hmac
import ipaddress
import logging
import secrets
import time
from typing import Optional

from core import config
from core.db import get_db

log = logging.getLogger("meeconic.security")

# "meeconic2026" used to be here (it was the old shipped default). The owner chose to keep it as the operator
# password in production (2026-10-12), so it is accepted: the 10-character minimum still applies.
_DEFAULT_PASSWORDS = {"direction2026", "admin", "password", "change-me", "changeme"}
_secret_cache: dict[str, str] = {}


# ── Secrets ────────────────────────────────────────────────────────────────────

def _persistent_secret(name: str) -> str:
    """Secret généré une fois puis conservé en base : identique entre workers et
    entre redémarrages (utilisé seulement si le .env n'en fournit pas)."""
    if name in _secret_cache:
        return _secret_cache[name]
    with get_db() as conn:
        conn.execute("INSERT OR IGNORE INTO app_kv (key, value) VALUES (?,?)",
                     (f"secret:{name}", secrets.token_hex(32)))
        conn.commit()
        value = conn.execute("SELECT value FROM app_kv WHERE key=?", (f"secret:{name}",)).fetchone()["value"]
    _secret_cache[name] = value
    return value


def jwt_secret() -> str:
    return config.ADMIN_JWT_SECRET or _persistent_secret("jwt")


def media_secret() -> str:
    if config.MEDIA_SECRET:
        return config.MEDIA_SECRET
    return hashlib.sha256(("media:" + jwt_secret()).encode()).hexdigest()


# ── Mots de passe ──────────────────────────────────────────────────────────────

def hash_password(password: str) -> str:
    salt = secrets.token_bytes(16)
    dk = hashlib.scrypt(password.encode(), salt=salt, n=2 ** 14, r=8, p=1, dklen=32)
    return f"scrypt${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}"


def verify_password(password: str, stored: Optional[str]) -> bool:
    if not stored:
        return False
    try:
        scheme, salt_b64, dk_b64 = stored.split("$")
        if scheme != "scrypt":
            return False
        salt, expected = base64.b64decode(salt_b64), base64.b64decode(dk_b64)
        dk = hashlib.scrypt(password.encode(), salt=salt, n=2 ** 14, r=8, p=1, dklen=len(expected))
        return hmac.compare_digest(dk, expected)
    except Exception:
        return False


def constant_time_eq(a: str, b: str) -> bool:
    return hmac.compare_digest(a.encode(), b.encode())


# ── URLs signées (aperçus filigranés, téléchargements) ──────────────────────────

def sign(payload: str, ttl_secs: int) -> tuple[int, str]:
    exp = int(time.time()) + ttl_secs
    sig = hmac.new(media_secret().encode(), f"{payload}|{exp}".encode(), hashlib.sha256).hexdigest()[:32]
    return exp, sig


def verify_signature(payload: str, exp: int, sig: str) -> bool:
    if exp < time.time():
        return False
    expected = hmac.new(media_secret().encode(), f"{payload}|{exp}".encode(), hashlib.sha256).hexdigest()[:32]
    return hmac.compare_digest(expected, sig or "")


# ── IP client (reverse proxy) ───────────────────────────────────────────────────

def _parse_networks(items: list[str]):
    nets = []
    for it in items:
        try:
            nets.append(ipaddress.ip_network(it, strict=False))
        except ValueError:
            log.warning("TRUSTED_PROXIES: entry ignored %r", it)
    return nets


_trusted_nets = _parse_networks(config.TRUSTED_PROXIES)


def _is_trusted(ip: str) -> bool:
    try:
        addr = ipaddress.ip_address(ip)
    except ValueError:
        return False
    return any(addr in n for n in _trusted_nets)


def client_ip(request) -> str:
    """IP réelle du client. X-Forwarded-For n'est lu que si la connexion vient d'un
    proxy de confiance ; on remonte la chaîne depuis la droite jusqu'à la première
    adresse non fiable (un client ne peut donc pas forger sa propre IP)."""
    peer = request.client.host if request.client else "unknown"
    if not _is_trusted(peer):
        return peer
    xff = request.headers.get("x-forwarded-for", "")
    if not xff:
        return peer
    for candidate in reversed([p.strip() for p in xff.split(",") if p.strip()]):
        if not _is_trusted(candidate):
            return candidate
    return peer


# ── Validation de la config de production ───────────────────────────────────────

def validate_production_config() -> list[str]:
    """Retourne la liste des problèmes bloquants (vide si OK)."""
    problems: list[str] = []
    pw = config.ADMIN_PASSWORD
    if not pw or pw.lower() in _DEFAULT_PASSWORDS or len(pw) < 10:
        problems.append("ADMIN_PASSWORD absent, par défaut ou trop court (min. 10 caractères).")
    if not config.ADMIN_JWT_SECRET or len(config.ADMIN_JWT_SECRET) < 32 or config.ADMIN_JWT_SECRET.startswith("change-me"):
        problems.append("ADMIN_JWT_SECRET absent ou trop court (min. 32 caractères).")
    if not config.STRIPE_SECRET_KEY:
        problems.append("STRIPE_SECRET_KEY absent : les commandes ne pourraient pas être payées.")
    if not config.STRIPE_WEBHOOK_SECRET:
        problems.append("STRIPE_WEBHOOK_SECRET absent : les paiements ne seraient jamais confirmés de façon sûre.")
    return problems
