"""
auth_module.py — Backend de autenticação magic-link para o Content Studio.

Stack stdlib-only (sqlite3, secrets, hashlib, hmac, base64) — sem dependências externas.

Tabelas (SQLite):
  users(id, email UNIQUE, name, role, beta_access, created_at, updated_at)
  magic_links(id, email, token UNIQUE, expires_at, used_at, created_at)
  sessions(id, user_id, token UNIQUE, expires_at, revoked_at, created_at, user_agent, ip)

Variáveis de ambiente:
  AUTH_DB_PATH      → caminho do SQLite (default: ./auth.db)
  BETA_EMAILS       → whitelist separada por vírgula (ex: a@b.com,c@d.com)
  SECRET_KEY        → chave HMAC pro JWT (gera randômico se ausente, com warning)
  DEV_MODE          → "1" libera retorno do dev_token no response do request-link
  SMTP_HOST         → se setado, send_magic_link_email usa SMTP
  SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM
  MAGIC_LINK_TTL_MIN → minutos pra magic link expirar (default 15)
  SESSION_TTL_DAYS   → dias pra sessão expirar (default 30)
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import json
import os
import secrets
import sqlite3
import smtplib
import sys
from email.mime.text import MIMEText
from datetime import datetime, timedelta, timezone
from typing import Optional, Tuple

# ─── Config (lê env on import) ─────────────────────────────────────────────

AUTH_DB_PATH = os.environ.get("AUTH_DB_PATH", "auth.db")
DEV_MODE = os.environ.get("DEV_MODE") == "1"

_secret_env = os.environ.get("SECRET_KEY")
if not _secret_env:
    _secret_env = secrets.token_urlsafe(48)
    print(f"⚠️  SECRET_KEY ausente — gerei uma temporária (NÃO use em produção): {_secret_env[:8]}…", file=sys.stderr)
SECRET_KEY = _secret_env.encode("utf-8")

MAGIC_LINK_TTL_MIN = int(os.environ.get("MAGIC_LINK_TTL_MIN", "15"))
SESSION_TTL_DAYS = int(os.environ.get("SESSION_TTL_DAYS", "30"))

BETA_EMAILS = {
    e.strip().lower()
    for e in (os.environ.get("BETA_EMAILS", "").split(","))
    if e.strip()
}


# ─── Util ──────────────────────────────────────────────────────────────────

def _now() -> datetime:
    return datetime.now(timezone.utc)


def _now_iso() -> str:
    return _now().isoformat()


def _parse_iso(s: str) -> datetime:
    # SQLite armazena ISO timezone-aware ("…+00:00")
    return datetime.fromisoformat(s)


# ─── JWT (HMAC-SHA256, stdlib only) ────────────────────────────────────────

def _b64url_encode(b: bytes) -> str:
    return base64.urlsafe_b64encode(b).rstrip(b"=").decode("ascii")


def _b64url_decode(s: str) -> bytes:
    pad = "=" * (-len(s) % 4)
    return base64.urlsafe_b64decode(s + pad)


def jwt_encode(payload: dict) -> str:
    header = {"alg": "HS256", "typ": "JWT"}
    h = _b64url_encode(json.dumps(header, separators=(",", ":")).encode("utf-8"))
    p = _b64url_encode(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
    signing_input = f"{h}.{p}".encode("ascii")
    sig = hmac.new(SECRET_KEY, signing_input, hashlib.sha256).digest()
    return f"{h}.{p}.{_b64url_encode(sig)}"


def jwt_decode(token: str) -> Optional[dict]:
    try:
        h, p, s = token.split(".")
    except ValueError:
        return None
    signing_input = f"{h}.{p}".encode("ascii")
    expected = hmac.new(SECRET_KEY, signing_input, hashlib.sha256).digest()
    try:
        provided = _b64url_decode(s)
    except Exception:
        return None
    if not hmac.compare_digest(expected, provided):
        return None
    try:
        payload = json.loads(_b64url_decode(p))
    except Exception:
        return None
    exp = payload.get("exp")
    if exp is not None and exp < int(_now().timestamp()):
        return None
    return payload


# ─── DB ────────────────────────────────────────────────────────────────────

def _connect() -> sqlite3.Connection:
    conn = sqlite3.connect(AUTH_DB_PATH)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    return conn


def init_db() -> None:
    """Cria as tabelas se ainda não existirem. Chame uma vez no startup."""
    with _connect() as conn:
        conn.executescript(
            """
            CREATE TABLE IF NOT EXISTS users (
              id TEXT PRIMARY KEY,
              email TEXT UNIQUE NOT NULL,
              name TEXT,
              role TEXT,
              beta_access INTEGER NOT NULL DEFAULT 1,
              created_at TEXT NOT NULL,
              updated_at TEXT NOT NULL
            );

            CREATE TABLE IF NOT EXISTS magic_links (
              id TEXT PRIMARY KEY,
              email TEXT NOT NULL,
              token TEXT UNIQUE NOT NULL,
              expires_at TEXT NOT NULL,
              used_at TEXT,
              created_at TEXT NOT NULL
            );
            CREATE INDEX IF NOT EXISTS idx_magic_links_token ON magic_links(token);

            CREATE TABLE IF NOT EXISTS sessions (
              id TEXT PRIMARY KEY,
              user_id TEXT NOT NULL,
              token TEXT UNIQUE NOT NULL,
              expires_at TEXT NOT NULL,
              revoked_at TEXT,
              created_at TEXT NOT NULL,
              user_agent TEXT,
              ip TEXT,
              FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
            );
            CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
            """
        )


# ─── Whitelist ─────────────────────────────────────────────────────────────

def is_email_whitelisted(email: str) -> bool:
    """Verifica se o email tem acesso ao beta privado."""
    if not BETA_EMAILS:
        return False
    return email.strip().lower() in BETA_EMAILS


# ─── Magic Links ───────────────────────────────────────────────────────────

class RateLimitExceeded(Exception):
    """Disparada quando o email passou do limite de magic links por janela."""
    def __init__(self, retry_after_seconds: int):
        self.retry_after_seconds = retry_after_seconds
        super().__init__(f"Muitos links solicitados. Tente novamente em {retry_after_seconds}s.")


MAGIC_LINK_MAX_PER_HOUR = int(os.environ.get("MAGIC_LINK_MAX_PER_HOUR", "5"))
MAGIC_LINK_MIN_INTERVAL_SEC = int(os.environ.get("MAGIC_LINK_MIN_INTERVAL_SEC", "30"))


def _check_rate_limit(conn: sqlite3.Connection, email_norm: str) -> None:
    """Lança RateLimitExceeded se o email pediu links demais recentemente.
    Política: máx 5 por hora + intervalo mínimo de 30s entre pedidos."""
    now = _now()
    hour_ago = (now - timedelta(hours=1)).isoformat()

    # Total na última hora
    row = conn.execute(
        "SELECT COUNT(*) as c, MAX(created_at) as last FROM magic_links WHERE email = ? AND created_at > ?",
        (email_norm, hour_ago),
    ).fetchone()

    count = row["c"]
    if count >= MAGIC_LINK_MAX_PER_HOUR:
        # Quanto falta pra um novo slot abrir
        oldest = conn.execute(
            "SELECT created_at FROM magic_links WHERE email = ? AND created_at > ? ORDER BY created_at ASC LIMIT 1",
            (email_norm, hour_ago),
        ).fetchone()
        if oldest:
            opens_at = _parse_iso(oldest["created_at"]) + timedelta(hours=1)
            retry = max(1, int((opens_at - now).total_seconds()))
            raise RateLimitExceeded(retry)

    # Intervalo mínimo desde o último pedido
    if row["last"]:
        last_dt = _parse_iso(row["last"])
        elapsed = (now - last_dt).total_seconds()
        if elapsed < MAGIC_LINK_MIN_INTERVAL_SEC:
            raise RateLimitExceeded(int(MAGIC_LINK_MIN_INTERVAL_SEC - elapsed))


def create_magic_link(email: str) -> dict:
    """Cria magic link, persiste no DB e retorna dados. Não envia email aqui.

    Aplica rate limit: máx 5/hora + intervalo mínimo 30s entre pedidos
    (configurável via MAGIC_LINK_MAX_PER_HOUR e MAGIC_LINK_MIN_INTERVAL_SEC).
    Lança RateLimitExceeded se exceder.
    """
    email_norm = email.strip().lower()
    token = secrets.token_urlsafe(24)  # 32 chars seguros
    now = _now()
    expires = now + timedelta(minutes=MAGIC_LINK_TTL_MIN)
    link_id = secrets.token_urlsafe(8)
    with _connect() as conn:
        _check_rate_limit(conn, email_norm)
        conn.execute(
            "INSERT INTO magic_links (id, email, token, expires_at, created_at) VALUES (?, ?, ?, ?, ?)",
            (link_id, email_norm, token, expires.isoformat(), now.isoformat()),
        )
        conn.commit()
    return {
        "id": link_id,
        "email": email_norm,
        "token": token,
        "expires_at": expires.isoformat(),
    }


def verify_magic_link(token: str) -> Optional[str]:
    """Valida token de magic link. Retorna email se válido, None caso contrário.
    Marca o link como `used_at` na sucesso (uso único)."""
    now = _now()
    with _connect() as conn:
        row = conn.execute(
            "SELECT id, email, expires_at, used_at FROM magic_links WHERE token = ?",
            (token,),
        ).fetchone()
        if not row:
            return None
        if row["used_at"]:
            return None
        if _parse_iso(row["expires_at"]) < now:
            return None
        conn.execute(
            "UPDATE magic_links SET used_at = ? WHERE id = ?",
            (now.isoformat(), row["id"]),
        )
        conn.commit()
        return row["email"]


# ─── Users ─────────────────────────────────────────────────────────────────

def get_or_create_user(email: str) -> dict:
    """Busca o user pelo email; se não existir, cria. Retorna dict do user."""
    email_norm = email.strip().lower()
    now_iso = _now_iso()
    user_id = f"user_{secrets.token_urlsafe(8)}"
    with _connect() as conn:
        conn.execute(
            """
            INSERT OR IGNORE INTO users (id, email, beta_access, created_at, updated_at)
            VALUES (?, ?, 1, ?, ?)
            """,
            (user_id, email_norm, now_iso, now_iso),
        )
        conn.commit()
        row = conn.execute(
            "SELECT id, email, name, role, beta_access, created_at, updated_at FROM users WHERE email = ?",
            (email_norm,),
        ).fetchone()
    return _user_row_to_dict(row)


def update_user(user_id: str, *, name: Optional[str] = None, role: Optional[str] = None) -> Optional[dict]:
    """Atualiza nome e/ou role do user. Retorna user atualizado ou None."""
    sets, args = [], []
    if name is not None:
        sets.append("name = ?")
        args.append(name.strip())
    if role is not None:
        sets.append("role = ?")
        args.append(role.strip())
    if not sets:
        return get_user_by_id(user_id)
    sets.append("updated_at = ?")
    args.append(_now_iso())
    args.append(user_id)
    with _connect() as conn:
        conn.execute(f"UPDATE users SET {', '.join(sets)} WHERE id = ?", args)
        conn.commit()
    return get_user_by_id(user_id)


def get_user_by_id(user_id: str) -> Optional[dict]:
    with _connect() as conn:
        row = conn.execute(
            "SELECT id, email, name, role, beta_access, created_at, updated_at FROM users WHERE id = ?",
            (user_id,),
        ).fetchone()
    return _user_row_to_dict(row) if row else None


def _user_row_to_dict(row: sqlite3.Row) -> dict:
    return {
        "id": row["id"],
        "email": row["email"],
        "name": row["name"],
        "role": row["role"],
        "beta_access": bool(row["beta_access"]),
        "created_at": row["created_at"],
        "updated_at": row["updated_at"],
    }


# ─── Sessions (JWT em cookie) ──────────────────────────────────────────────

def create_session(user_id: str, user_agent: str = "", ip: str = "") -> str:
    """Cria sessão no DB e retorna o JWT pra ser setado como cookie."""
    session_id = f"sess_{secrets.token_urlsafe(8)}"
    now = _now()
    expires = now + timedelta(days=SESSION_TTL_DAYS)
    # JWT payload referencia o session_id (DB-backed) pra permitir revogação
    jwt_payload = {
        "sid": session_id,
        "uid": user_id,
        "iat": int(now.timestamp()),
        "exp": int(expires.timestamp()),
    }
    jwt_token = jwt_encode(jwt_payload)
    with _connect() as conn:
        conn.execute(
            """
            INSERT INTO sessions (id, user_id, token, expires_at, created_at, user_agent, ip)
            VALUES (?, ?, ?, ?, ?, ?, ?)
            """,
            (session_id, user_id, jwt_token, expires.isoformat(), now.isoformat(), user_agent[:255], ip[:64]),
        )
        conn.commit()
    return jwt_token


def verify_session(jwt_token: Optional[str]) -> Optional[dict]:
    """Valida JWT + checa session no DB (não revogada, não expirada). Retorna user dict ou None."""
    if not jwt_token:
        return None
    payload = jwt_decode(jwt_token)
    if not payload:
        return None
    session_id = payload.get("sid")
    user_id = payload.get("uid")
    if not session_id or not user_id:
        return None
    now = _now()
    with _connect() as conn:
        row = conn.execute(
            "SELECT revoked_at, expires_at FROM sessions WHERE id = ? AND token = ?",
            (session_id, jwt_token),
        ).fetchone()
        if not row:
            return None
        if row["revoked_at"]:
            return None
        if _parse_iso(row["expires_at"]) < now:
            return None
    return get_user_by_id(user_id)


def revoke_session(jwt_token: Optional[str]) -> bool:
    """Revoga sessão. Retorna True se algo foi revogado."""
    if not jwt_token:
        return False
    payload = jwt_decode(jwt_token)
    if not payload:
        return False
    session_id = payload.get("sid")
    if not session_id:
        return False
    with _connect() as conn:
        cur = conn.execute(
            "UPDATE sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL",
            (_now_iso(), session_id),
        )
        conn.commit()
        return cur.rowcount > 0


# ─── Envio de magic link (WhatsApp via Evolution → fallback SMTP → mock) ────

def _log_magic_link_sent(
    email: str,
    channel: str,
    target: str,
    success: bool,
    message_id: str,
) -> None:
    """Audit trail: registra todo envio de magic link (sucesso ou erro)."""
    try:
        with _connect() as conn:
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS magic_links_sent (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    email TEXT NOT NULL,
                    channel TEXT NOT NULL,         -- 'whatsapp' | 'smtp' | 'mock'
                    target TEXT NOT NULL,          -- phone ou email pra onde foi
                    success INTEGER NOT NULL,
                    message_id TEXT,
                    created_at TEXT NOT NULL
                )
                """
            )
            conn.execute(
                """
                INSERT INTO magic_links_sent (email, channel, target, success, message_id, created_at)
                VALUES (?, ?, ?, ?, ?, ?)
                """,
                (email, channel, target, 1 if success else 0, message_id, _now_iso()),
            )
            conn.commit()
    except Exception as e:
        # Não pode falhar o envio por causa do audit
        print(f"[AUDIT WARN] Falha ao gravar magic_links_sent: {e}", file=sys.stderr)


