"""
climbleads_signup.py — Sprint 1 Fase 2 ClimbLeads
=================================================

Módulo isolado que agrupa TODAS as rotas / lógica adicionadas no Sprint 1
Fase 2. Fica separado do app.py monolítico pra facilitar review, rollback
e manutenção. É registrado via `register_climbleads_routes(app)` no boot.

Cobre:
  Item 2 — Cadastro público self-service (POST /signup, GET /verify/<token>)
  Item 3 — Integração Asaas (customer, trial, subscription, webhook, cancel)
  Item 4 — Enforcement de cota (trial 100 leads/7d, plano 1000 leads/mês)
  Item 5 — LGPD (termos, privacidade, aceite, deleção de conta)

Design:
  - Persistência 100% em users.db (mesmo banco do resto).
  - Tabelas novas criadas idempotentemente em ensure_schema().
  - Email de verificação: se SMTP/Resend/Sendgrid configurado, envia. Caso
    contrário, imprime o link no log e deixa em um arquivo local pra dev.
  - Captcha: hCaptcha OU Turnstile suportados; se nenhum configurado,
    fica desativado (log warn). Ative com HCAPTCHA_SECRET ou TURNSTILE_SECRET.
  - Segurança: rate limit por IP no /signup; token 32 bytes secrets.token_urlsafe.
"""

from __future__ import annotations

import json
import os
import re
import secrets
import sqlite3
import threading
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path

import requests
from flask import (Blueprint, current_app, jsonify, redirect,
                   render_template_string, request, url_for)
from flask_login import current_user, login_required, login_user
from werkzeug.security import generate_password_hash

from auth import (User, _get_conn, buscar_por_email, criar_conta,
                  criar_usuario, get_quota, incrementar_quota)


# ═════════════════════════════════════════════════════════════════════════
# CONFIG
# ═════════════════════════════════════════════════════════════════════════

BASE_DIR = Path(__file__).resolve().parent
DB_PATH = BASE_DIR / "users.db"
DEV_VERIFY_LINKS_LOG = BASE_DIR / "dev_verify_links.log"

# Asaas
ASAAS_API_KEY = os.environ.get("ASAAS_API_KEY", "").strip()
ASAAS_BASE_URL = os.environ.get("ASAAS_BASE_URL", "https://api.asaas.com/v3").rstrip("/")
ASAAS_WEBHOOK_TOKEN = os.environ.get("ASAAS_WEBHOOK_TOKEN", "").strip()

# Email
SMTP_HOST = os.environ.get("SMTP_HOST", "").strip()
SMTP_PORT = int(os.environ.get("SMTP_PORT", "587") or 587)
SMTP_USER = os.environ.get("SMTP_USER", "").strip()
SMTP_PASS = os.environ.get("SMTP_PASS", "").strip()
SMTP_FROM = os.environ.get("SMTP_FROM", "no-reply@agenciaclimb.com.br").strip()
RESEND_API_KEY = os.environ.get("RESEND_API_KEY", "").strip()

# Captcha
HCAPTCHA_SECRET = os.environ.get("HCAPTCHA_SECRET", "").strip()
TURNSTILE_SECRET = os.environ.get("TURNSTILE_SECRET", "").strip()

# URL base pública (pra links de verify em email)
PUBLIC_BASE_URL = os.environ.get("PUBLIC_BASE_URL", "").rstrip("/")

# LGPD
LGPD_TERMOS_VERSAO = "2026-08-13"
LGPD_PRIVACIDADE_VERSAO = "2026-08-13"
LGPD_DPO_EMAIL = "contato@agenciaclimb.com.br"

# Planos e limites
PLANO_TRIAL_DIAS = 7
PLANO_TRIAL_LEADS = 100
PLANO_PAGO_LEADS_MES = 1000
PLANO_MENSAL_VALOR = 197.00
PLANO_ANUAL_VALOR = 1764.00

# Rate limit signup (in-memory, por IP)
_signup_attempts: dict[str, list[float]] = {}
_signup_lock = threading.Lock()
SIGNUP_LIMIT_PER_HOUR = 5


# ═════════════════════════════════════════════════════════════════════════
# SCHEMA (idempotente)
# ═════════════════════════════════════════════════════════════════════════

def ensure_schema() -> None:
    """Cria/migra tabelas do Sprint 1 Fase 2 se ainda não existem."""
    conn = sqlite3.connect(str(DB_PATH))
    cur = conn.cursor()

    # Metadados adicionais no signup (não quebra User existente)
    cur.execute("""
        CREATE TABLE IF NOT EXISTS user_signup_meta (
            user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
            telefone TEXT,
            cpf_cnpj TEXT,
            email_verified_at TEXT,
            verify_token TEXT UNIQUE,
            verify_token_expira_em TEXT,
            criado_em TEXT NOT NULL,
            ip_signup TEXT,
            user_agent TEXT
        )
    """)

    # Aceites LGPD
    cur.execute("""
        CREATE TABLE IF NOT EXISTS lgpd_aceites (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            termos_versao TEXT NOT NULL,
            privacidade_versao TEXT NOT NULL,
            ip TEXT,
            aceito_em TEXT NOT NULL
        )
    """)

    # Assinaturas (uma conta = uma assinatura ativa por vez, mas mantemos histórico)
    cur.execute("""
        CREATE TABLE IF NOT EXISTS assinaturas (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            conta_id INTEGER NOT NULL REFERENCES contas(id) ON DELETE CASCADE,
            user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
            asaas_customer_id TEXT,
            asaas_subscription_id TEXT UNIQUE,
            status TEXT NOT NULL DEFAULT 'trial',
            plano TEXT NOT NULL DEFAULT 'trial',
            valor REAL DEFAULT 0,
            trial_expira_em TEXT,
            ultimo_pagamento TEXT,
            proximo_pagamento TEXT,
            criado_em TEXT NOT NULL,
            atualizado_em TEXT
        )
    """)

    # Eventos de webhook Asaas (auditoria)
    cur.execute("""
        CREATE TABLE IF NOT EXISTS asaas_webhook_events (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            event_id TEXT UNIQUE,
            event_type TEXT,
            payment_id TEXT,
            subscription_id TEXT,
            customer_id TEXT,
            payload TEXT,
            processado_em TEXT NOT NULL
        )
    """)

    # Solicitações de deleção de conta (LGPD)
    cur.execute("""
        CREATE TABLE IF NOT EXISTS lgpd_delecoes (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
            solicitado_em TEXT NOT NULL,
            executar_em TEXT NOT NULL,
            executado_em TEXT,
            status TEXT NOT NULL DEFAULT 'pendente'
        )
    """)

    # Migração: coluna quota_trial_expira em ghl_config (pra trial de 7d por conta)
    for ddl in (
        "ALTER TABLE ghl_config ADD COLUMN trial_expira_em TEXT",
    ):
        try:
            cur.execute(ddl)
        except sqlite3.OperationalError:
            pass

    conn.commit()
    conn.close()


