import base64
import hashlib
import hmac
import secrets
import struct
import time
from datetime import datetime, timedelta, timezone
from urllib.parse import quote

from argon2 import PasswordHasher
from argon2.exceptions import VerificationError
from sqlalchemy import bindparam, text

from app.core.config import get_settings
from app.core.db import db_session
from app.core.security import decrypt_text, encrypt_text, keyed_hash, normalize_email


password_hasher = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=2,
                                 hash_len=32, salt_len=16)


def hash_password(password: str) -> str:
    if len(password) < 12:
        raise ValueError("La contraseña debe tener al menos 12 caracteres")
    return password_hasher.hash(password)


def verify_password(stored: str, password: str) -> bool:
    try:
        return password_hasher.verify(stored, password)
    except VerificationError:
        return False


def _digest(value: str) -> bytes:
    return hashlib.sha256(value.encode()).digest()


def _client_hash(value: str | None) -> bytes | None:
    return keyed_hash(value, get_settings().access_token_hmac_key) if value else None


def audit(db, user_id: int | None, action: str, entity: str, entity_id: int | None,
          reason: str | None = None):
    db.execute(text("""
      INSERT INTO auditoria
        (correlation_id,actor_tipo,actor_referencia,accion,entidad,entidad_id,motivo)
      VALUES (UUID(),'USUARIO',:actor,:action,:entity,:entity_id,:reason)
    """), {"actor": str(user_id) if user_id else None, "action": action,
            "entity": entity, "entity_id": entity_id, "reason": reason})


def create_session(db, user_id: int, ip: str | None, user_agent: str | None):
    settings = get_settings()
    token = secrets.token_urlsafe(48)
    csrf = secrets.token_urlsafe(32)
    db.execute(text("""
      INSERT INTO sesiones_usuario
        (usuario_id,token_hash,csrf_hash,ip_hash,agente_hash,expira_en,expira_absoluta_en)
      VALUES (:user,:token,:csrf,:ip,:agent,
              DATE_ADD(UTC_TIMESTAMP(6),INTERVAL :idle MINUTE),
              DATE_ADD(UTC_TIMESTAMP(6),INTERVAL :absolute HOUR))
    """), {"user": user_id, "token": _digest(token), "csrf": _digest(csrf),
            "ip": _client_hash(ip), "agent": _client_hash(user_agent),
            "idle": settings.session_idle_minutes,
            "absolute": settings.session_absolute_hours})
    audit(db, user_id, "LOGIN_EXITOSO", "USUARIO", user_id)
    return token, csrf


def authenticate(email: str, password: str, ip: str | None, user_agent: str | None):
    normalized = normalize_email(email)
    with db_session() as db:
        user = db.execute(text("SELECT * FROM usuarios WHERE email=:email FOR UPDATE"),
                          {"email": normalized}).mappings().first()
        success = bool(user and user["activo"] and
                       (not user["bloqueado_hasta"] or user["bloqueado_hasta"].replace(tzinfo=timezone.utc) <= datetime.now(timezone.utc)) and
                       verify_password(user["password_hash"], password))
        db.execute(text("""
          INSERT INTO intentos_login (email_hash,ip_hash,exitoso,motivo)
          VALUES (:email,:ip,:ok,:reason)
        """), {"email": _client_hash(normalized), "ip": _client_hash(ip),
                "ok": success, "reason": None if success else "CREDENCIALES_INVALIDAS"})
        if not success:
            if user:
                db.execute(text("""
                  UPDATE usuarios SET intentos_fallidos=intentos_fallidos+1,
                    bloqueado_hasta=IF(intentos_fallidos+1>=5,DATE_ADD(UTC_TIMESTAMP(6),INTERVAL 15 MINUTE),bloqueado_hasta)
                  WHERE id=:id
                """), {"id": user["id"]})
            raise ValueError("Credenciales inválidas o acceso temporalmente bloqueado")
        db.execute(text("UPDATE usuarios SET intentos_fallidos=0,bloqueado_hasta=NULL WHERE id=:id"), {"id": user["id"]})
        db.execute(text("""
          UPDATE ventas v JOIN clientes c ON c.id=v.cliente_id
             SET v.usuario_id=:id
           WHERE v.usuario_id IS NULL AND c.email_hmac=:email_hash
        """),{"id":user["id"],"email_hash":keyed_hash(normalized,get_settings().pii_hmac_key)})
        if user["mfa_habilitado"]:
            challenge = secrets.token_urlsafe(40)
            db.execute(text("""
              INSERT INTO desafios_mfa (usuario_id,token_hash,expira_en)
              VALUES (:user,:token,DATE_ADD(UTC_TIMESTAMP(6),INTERVAL 10 MINUTE))
            """), {"user": user["id"], "token": _digest(challenge)})
            return {"mfa_required": True, "challenge_token": challenge}
        token, csrf = create_session(db, user["id"], ip, user_agent)
        return {"mfa_required": False, "session_token": token, "csrf_token": csrf}