def _chatwoot_audit_async(email: str, link: str, channel: str, target: str, success: bool) -> None:
    """
    Cria conversation no Chatwoot inbox Dalton-Pessoal com info do magic link.
    Não bloqueia caso falhe — apenas warning. Chamado em paralelo com envio principal.
    """
    try:
        import chatwoot_sender
        if not chatwoot_sender.is_configured():
            return
        ok, info = chatwoot_sender.audit_magic_link(email, link, channel=channel, target=target, success=success)
        if ok:
            print(f"[CHATWOOT] Audit registrado: {info}")
        else:
            print(f"[CHATWOOT] Audit falhou: {info}", file=sys.stderr)
    except Exception as e:
        print(f"[CHATWOOT] Erro audit: {e}", file=sys.stderr)


def send_magic_link_email(email: str, token: str, base_url: str) -> None:
    """
    Envia o magic link na ordem de preferência:
      1. WhatsApp via Evolution API (se EVOLUTION_API_KEY estiver setado)
      2. SMTP (se SMTP_HOST estiver setado)
      3. Mock no stdout

    Em modo BETA (default), TODOS os envios WhatsApp vão pro Caue (+5511947452497).
    Audit trail em tabela `magic_links_sent`.
    Sprint F: também cria conversation no Chatwoot inbox Dalton-Pessoal (audit visual).
    """
    link = f"{base_url.rstrip('/')}/?token={token}"

    # ─── 1. Tentar WhatsApp via Evolution ──
    try:
        import whatsapp_sender  # local import pra não acoplar no startup
        if whatsapp_sender.is_configured():
            msg_body = whatsapp_sender.build_magic_link_message(link, MAGIC_LINK_TTL_MIN)
            # Phone de destino: usa BETA target se modo beta, senão precisa lookup
            # do telefone do usuário (não implementado ainda — só beta agora)
            target_phone = whatsapp_sender.BETA_WHATSAPP_TARGET if whatsapp_sender.BETA_MODE else ""
            if not target_phone:
                # Sem modo beta + sem lookup de phone do user → cai pra SMTP
                raise RuntimeError("Sem phone alvo fora do modo beta — fallback SMTP")
            ok, info = whatsapp_sender.send_whatsapp(target_phone, msg_body, real_recipient=email)
            _log_magic_link_sent(email, "whatsapp", target_phone, ok, info)
            # Sprint F: audit no Chatwoot (paralelo, não-bloqueante)
            _chatwoot_audit_async(email, link, "whatsapp", target_phone, ok)
            if ok:
                print(f"[WHATSAPP] Magic link enviado para {target_phone} (real: {email}) · id={info}")
                return
            else:
                print(f"[WHATSAPP] Falha: {info} — tentando SMTP fallback", file=sys.stderr)
    except Exception as e:
        print(f"[WHATSAPP] Erro: {e} — tentando SMTP fallback", file=sys.stderr)

    # ─── 2. Fallback SMTP ──
    smtp_host = os.environ.get("SMTP_HOST")
    if not smtp_host:
        # ─── 3. Mock final ──
        print(f"[MOCK EMAIL] Para: {email}")
        print(f"[MOCK EMAIL] Link: {link}")
        print(f"[MOCK EMAIL] Token: {token}")
        _log_magic_link_sent(email, "mock", email, True, "mock-no-channel")
        return

    smtp_port = int(os.environ.get("SMTP_PORT", "587"))
    smtp_user = os.environ.get("SMTP_USER", "")
    smtp_password = os.environ.get("SMTP_PASSWORD", "")
    smtp_from = os.environ.get("SMTP_FROM", smtp_user or "noreply@studio.local")

    body = (
        f"Olá,\n\n"
        f"Use o link abaixo pra entrar no Six Hype (válido por {MAGIC_LINK_TTL_MIN} min):\n\n"
        f"{link}\n\n"
        f"Se você não solicitou este acesso, ignore este email.\n"
    )
    msg = MIMEText(body, "plain", "utf-8")
    msg["Subject"] = "Seu link de acesso · Six Hype"
    msg["From"] = smtp_from
    msg["To"] = email

    try:
        with smtplib.SMTP(smtp_host, smtp_port) as s:
            s.ehlo()
            s.starttls()
            if smtp_user:
                s.login(smtp_user, smtp_password)
            s.sendmail(smtp_from, [email], msg.as_string())
        print(f"[SMTP] Magic link enviado para {email}")
        _log_magic_link_sent(email, "smtp", email, True, "smtp-ok")
    except Exception as e:
        print(f"[SMTP] Falha: {e}", file=sys.stderr)
        _log_magic_link_sent(email, "smtp", email, False, f"error: {e}")
        raise