# ═════════════════════════════════════════════════════════════════════════
# EMAIL (mock em dev, real via SMTP/Resend)
# ═════════════════════════════════════════════════════════════════════════

def enviar_email(destinatario: str, assunto: str, html_body: str,
                 text_body: str = "") -> tuple[bool, str]:
    """Envia email. Ordem de prioridade:
       1. Resend (se RESEND_API_KEY)
       2. SMTP (se SMTP_HOST + SMTP_USER + SMTP_PASS)
       3. Mock (grava em log + arquivo, retorna True)
    """
    text_body = text_body or re.sub(r"<[^>]+>", "", html_body)

    if RESEND_API_KEY:
        try:
            r = requests.post(
                "https://api.resend.com/emails",
                headers={
                    "Authorization": f"Bearer {RESEND_API_KEY}",
                    "Content-Type": "application/json",
                },
                json={
                    "from": SMTP_FROM,
                    "to": [destinatario],
                    "subject": assunto,
                    "html": html_body,
                    "text": text_body,
                },
                timeout=15,
            )
            if r.status_code < 300:
                return True, "resend"
            return False, f"resend HTTP {r.status_code}: {r.text[:200]}"
        except Exception as e:
            return False, f"resend erro: {e}"

    if SMTP_HOST and SMTP_USER and SMTP_PASS:
        try:
            import smtplib
            from email.mime.multipart import MIMEMultipart
            from email.mime.text import MIMEText
            msg = MIMEMultipart("alternative")
            msg["Subject"] = assunto
            msg["From"] = SMTP_FROM
            msg["To"] = destinatario
            msg.attach(MIMEText(text_body, "plain", "utf-8"))
            msg.attach(MIMEText(html_body, "html", "utf-8"))
            with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=15) as s:
                s.starttls()
                s.login(SMTP_USER, SMTP_PASS)
                s.sendmail(SMTP_FROM, [destinatario], msg.as_string())
            return True, "smtp"
        except Exception as e:
            return False, f"smtp erro: {e}"

    # Mock (dev)
    try:
        with DEV_VERIFY_LINKS_LOG.open("a", encoding="utf-8") as f:
            f.write(f"[{datetime.now().isoformat()}] TO={destinatario} | SUBJ={assunto}\n")
            f.write(text_body + "\n" + ("-" * 60) + "\n")
    except Exception:
        pass
    print(f"[EMAIL-MOCK] to={destinatario} subj={assunto!r}")
    return True, "mock"


# ═════════════════════════════════════════════════════════════════════════
# CAPTCHA
# ═════════════════════════════════════════════════════════════════════════

def _validar_captcha(token: str, remote_ip: str) -> tuple[bool, str]:
    """Valida hCaptcha OU Turnstile. Retorna (ok, provedor|erro).
       Se nenhum secret configurado, retorna (True, 'disabled') pra não
       travar dev — mas loga um WARN.
    """
    if not token:
        if not (HCAPTCHA_SECRET or TURNSTILE_SECRET):
            print("[captcha] WARN: nenhum provedor configurado; aceitando signup sem captcha")
            return True, "disabled"
        return False, "captcha ausente"

    if HCAPTCHA_SECRET:
        try:
            r = requests.post(
                "https://hcaptcha.com/siteverify",
                data={"secret": HCAPTCHA_SECRET, "response": token, "remoteip": remote_ip},
                timeout=8,
            )
            if r.status_code == 200 and r.json().get("success"):
                return True, "hcaptcha"
            return False, f"hcaptcha: {r.text[:120]}"
        except Exception as e:
            return False, f"hcaptcha erro: {e}"

    if TURNSTILE_SECRET:
        try:
            r = requests.post(
                "https://challenges.cloudflare.com/turnstile/v0/siteverify",
                data={"secret": TURNSTILE_SECRET, "response": token, "remoteip": remote_ip},
                timeout=8,
            )
            if r.status_code == 200 and r.json().get("success"):
                return True, "turnstile"
            return False, f"turnstile: {r.text[:120]}"
        except Exception as e:
            return False, f"turnstile erro: {e}"

    return False, "nenhum provedor configurado mas token enviado"


# ═════════════════════════════════════════════════════════════════════════
# ASAAS API
# ═════════════════════════════════════════════════════════════════════════

def _asaas_headers() -> dict:
    return {
        "access_token": ASAAS_API_KEY,
        "Content-Type": "application/json",
        "User-Agent": "ClimbLeads/1.0",
    }


