"""Autenticação por usuário individual (e-mail + senha) com sessão em cookie assinado.

- Senha: `hashlib.scrypt` (stdlib) com sal aleatório; o hash guarda os parâmetros (`scrypt$n$r$p$sal$hash`).
- Sessão: cookie `sessao` = `payload.assinatura` (HMAC-SHA256 com `SECRET_KEY`); o payload leva o id do
  usuário, a validade e uma "versão" derivada do hash da senha — trocar a senha invalida as sessões antigas.
- Contra CSRF: além de `SameSite=Lax`, quem altera dados precisa mandar `X-Requested-With`.
- Bloqueio de força bruta por e-mail (em memória do processo; suficiente para uso interno).
"""

import base64
import hashlib
import hmac
import json
import os
import re
import secrets
import time
from dataclasses import dataclass

from fastapi import Depends, HTTPException, Request
from sqlalchemy.orm import Session

from custos_precificacao import config
from custos_precificacao.api.deps import get_session
from custos_precificacao.models import Usuario

COOKIE = "sessao"
VALIDADE_SESSAO_S = 12 * 60 * 60
SENHA_MINIMA = 10
METODOS_SEGUROS = {"GET", "HEAD", "OPTIONS"}
MAX_FALHAS = 5
BLOQUEIO_S = 5 * 60

_SCRYPT_N, _SCRYPT_R, _SCRYPT_P = 2**14, 8, 1
_EMAIL = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")


@dataclass(frozen=True)
class UsuarioAtual:
    id: int
    email: str
    nome: str
    admin: bool


# --------------------------------------------------------------------------- senha


def _b64(dados: bytes) -> str:
    return base64.urlsafe_b64encode(dados).decode().rstrip("=")


def _unb64(texto: str) -> bytes:
    return base64.urlsafe_b64decode(texto + "=" * (-len(texto) % 4))


def validar_senha(senha: str) -> None:
    if len(senha) < SENHA_MINIMA:
        raise ValueError(f"A senha precisa ter pelo menos {SENHA_MINIMA} caracteres")
    if len(senha) > 200:
        raise ValueError("A senha é longa demais (máximo 200 caracteres)")


def hash_senha(senha: str) -> str:
    sal = secrets.token_bytes(16)
    derivado = hashlib.scrypt(
        senha.encode(), salt=sal, n=_SCRYPT_N, r=_SCRYPT_R, p=_SCRYPT_P, dklen=32
    )
    return f"scrypt${_SCRYPT_N}${_SCRYPT_R}${_SCRYPT_P}${_b64(sal)}${_b64(derivado)}"


def verificar_senha(senha: str, armazenado: str) -> bool:
    try:
        esquema, n, r, p, sal, esperado = armazenado.split("$")
        if esquema != "scrypt":
            return False
        derivado = hashlib.scrypt(
            senha.encode(), salt=_unb64(sal), n=int(n), r=int(r), p=int(p), dklen=32
        )
        return hmac.compare_digest(derivado, _unb64(esperado))
    except (ValueError, TypeError):
        return False


_HASH_FALSO = hash_senha("senha-que-ninguem-usa")  # gasta o mesmo tempo quando o e-mail não existe


# --------------------------------------------------------------------------- sessão


_chave_dev = secrets.token_bytes(32)


def chave_secreta() -> bytes:
    """`SECRET_KEY` do ambiente. Em produção é obrigatória (≥ 32 caracteres); em dev cai numa chave
    aleatória do processo (as sessões se perdem ao reiniciar)."""
    valor = os.getenv("SECRET_KEY", "")
    if config.em_producao():
        if len(valor) < 32:
            raise RuntimeError("SECRET_KEY é obrigatória em produção (mínimo de 32 caracteres)")
        return valor.encode()
    return valor.encode() if valor else _chave_dev


def _versao_da_senha(senha_hash: str) -> str:
    return hashlib.sha256(senha_hash.encode()).hexdigest()[:16]


def _assinar(corpo: str) -> str:
    return _b64(hmac.new(chave_secreta(), corpo.encode(), hashlib.sha256).digest())


def criar_token(usuario: Usuario, agora: float | None = None) -> str:
    payload = {
        "uid": usuario.id,
        "exp": int((agora if agora is not None else time.time()) + VALIDADE_SESSAO_S),
        "sv": _versao_da_senha(usuario.senha_hash),
    }
    corpo = _b64(json.dumps(payload, separators=(",", ":")).encode())
    return f"{corpo}.{_assinar(corpo)}"


def ler_token(token: str | None, agora: float | None = None) -> dict | None:
    """Devolve o payload se a assinatura e a validade estão ok; senão None."""
    if not token or token.count(".") != 1:
        return None
    corpo, assinatura = token.split(".")
    if not hmac.compare_digest(assinatura, _assinar(corpo)):
        return None
    try:
        payload = json.loads(_unb64(corpo))
    except (ValueError, TypeError):
        return None
    if not isinstance(payload, dict) or payload.get("exp", 0) < (agora if agora is not None else time.time()):
        return None
    return payload