def current_user(session_token: str | None, refresh: bool = True):
    if not session_token:
        return None
    with db_session() as db:
        row = db.execute(text("""
          SELECT s.id session_id,s.usuario_id,s.csrf_hash,s.expira_absoluta_en,
                 u.cliente_id,u.email,u.nombres,u.apellidos,u.telefono,u.mfa_habilitado,u.mfa_requerido
          FROM sesiones_usuario s JOIN usuarios u ON u.id=s.usuario_id
          WHERE s.token_hash=:token AND s.revocada_en IS NULL AND u.activo=TRUE
            AND s.expira_en>UTC_TIMESTAMP(6) AND s.expira_absoluta_en>UTC_TIMESTAMP(6)
        """), {"token": _digest(session_token)}).mappings().first()
        if not row:
            return None
        roles = db.execute(text("""
          SELECT r.codigo FROM roles r JOIN usuarios_roles ur ON ur.rol_id=r.id
          WHERE ur.usuario_id=:user AND r.activo=TRUE ORDER BY r.codigo
        """), {"user": row["usuario_id"]}).scalars().all()
        permissions = db.execute(text("""
          SELECT DISTINCT p.codigo FROM permisos p
          JOIN roles_permisos rp ON rp.permiso_id=p.id
          JOIN usuarios_roles ur ON ur.rol_id=rp.rol_id WHERE ur.usuario_id=:user
        """), {"user": row["usuario_id"]}).scalars().all()
        notaries = db.execute(text("""
          SELECT n.id,n.nombre_fantasia,
                 COALESCE(MAX(em.rol_codigo),IF(:legacy_admin=1,'ADMIN','OPERADOR')) rol_membresia,
                 (SELECT GROUP_CONCAT(te.codigo ORDER BY te.orden SEPARATOR ',')
                    FROM entidad_capacidades ec
                    JOIN tipos_entidad te ON te.id=ec.tipo_entidad_id
                   WHERE ec.entidad_id=n.id AND ec.activo=TRUE AND te.activo=TRUE) capacidades_csv
          FROM notarias n
          JOIN estados_notaria en ON en.id=n.estado_notaria_id
          LEFT JOIN entidad_membresias em ON em.entidad_id=n.id
            AND em.usuario_id=:user AND em.activo=TRUE
          LEFT JOIN notaria_usuarios nu ON nu.notaria_id=n.id
            AND nu.usuario_id=:user AND nu.activo=TRUE
          WHERE (em.id IS NOT NULL OR nu.usuario_id IS NOT NULL)
            AND en.codigo='ACTIVA' AND n.eliminado_en IS NULL
          GROUP BY n.id
          ORDER BY n.nombre_fantasia
        """), {"user": row["usuario_id"],
                 "legacy_admin": 1 if "NOTARIA_ADMIN" in roles else 0}).mappings().all()
        active_context = db.execute(text("""
          SELECT sec.entidad_id,te.codigo capacidad
          FROM sesion_contextos_entidad sec
          JOIN tipos_entidad te ON te.id=sec.tipo_entidad_id
          WHERE sec.sesion_id=:session
        """), {"session": row["session_id"]}).mappings().first()
        if refresh:
            db.execute(text("""
              UPDATE sesiones_usuario SET ultima_actividad_en=UTC_TIMESTAMP(6),
                expira_en=LEAST(DATE_ADD(UTC_TIMESTAMP(6),INTERVAL :idle MINUTE),expira_absoluta_en)
              WHERE id=:id
            """), {"idle": get_settings().session_idle_minutes, "id": row["session_id"]})
        result = dict(row)
        result["roles"] = list(roles)
        result["permissions"] = list(permissions)
        entities = []
        for source in notaries:
            item = dict(source)
            raw = item.pop("capacidades_csv", "") or ""
            item["capacidades"] = [value for value in raw.split(",") if value]
            entities.append(item)
        # `notarias` se mantiene como alias contractual mientras los clientes
        # existentes migran al nombre neutral `entidades`.
        result["entidades"] = entities
        result["notarias"] = entities
        result["contexto_entidad"] = dict(active_context) if active_context else None
        return result


def check_csrf(user: dict, supplied: str | None):
    if not supplied or not hmac.compare_digest(user["csrf_hash"], _digest(supplied)):
        raise ValueError("Token CSRF inválido")


def totp_secret() -> str:
    return base64.b32encode(secrets.token_bytes(20)).decode().rstrip("=")


def totp_code(secret: str, moment: int | None = None) -> str:
    counter = int((moment or time.time()) // 30)
    padded = secret + "=" * ((8 - len(secret) % 8) % 8)
    digest = hmac.new(base64.b32decode(padded), struct.pack(">Q", counter), hashlib.sha1).digest()
    offset = digest[-1] & 15
    value = (struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7fffffff) % 1_000_000
    return f"{value:06d}"


def verify_totp(secret: str, code: str) -> bool:
    now = int(time.time())
    return any(hmac.compare_digest(totp_code(secret, now + drift), code) for drift in (-30, 0, 30))


def provisioning_uri(secret: str, email: str) -> str:
    issuer = get_settings().mfa_issuer
    return f"otpauth://totp/{quote(issuer)}:{quote(email)}?secret={secret}&issuer={quote(issuer)}&digits=6&period=30"


def issue_recovery_codes(db, user_id: int):
    db.execute(text("DELETE FROM codigos_recuperacion_mfa WHERE usuario_id=:user"), {"user": user_id})
    codes = [secrets.token_hex(5).upper() for _ in range(8)]
    for code in codes:
        db.execute(text("INSERT INTO codigos_recuperacion_mfa (usuario_id,codigo_hash) VALUES (:user,:hash)"),
                   {"user": user_id, "hash": _digest(code)})
    return codes


def roles_for(db, user_id: int):
    return db.execute(text("""
      SELECT r.codigo FROM roles r JOIN usuarios_roles ur ON ur.rol_id=r.id WHERE ur.usuario_id=:id
    """), {"id": user_id}).scalars().all()