def asaas_criar_customer(nome: str, email: str, cpf_cnpj: str,
                          telefone: str = "") -> tuple[str | None, str]:
    """Cria customer no Asaas. Retorna (customer_id, erro)."""
    if not ASAAS_API_KEY:
        return None, "ASAAS_API_KEY não configurado"
    payload = {
        "name": nome,
        "email": email,
        "cpfCnpj": re.sub(r"\D", "", cpf_cnpj or ""),
    }
    if telefone:
        payload["mobilePhone"] = re.sub(r"\D", "", telefone)
    try:
        r = requests.post(f"{ASAAS_BASE_URL}/customers", headers=_asaas_headers(),
                          json=payload, timeout=15)
        if r.status_code in (200, 201):
            return r.json().get("id"), ""
        return None, f"HTTP {r.status_code}: {r.text[:200]}"
    except Exception as e:
        return None, str(e)


def asaas_criar_subscription(customer_id: str, plano: str, credit_card: dict,
                              credit_card_holder: dict, remote_ip: str) -> tuple[str | None, str, dict]:
    """Cria assinatura recorrente com cartão tokenizado.
       plano: 'mensal' | 'anual'
       Retorna (subscription_id, erro, payload_completo).
    """
    if not ASAAS_API_KEY:
        return None, "ASAAS_API_KEY não configurado", {}

    if plano == "anual":
        valor = PLANO_ANUAL_VALOR
        ciclo = "YEARLY"
    else:
        valor = PLANO_MENSAL_VALOR
        ciclo = "MONTHLY"

    proximo_venc = (datetime.now() + timedelta(days=PLANO_TRIAL_DIAS)).strftime("%Y-%m-%d")
    payload = {
        "customer": customer_id,
        "billingType": "CREDIT_CARD",
        "value": valor,
        "nextDueDate": proximo_venc,
        "cycle": ciclo,
        "description": f"ClimbLeads plano {plano}",
        "creditCard": credit_card,
        "creditCardHolderInfo": credit_card_holder,
        "remoteIp": remote_ip,
    }
    try:
        r = requests.post(f"{ASAAS_BASE_URL}/subscriptions", headers=_asaas_headers(),
                          json=payload, timeout=20)
        if r.status_code in (200, 201):
            data = r.json()
            return data.get("id"), "", data
        return None, f"HTTP {r.status_code}: {r.text[:300]}", {}
    except Exception as e:
        return None, str(e), {}


def asaas_cancelar_subscription(subscription_id: str) -> tuple[bool, str]:
    if not ASAAS_API_KEY:
        return False, "ASAAS_API_KEY não configurado"
    try:
        r = requests.delete(f"{ASAAS_BASE_URL}/subscriptions/{subscription_id}",
                            headers=_asaas_headers(), timeout=15)
        if r.status_code in (200, 204):
            return True, ""
        return False, f"HTTP {r.status_code}: {r.text[:200]}"
    except Exception as e:
        return False, str(e)


# ═════════════════════════════════════════════════════════════════════════
# HELPERS DE ASSINATURA
# ═════════════════════════════════════════════════════════════════════════

def get_assinatura_by_conta(conta_id: int) -> dict | None:
    conn = _get_conn()
    row = conn.execute(
        "SELECT * FROM assinaturas WHERE conta_id = ? ORDER BY id DESC LIMIT 1",
        (conta_id,),
    ).fetchone()
    conn.close()
    return dict(row) if row else None


def criar_assinatura_trial(conta_id: int, user_id: int,
                            asaas_customer_id: str = "") -> dict:
    trial_fim = (datetime.now() + timedelta(days=PLANO_TRIAL_DIAS)).isoformat()
    conn = _get_conn()
    cur = conn.execute("""
        INSERT INTO assinaturas
            (conta_id, user_id, asaas_customer_id, status, plano, valor,
             trial_expira_em, criado_em, atualizado_em)
        VALUES (?, ?, ?, 'trial', 'trial', 0, ?, ?, ?)
    """, (conta_id, user_id, asaas_customer_id, trial_fim,
          datetime.now().isoformat(), datetime.now().isoformat()))
    # Também escreve trial_expira_em em ghl_config pra facilitar enforcement
    conn.execute("""
        INSERT INTO ghl_config (conta_id, leads_limite, leads_usados, quota_reset_em, trial_expira_em)
        VALUES (?, ?, 0, ?, ?)
        ON CONFLICT(conta_id) DO UPDATE SET
            leads_limite=excluded.leads_limite,
            trial_expira_em=excluded.trial_expira_em,
            quota_reset_em=excluded.quota_reset_em
    """, (conta_id, PLANO_TRIAL_LEADS, datetime.now().isoformat(), trial_fim))
    conn.commit()
    ass_id = cur.lastrowid
    row = conn.execute("SELECT * FROM assinaturas WHERE id = ?", (ass_id,)).fetchone()
    conn.close()
    return dict(row)


def atualizar_assinatura_paga(conta_id: int, subscription_id: str,
                               plano: str, valor: float) -> None:
    conn = _get_conn()
    conn.execute("""
        UPDATE assinaturas
           SET asaas_subscription_id = ?, status = 'ativa',
               plano = ?, valor = ?, atualizado_em = ?
         WHERE conta_id = ?
           AND id = (SELECT id FROM assinaturas WHERE conta_id = ? ORDER BY id DESC LIMIT 1)
    """, (subscription_id, plano, valor, datetime.now().isoformat(),
          conta_id, conta_id))
    # Escala quota da conta pra 1000/mês
    conn.execute("""
        UPDATE ghl_config
           SET leads_limite = ?, leads_usados = 0, trial_expira_em = NULL,
               quota_reset_em = ?
         WHERE conta_id = ?
    """, (PLANO_PAGO_LEADS_MES, datetime.now().isoformat(), conta_id))
    conn.commit()
    conn.close()