# --------------------------------------------------------------------------- bloqueio por tentativas

_falhas: dict[str, tuple[int, float]] = {}  # email -> (falhas seguidas, bloqueado até)


def _purgar_falhas(agora: float) -> None:
    if len(_falhas) > 1000:
        for chave in [c for c, (_, ate) in _falhas.items() if ate < agora]:
            del _falhas[chave]


def checar_bloqueio(email: str, agora: float | None = None) -> None:
    agora = agora if agora is not None else time.time()
    _, ate = _falhas.get(email, (0, 0.0))
    if ate > agora:
        raise HTTPException(status_code=429, detail="Muitas tentativas. Aguarde alguns minutos e tente de novo.")


def registrar_falha(email: str, agora: float | None = None) -> None:
    agora = agora if agora is not None else time.time()
    _purgar_falhas(agora)
    falhas, ate = _falhas.get(email, (0, 0.0))
    if ate and ate <= agora:  # bloqueio anterior já expirou: recomeça a contagem
        falhas = 0
    falhas += 1
    _falhas[email] = (falhas, agora + BLOQUEIO_S if falhas >= MAX_FALHAS else 0.0)


def limpar_falhas(email: str) -> None:
    _falhas.pop(email, None)


def reiniciar_bloqueios() -> None:  # para os testes
    _falhas.clear()


# --------------------------------------------------------------------------- usuários


def normalizar_email(email: str) -> str:
    email = (email or "").strip().lower()
    if not _EMAIL.match(email) or len(email) > 200:
        raise ValueError("E-mail inválido")
    return email


def criar_usuario(session: Session, email: str, nome: str, senha: str, *, admin: bool = False) -> Usuario:
    email = normalizar_email(email)
    nome = (nome or "").strip()
    if not nome:
        raise ValueError("O nome é obrigatório")
    validar_senha(senha)
    if session.query(Usuario).filter_by(email=email).first() is not None:
        raise ValueError(f"Já existe um usuário com o e-mail {email!r}")
    usuario = Usuario(email=email, nome=nome, senha_hash=hash_senha(senha), admin=admin, ativo=True)
    session.add(usuario)
    session.flush()
    return usuario


def autenticar(session: Session, email: str, senha: str) -> Usuario | None:
    """Confere e-mail+senha; None se inválido. Aplica e zera o bloqueio por tentativas."""
    try:
        email = normalizar_email(email)
    except ValueError:
        verificar_senha(senha, _HASH_FALSO)
        return None
    checar_bloqueio(email)
    usuario = session.query(Usuario).filter_by(email=email).one_or_none()
    valido = verificar_senha(senha, usuario.senha_hash if usuario else _HASH_FALSO)
    if usuario is None or not valido or not usuario.ativo:
        registrar_falha(email)
        return None
    limpar_falhas(email)
    return usuario


class NomesDeAutores:
    """Traduz e-mail → nome para exibir autoria (uma consulta por requisição); sem usuário, devolve o e-mail."""

    def __init__(self, session: Session):
        self._nomes = {u.email: u.nome for u in session.query(Usuario)}

    def __call__(self, email: str | None) -> str | None:
        return None if not email else self._nomes.get(email, email)


# --------------------------------------------------------------------------- dependências do FastAPI


def exigir_cabecalho_csrf(request: Request) -> None:
    if request.method not in METODOS_SEGUROS and not request.headers.get("x-requested-with"):
        raise HTTPException(status_code=403, detail="Requisição recusada (cabeçalho X-Requested-With ausente)")


def exigir_usuario(request: Request, session: Session = Depends(get_session)) -> UsuarioAtual:
    if config.auth_desligada():
        session.info["usuario"] = "dev@local"
        return UsuarioAtual(id=0, email="dev@local", nome="Desenvolvimento (sem login)", admin=True)
    exigir_cabecalho_csrf(request)
    payload = ler_token(request.cookies.get(COOKIE))
    usuario = session.get(Usuario, payload["uid"]) if payload and isinstance(payload.get("uid"), int) else None
    if (
        usuario is None
        or not usuario.ativo
        or not hmac.compare_digest(str(payload.get("sv", "")), _versao_da_senha(usuario.senha_hash))
    ):
        raise HTTPException(status_code=401, detail="Sessão expirada ou ausente. Entre novamente.")
    session.info["usuario"] = usuario.email  # quem grava (histórico, custeio, log de atividade) lê daqui
    return UsuarioAtual(id=usuario.id, email=usuario.email, nome=usuario.nome, admin=usuario.admin)


def exigir_admin(atual: UsuarioAtual = Depends(exigir_usuario)) -> UsuarioAtual:
    if not atual.admin:
        raise HTTPException(status_code=403, detail="Apenas administradores podem fazer isso")
    return atual