def suspender_assinatura(conta_id: int, motivo: str = "pagamento") -> None:
    conn = _get_conn()
    conn.execute("""
        UPDATE assinaturas SET status = ?, atualizado_em = ?
         WHERE conta_id = ?
           AND id = (SELECT id FROM assinaturas WHERE conta_id = ? ORDER BY id DESC LIMIT 1)
    """, (f"suspensa_{motivo}", datetime.now().isoformat(), conta_id, conta_id))
    conn.commit()
    conn.close()


def reativar_assinatura(conta_id: int) -> None:
    conn = _get_conn()
    conn.execute("""
        UPDATE assinaturas SET status = 'ativa', atualizado_em = ?
         WHERE conta_id = ?
           AND id = (SELECT id FROM assinaturas WHERE conta_id = ? ORDER BY id DESC LIMIT 1)
    """, (datetime.now().isoformat(), conta_id, conta_id))
    conn.commit()
    conn.close()


def assinatura_permite_uso(conta_id: int) -> tuple[bool, str]:
    """True se conta pode usar a ferramenta (trial válido OU assinatura ativa)."""
    ass = get_assinatura_by_conta(conta_id)
    if not ass:
        # Contas legadas (pré Fase 2) sem assinatura: liberadas (comportamento antigo)
        return True, "legado"
    status = ass.get("status") or ""
    if status == "ativa":
        return True, "ativa"
    if status == "trial":
        trial_fim = ass.get("trial_expira_em")
        if trial_fim:
            try:
                if datetime.fromisoformat(trial_fim) >= datetime.now():
                    return True, "trial"
                return False, "trial_expirado"
            except Exception:
                return False, "trial_data_invalida"
        return True, "trial"
    if status.startswith("suspensa"):
        return False, status
    if status == "cancelada":
        return False, "cancelada"
    return False, status or "indefinido"


# ═════════════════════════════════════════════════════════════════════════
# RATE LIMIT SIGNUP
# ═════════════════════════════════════════════════════════════════════════

def _signup_pode(ip: str) -> bool:
    now = time.time()
    with _signup_lock:
        arr = [t for t in _signup_attempts.get(ip, []) if now - t < 3600]
        _signup_attempts[ip] = arr
        return len(arr) < SIGNUP_LIMIT_PER_HOUR


def _signup_registrar(ip: str) -> None:
    with _signup_lock:
        _signup_attempts.setdefault(ip, []).append(time.time())


# ═════════════════════════════════════════════════════════════════════════
# CPF/CNPJ validação básica (só formato, não dígito verificador — Asaas valida)
# ═════════════════════════════════════════════════════════════════════════

def _cpf_cnpj_ok(raw: str) -> bool:
    d = re.sub(r"\D", "", raw or "")
    return len(d) in (11, 14)


def _senha_ok(senha: str) -> tuple[bool, str]:
    if len(senha or "") < 8:
        return False, "senha precisa ter ao menos 8 caracteres"
    return True, ""


# ═════════════════════════════════════════════════════════════════════════
# BLUEPRINT
# ═════════════════════════════════════════════════════════════════════════

bp = Blueprint("climbleads", __name__)


# ── ITEM 2: SIGNUP + VERIFY ──────────────────────────────────────────────

@bp.route("/signup", methods=["POST"])
def signup_post():
    ip = (request.headers.get("X-Forwarded-For") or request.remote_addr or "?").split(",")[0].strip()

    if not _signup_pode(ip):
        return jsonify({"ok": False, "erro": "muitas tentativas, aguarde 1h"}), 429

    data = request.get_json(silent=True) or request.form.to_dict()
    email = (data.get("email") or "").strip().lower()
    senha = (data.get("senha") or "").strip()
    nome = (data.get("nome") or "").strip()
    telefone = (data.get("telefone") or "").strip()
    cpf_cnpj = (data.get("cpf_cnpj") or "").strip()
    aceite_lgpd = bool(data.get("aceite_lgpd"))
    captcha_token = data.get("captcha_token") or data.get("h-captcha-response") or data.get("cf-turnstile-response") or ""

    # Validações básicas
    if not email or not re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", email):
        return jsonify({"ok": False, "erro": "email inválido"}), 400
    if not nome or len(nome) < 2:
        return jsonify({"ok": False, "erro": "nome obrigatório"}), 400
    ok_senha, erro_senha = _senha_ok(senha)
    if not ok_senha:
        return jsonify({"ok": False, "erro": erro_senha}), 400
    if not telefone or len(re.sub(r"\D", "", telefone)) < 10:
        return jsonify({"ok": False, "erro": "telefone inválido (com DDD)"}), 400
    if not _cpf_cnpj_ok(cpf_cnpj):
        return jsonify({"ok": False, "erro": "CPF ou CNPJ inválido"}), 400
    if not aceite_lgpd:
        return jsonify({"ok": False, "erro": "é obrigatório aceitar Termos e Privacidade"}), 400

    # Captcha (opcional se nenhum secret configurado)
    ok_cap, msg_cap = _validar_captcha(captcha_token, ip)
    if not ok_cap:
        return jsonify({"ok": False, "erro": f"captcha inválido: {msg_cap}"}), 400

    if buscar_por_email(email):
        return jsonify({"ok": False, "erro": "email já cadastrado"}), 409

    # Cria conta ClimbLeads + user + trial
    conta = criar_conta(nome=f"{nome} (self-signup)")
    user, err = criar_usuario(nome=nome, email=email, senha=senha,
                              plano="trial", conta_id=conta["id"])
    if not user:
        return jsonify({"ok": False, "erro": f"não conseguimos criar sua conta: {err}"}), 500

    # Cria assinatura trial local (Asaas customer é opcional aqui, criado no checkout)
    criar_assinatura_trial(conta["id"], int(user.id))

    # Registra aceite LGPD
    conn = _get_conn()
    conn.execute("""
        INSERT INTO lgpd_aceites (user_id, termos_versao, privacidade_versao, ip, aceito_em)
        VALUES (?, ?, ?, ?, ?)
    """, (int(user.id), LGPD_TERMOS_VERSAO, LGPD_PRIVACIDADE_VERSAO, ip, datetime.now().isoformat()))
    conn.commit()

    # Gera token de verificação (24h)
    token = secrets.token_urlsafe(32)
    expira = (datetime.now() + timedelta(hours=24)).isoformat()
    conn.execute("""
        INSERT INTO user_signup_meta
            (user_id, telefone, cpf_cnpj, verify_token, verify_token_expira_em,
             criado_em, ip_signup, user_agent)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT(user_id) DO UPDATE SET
            verify_token=excluded.verify_token,
            verify_token_expira_em=excluded.verify_token_expira_em
    """, (int(user.id), telefone, re.sub(r"\D", "", cpf_cnpj), token, expira,
          datetime.now().isoformat(), ip, request.headers.get("User-Agent", "")[:200]))
    # Deixa o usuário INATIVO até verificar email
    conn.execute("UPDATE users SET ativo = 0 WHERE id = ?", (int(user.id),))
    conn.commit()
    conn.close()

    _signup_registrar(ip)

    # Envia email
    base_url = PUBLIC_BASE_URL or request.host_url.rstrip("/")
    link = f"{base_url}/verify/{token}"
    html = f"""
        <p>Olá, {nome}!</p>
        <p>Bem-vindo ao ClimbLeads. Pra ativar sua conta, clique no link abaixo (válido por 24h):</p>
        <p><a href="{link}">{link}</a></p>
        <p>Se você não solicitou este cadastro, ignore este email.</p>
        <p>— Equipe Climb Digital</p>
    """
    ok_mail, provedor = enviar_email(email, "Ative sua conta ClimbLeads", html)

    return jsonify({
        "ok": True,
        "user_id": int(user.id),
        "conta_id": conta["id"],
        "email": email,
        "verify_email_enviado": ok_mail,
        "email_provedor": provedor,
        "mensagem": "conta criada, verifique seu email pra ativar",
    }), 201


@bp.route("/verify/<token>", methods=["GET"])
def verify_get(token: str):
    conn = _get_conn()
    row = conn.execute("""
        SELECT usm.user_id, usm.verify_token_expira_em, u.email, u.nome, u.conta_id
          FROM user_signup_meta usm
          JOIN users u ON u.id = usm.user_id
         WHERE usm.verify_token = ?
    """, (token,)).fetchone()
    if not row:
        conn.close()
        return _pagina_simples("Link inválido", "Este link de verificação não existe ou já foi usado."), 404

    try:
        expira = datetime.fromisoformat(row["verify_token_expira_em"])
    except Exception:
        expira = datetime.now() - timedelta(days=1)
    if expira < datetime.now():
        conn.close()
        return _pagina_simples("Link expirado",
            "Este link expirou (válido por 24h). Solicite um novo pelo suporte."), 410

    user_id = int(row["user_id"])
    conn.execute("UPDATE users SET ativo = 1 WHERE id = ?", (user_id,))
    conn.execute("""
        UPDATE user_signup_meta
           SET email_verified_at = ?, verify_token = NULL
         WHERE user_id = ?
    """, (datetime.now().isoformat(), user_id))
    conn.commit()
    conn.close()

    # (Opcional) auto login pós verify
    user = User.get(user_id)
    if user:
        try:
            login_user(user, remember=True)
        except Exception:
            pass

    return _pagina_simples(
        "Conta ativada com sucesso",
        f"Bem-vindo, {row['nome']}. Sua conta ClimbLeads está pronta pra usar. "
        f"<a href='{url_for('index') if _tem_endpoint('index') else '/'}'>Ir pra ferramenta</a>",
    )


# ── ITEM 3: ASAAS CHECKOUT + WEBHOOK ────────────────────────────────────

@bp.route("/api/asaas/checkout", methods=["POST"])
@login_required
def asaas_checkout():
    """Cria/reusa customer Asaas e ativa subscription com cartão tokenizado.
       Espera JSON:
         { plano: 'mensal'|'anual',
           credit_card: { holderName, number, expiryMonth, expiryYear, ccv },
           credit_card_holder: { name, email, cpfCnpj, postalCode, addressNumber, phone } }
    """
    ip = (request.headers.get("X-Forwarded-For") or request.remote_addr or "?").split(",")[0]
    data = request.get_json(silent=True) or {}
    plano = (data.get("plano") or "mensal").lower()
    if plano not in ("mensal", "anual"):
        return jsonify({"ok": False, "erro": "plano inválido"}), 400
    credit_card = data.get("credit_card") or {}
    credit_card_holder = data.get("credit_card_holder") or {}
    if not credit_card.get("number") or not credit_card.get("ccv"):
        return jsonify({"ok": False, "erro": "cartão obrigatório"}), 400

    conta_id = int(current_user.conta_id or 0)
    if not conta_id:
        return jsonify({"ok": False, "erro": "usuário sem conta"}), 400

    # Busca ou cria customer Asaas
    ass = get_assinatura_by_conta(conta_id) or {}
    customer_id = ass.get("asaas_customer_id") or ""
    if not customer_id:
        # Puxa CPF/CNPJ do signup
        conn = _get_conn()
        meta = conn.execute("SELECT cpf_cnpj, telefone FROM user_signup_meta WHERE user_id=?",
                            (int(current_user.id),)).fetchone()
        conn.close()
        customer_id, err = asaas_criar_customer(
            nome=current_user.nome,
            email=current_user.email,
            cpf_cnpj=(meta["cpf_cnpj"] if meta else credit_card_holder.get("cpfCnpj", "")),
            telefone=(meta["telefone"] if meta else credit_card_holder.get("phone", "")),
        )
        if not customer_id:
            return jsonify({"ok": False, "erro": f"asaas customer: {err}"}), 502

    sub_id, err_sub, resp = asaas_criar_subscription(
        customer_id=customer_id,
        plano=plano,
        credit_card=credit_card,
        credit_card_holder=credit_card_holder,
        remote_ip=ip,
    )
    if not sub_id:
        return jsonify({"ok": False, "erro": f"asaas subscription: {err_sub}"}), 502

    valor = PLANO_ANUAL_VALOR if plano == "anual" else PLANO_MENSAL_VALOR
    atualizar_assinatura_paga(conta_id, sub_id, plano, valor)
    # Salva customer_id na assinatura (se ainda não estava)
    conn = _get_conn()
    conn.execute("""
        UPDATE assinaturas SET asaas_customer_id = ?
         WHERE conta_id = ?
           AND id = (SELECT id FROM assinaturas WHERE conta_id = ? ORDER BY id DESC LIMIT 1)
    """, (customer_id, conta_id, conta_id))
    conn.commit()
    conn.close()

    return jsonify({
        "ok": True,
        "subscription_id": sub_id,
        "customer_id": customer_id,
        "plano": plano,
        "valor": valor,
    }), 200


@bp.route("/api/asaas/cancelar", methods=["POST"])
@login_required
def asaas_cancelar():
    """Cancelamento self-service."""
    conta_id = int(current_user.conta_id or 0)
    ass = get_assinatura_by_conta(conta_id)
    if not ass or not ass.get("asaas_subscription_id"):
        return jsonify({"ok": False, "erro": "sem assinatura ativa"}), 404
    ok, err = asaas_cancelar_subscription(ass["asaas_subscription_id"])
    if not ok:
        return jsonify({"ok": False, "erro": err}), 502
    conn = _get_conn()
    conn.execute("""
        UPDATE assinaturas SET status = 'cancelada', atualizado_em = ?
         WHERE id = ?
    """, (datetime.now().isoformat(), ass["id"]))
    conn.commit()
    conn.close()
    return jsonify({"ok": True})


@bp.route("/webhooks/asaas", methods=["POST"])
def webhook_asaas():
    """Recebe eventos do Asaas. Valida token (se configurado) e processa."""
    # Segurança: Asaas envia asaas-access-token header
    if ASAAS_WEBHOOK_TOKEN:
        recv = request.headers.get("asaas-access-token", "")
        if recv != ASAAS_WEBHOOK_TOKEN:
            return jsonify({"ok": False, "erro": "token inválido"}), 401

    payload = request.get_json(silent=True) or {}
    event_id = payload.get("id") or ""
    event_type = payload.get("event") or ""
    payment = payload.get("payment") or {}
    subscription = payment.get("subscription") or payload.get("subscription", {}).get("id") if isinstance(payload.get("subscription"), dict) else payload.get("subscription")
    if isinstance(subscription, dict):
        subscription_id = subscription.get("id", "")
    else:
        subscription_id = subscription or ""
    payment_id = payment.get("id") or ""
    customer_id = payment.get("customer") or ""

    conn = _get_conn()
    # Dedupe por event_id
    try:
        conn.execute("""
            INSERT INTO asaas_webhook_events
                (event_id, event_type, payment_id, subscription_id, customer_id, payload, processado_em)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        """, (event_id or f"noid-{time.time_ns()}", event_type, payment_id,
              subscription_id, customer_id, json.dumps(payload)[:5000],
              datetime.now().isoformat()))
        conn.commit()
    except sqlite3.IntegrityError:
        # já processado
        conn.close()
        return jsonify({"ok": True, "dedup": True})

    # Localiza conta pela subscription (ou customer)
    row = None
    if subscription_id:
        row = conn.execute(
            "SELECT * FROM assinaturas WHERE asaas_subscription_id = ?", (subscription_id,)
        ).fetchone()
    if not row and customer_id:
        row = conn.execute(
            "SELECT * FROM assinaturas WHERE asaas_customer_id = ? ORDER BY id DESC LIMIT 1",
            (customer_id,),
        ).fetchone()
    conn.close()

    if not row:
        # Evento pra conta desconhecida (pode ser lixo/teste): registra e ignora
        return jsonify({"ok": True, "sem_assinatura_vinculada": True})

    conta_id = int(row["conta_id"])

    if event_type in ("PAYMENT_CONFIRMED", "PAYMENT_RECEIVED"):
        conn = _get_conn()
        proximo = payment.get("dueDate") or ""
        conn.execute("""
            UPDATE assinaturas SET status='ativa', ultimo_pagamento=?, proximo_pagamento=?, atualizado_em=?
             WHERE id=?
        """, (datetime.now().isoformat(), proximo, datetime.now().isoformat(), row["id"]))
        conn.execute("""
            UPDATE ghl_config SET leads_usados = 0, quota_reset_em = ?
             WHERE conta_id = ?
        """, (datetime.now().isoformat(), conta_id))
        conn.commit()
        conn.close()
        reativar_assinatura(conta_id)
    elif event_type == "PAYMENT_OVERDUE":
        suspender_assinatura(conta_id, motivo="atrasada")
    elif event_type == "PAYMENT_REFUNDED":
        suspender_assinatura(conta_id, motivo="reembolsada")
    elif event_type in ("SUBSCRIPTION_DELETED", "SUBSCRIPTION_INACTIVATED"):
        conn = _get_conn()
        conn.execute("""
            UPDATE assinaturas SET status='cancelada', atualizado_em=? WHERE id=?
        """, (datetime.now().isoformat(), row["id"]))
        conn.commit()
        conn.close()

    return jsonify({"ok": True, "evento": event_type, "conta_id": conta_id})


# ── ITEM 4: STATUS DE COTA / ASSINATURA ─────────────────────────────────

@bp.route("/api/minha-conta", methods=["GET"])
@login_required
def minha_conta():
    conta_id = int(current_user.conta_id or 0)
    ass = get_assinatura_by_conta(conta_id) or {}
    quota = get_quota(conta_id)
    permite, motivo = assinatura_permite_uso(conta_id)
    return jsonify({
        "ok": True,
        "user": {"id": current_user.id, "nome": current_user.nome, "email": current_user.email},
        "conta_id": conta_id,
        "assinatura": {k: ass.get(k) for k in
                       ("status", "plano", "valor", "trial_expira_em",
                        "ultimo_pagamento", "proximo_pagamento", "asaas_subscription_id")},
        "quota": quota,
        "acesso_liberado": permite,
        "acesso_motivo": motivo,
    })


# ── ITEM 5: LGPD ────────────────────────────────────────────────────────

_TERMOS_HTML = """
<!doctype html><meta charset="utf-8"><title>Termos de Uso - ClimbLeads</title>
<style>body{font-family:system-ui;max-width:820px;margin:40px auto;padding:0 24px;line-height:1.55;color:#222}
h1,h2{color:#0a2540}.small{color:#666;font-size:14px}</style>
<h1>Termos de Uso — ClimbLeads</h1>
<p class="small">Versão {ver} — Climb Digital LTDA (contato: contato@agenciaclimb.com.br)</p>

<h2>1. Objeto</h2>
<p>O ClimbLeads é uma plataforma SaaS de prospecção ativa business-to-business
que coleta dados publicamente disponíveis (Google Maps / Places, sites e redes
sociais abertos) e integra esses dados a um CRM do usuário para fins de contato
comercial legítimo.</p>

<h2>2. Aceite</h2>
<p>Ao criar conta, o usuário declara ter capacidade civil, ser responsável pelas
credenciais e pelo uso conferido aos leads coletados, incluindo o cumprimento da
LGPD (Lei 13.709/2018) e do Código de Defesa do Consumidor.</p>

<h2>3. Prospecção sobre dados públicos</h2>
<p>Os leads entregues são pessoas jurídicas ou profissionais cujas informações
comerciais estão publicamente indexadas. O usuário assume responsabilidade pelo
contato realizado e pelos direitos dos titulares em opt-out imediato.</p>

<h2>4. Planos e cobrança</h2>
<p>Trial de 7 dias com limite de 100 leads mediante cartão pré-autorizado. Ao
final do trial, cobrança automática do plano mensal (R$ 197) ou anual (R$ 1.764)
escolhido no checkout. Cancelamento self-service a qualquer momento.</p>

<h2>5. Suspensão e rescisão</h2>
<p>Contas ficam suspensas em caso de inadimplência (retorna PAYMENT_OVERDUE do
gateway), reembolso ou cancelamento. Reativação automática ao recebimento do
pagamento (PAYMENT_CONFIRMED).</p>

<h2>6. Limitação de responsabilidade</h2>
<p>A Climb Digital não se responsabiliza por decisões comerciais tomadas com base
nos leads entregues nem por eventuais indisponibilidades das APIs de terceiros
(Google, GHL/Linkia, Asaas).</p>

<h2>7. Foro</h2>
<p>Fica eleito o foro da comarca de Belo Horizonte (MG) para dirimir quaisquer
questões oriundas destes Termos.</p>
""".replace("{ver}", LGPD_TERMOS_VERSAO)


_PRIVACIDADE_HTML = """
<!doctype html><meta charset="utf-8"><title>Política de Privacidade - ClimbLeads</title>
<style>body{font-family:system-ui;max-width:820px;margin:40px auto;padding:0 24px;line-height:1.55;color:#222}
h1,h2{color:#0a2540}.small{color:#666;font-size:14px}</style>
<h1>Política de Privacidade — ClimbLeads</h1>
<p class="small">Versão {ver} — DPO: {dpo}</p>

<h2>1. Dados coletados</h2>
<ul>
  <li><b>Cadastro:</b> nome, email, telefone, CPF/CNPJ, senha (hash).</li>
  <li><b>Pagamento:</b> tokenização do cartão feita diretamente no Asaas;
      NÃO armazenamos número de cartão em nossos servidores.</li>
  <li><b>Uso:</b> logs de acesso, IP, user-agent, quotas consumidas.</li>
  <li><b>Leads prospectados:</b> dados públicos de empresas retornados pelo
      Google Places / scraping; retidos por até 90 dias em nossa base pra evitar
      duplicidade nas rodadas de prospecção do próprio usuário.</li>
</ul>

<h2>2. Base legal (LGPD art. 7º)</h2>
<ul>
  <li>Execução de contrato (I) — pra fornecer o serviço.</li>
  <li>Legítimo interesse (IX) — pra prospecção B2B sobre dados públicos.</li>
  <li>Cumprimento de obrigação legal (II) — pra emissão fiscal.</li>
</ul>

<h2>3. Compartilhamento</h2>
<p>Compartilhamos dados exclusivamente com: Asaas (processador de pagamento),
GoHighLevel/Linkia (CRM do próprio usuário), Google Places (busca de leads),
Resend/SMTP (envio de emails transacionais).</p>

<h2>4. Retenção</h2>
<p>Cadastro do usuário: enquanto a conta estiver ativa. Após deleção, os dados
pessoais do titular são anonimizados em até 30 dias. Registros fiscais são
mantidos pelos prazos legais (5 anos).</p>

<h2>5. Direitos do titular</h2>
<p>Confirmação, acesso, correção, portabilidade, anonimização e eliminação —
via endpoint DELETE /api/lgpd/deletar-conta na área do usuário ou solicitação a
{dpo}.</p>

<h2>6. Segurança</h2>
<p>Comunicação via HTTPS, senhas com hash bcrypt/werkzeug, backups diários.
Incidentes serão comunicados à ANPD e aos titulares afetados em até 72h.</p>

<h2>7. DPO</h2>
<p>Encarregado de Proteção de Dados: {dpo}</p>
""".replace("{ver}", LGPD_PRIVACIDADE_VERSAO).replace("{dpo}", LGPD_DPO_EMAIL)


@bp.route("/termos", methods=["GET"])
def termos():
    return _TERMOS_HTML, 200, {"Content-Type": "text/html; charset=utf-8"}


@bp.route("/privacidade", methods=["GET"])
def privacidade():
    return _PRIVACIDADE_HTML, 200, {"Content-Type": "text/html; charset=utf-8"}


@bp.route("/api/lgpd/deletar-conta", methods=["DELETE", "POST"])
@login_required
def lgpd_deletar():
    """Solicita deleção da conta. Cancela Asaas imediatamente, agenda anonimização em 30d."""
    conta_id = int(current_user.conta_id or 0)
    user_id = int(current_user.id)
    ass = get_assinatura_by_conta(conta_id)
    if ass and ass.get("asaas_subscription_id") and ass.get("status") not in ("cancelada",):
        asaas_cancelar_subscription(ass["asaas_subscription_id"])
        conn = _get_conn()
        conn.execute("UPDATE assinaturas SET status='cancelada', atualizado_em=? WHERE id=?",
                     (datetime.now().isoformat(), ass["id"]))
        conn.commit()
        conn.close()

    executar_em = (datetime.now() + timedelta(days=30)).isoformat()
    conn = _get_conn()
    conn.execute("""
        INSERT INTO lgpd_delecoes (user_id, solicitado_em, executar_em, status)
        VALUES (?, ?, ?, 'pendente')
    """, (user_id, datetime.now().isoformat(), executar_em))
    conn.execute("UPDATE users SET ativo=0 WHERE id=?", (user_id,))
    conn.commit()
    conn.close()

    return jsonify({
        "ok": True,
        "mensagem": "conta desativada. Anonimização automática em 30 dias.",
        "executar_em": executar_em,
    })


# ═════════════════════════════════════════════════════════════════════════
# ENFORCEMENT DE COTA (middleware) — Item 4
# ═════════════════════════════════════════════════════════════════════════

# Rotas monitoradas de prospecção/enriquecimento (definidas por prefixo).
# Cada rota que "gasta" leads deve ser interceptada.
# Lista mapeada em 2026-08-13 a partir do app.py atual (grep de rotas).
QUOTA_ROTAS_PREFIXOS = (
    # UI/form
    "/prospectar",
    "/enriquecer",
    "/planilha/enriquecer",
    "/analisar-lead",
    "/automacao/rodar-agora",
    # API pública (agente/externa)
    "/api/v1/prospectar",
    "/api/v1/analisar",
    # Rotas antigas do brief (pra compat futura, não existem hoje):
    "/api/prospectar",
    "/api/enriquecer",
    "/api/prospeccao/rodar",
    "/api/analisar-lead",
    "/api/enviar-crm",
)


def _quota_deve_auditar(path: str) -> bool:
    for p in QUOTA_ROTAS_PREFIXOS:
        if path.startswith(p):
            return True
    return False


def registrar_middleware_quota(app):
    """Registra hook before_request que valida assinatura/quota nas rotas críticas."""

    @app.before_request
    def _guard_assinatura_quota():
        # Só POST audita (GET/status não gasta cota).
        if request.method != "POST":
            return None
        path = request.path or ""
        if not _quota_deve_auditar(path):
            return None
        if not current_user.is_authenticated:
            return None  # login_manager cuida
        conta_id = int(getattr(current_user, "conta_id", 0) or 0)
        if not conta_id:
            return None
        # 1. assinatura ativa/trial válido?
        permite, motivo = assinatura_permite_uso(conta_id)
        if not permite:
            return jsonify({
                "ok": False,
                "erro": "assinatura suspensa, atualize seu cartão",
                "motivo": motivo,
                "upgrade_url": "/planos",
            }), 402
        # 2. quota disponível?
        quota = get_quota(conta_id)
        if quota["disponivel"] <= 0:
            return jsonify({
                "ok": False,
                "erro": "limite de leads do plano atingido",
                "quota": quota,
                "upgrade_url": "/planos",
            }), 402
        return None


# ═════════════════════════════════════════════════════════════════════════
# HELPERS INTERNOS
# ═════════════════════════════════════════════════════════════════════════

def _tem_endpoint(name: str) -> bool:
    try:
        return name in current_app.view_functions
    except Exception:
        return False


def _pagina_simples(titulo: str, mensagem_html: str) -> str:
    return f"""<!doctype html><meta charset="utf-8"><title>{titulo}</title>
<style>body{{font-family:system-ui;max-width:560px;margin:80px auto;padding:0 24px;text-align:center;color:#222}}
h1{{color:#0a2540}}p{{line-height:1.6}}a{{color:#0a2540}}</style>
<h1>{titulo}</h1><p>{mensagem_html}</p>"""


# ═════════════════════════════════════════════════════════════════════════
# BOOTSTRAP
# ═════════════════════════════════════════════════════════════════════════

def register_climbleads_routes(app) -> None:
    """Chame do app.py após criar Flask app e LoginManager."""
    ensure_schema()
    app.register_blueprint(bp)
    registrar_middleware_quota(app)
    print("[climbleads] rotas Sprint 1 Fase 2 registradas: /signup, /verify, /webhooks/asaas, /termos, /privacidade, /api/asaas/*, /api/minha-conta, /api/lgpd/deletar-conta")
