#!/usr/bin/env python3
"""
analisador.py - Analisador automático de leads (prospeccao ativa)

Fluxo:
  1) Coleta dados públicos (Google Places, Instagram, site atual)
  2) Gera copy persuasiva com OpenAI GPT-4o
  3) Baixa imagens (Google Photos ou Unsplash fallback)
  4) Monta site HTML + serve via porta local + registra rota Traefik + DNS Cloudflare
  5) Gera dossiê PDF via WeasyPrint (com screenshot Playwright)
"""

import os
import re
import io
import json
import time
import base64
import shutil
import socket
import sqlite3
import unicodedata
import subprocess
from pathlib import Path
from datetime import datetime
from urllib.parse import quote_plus

import requests
import anthropic

# ── Paths ──────────────────────────────────────────────────
BASE_ANALISES = Path("/opt/mia/workspace/analises")
BASE_ANALISES.mkdir(exist_ok=True)
DB_PATH = Path("/opt/mia/workspace/prospeccao_ativa/users.db")

TRAEFIK_CONFIG = Path("/etc/easypanel/traefik/config/custom.yaml")
DOMINIO_BASE = "agentesclimb.us"

# ── Env ────────────────────────────────────────────────────
def _load_env_file():
    """Carrega /opt/mia/.env em os.environ se ainda não estiver setado."""
    env_path = Path("/opt/mia/.env")
    if not env_path.exists():
        return
    for line in env_path.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        k = k.strip()
        v = v.strip().strip('"').strip("'")
        if k and k not in os.environ:
            os.environ[k] = v

_load_env_file()

GOOGLE_PLACES_KEY = os.environ.get("GOOGLE_PLACES_KEY") or "AIzaSyCnOTQxLdFua3OLBnNbb0Husoo9dH80Fbw"
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
ANTHROPIC_OAUTH_TOKEN = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN", "")
CLOUDFLARE_DNS_TOKEN = os.environ.get("CLOUDFLARE_DNS_TOKEN", "")
CLOUDFLARE_ZONE_ID = os.environ.get("CLOUDFLARE_ZONE_ID", "")
UNSPLASH_ACCESS_KEY = os.environ.get("UNSPLASH_ACCESS_KEY", "")  # opcional

def _get_anthropic_client():
    """Retorna cliente Anthropic usando API key ou OAuth token (Bearer)."""
    import anthropic as _anthropic
    if ANTHROPIC_API_KEY:
        return _anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
    if ANTHROPIC_OAUTH_TOKEN:
        return _anthropic.Anthropic(auth_token=ANTHROPIC_OAUTH_TOKEN)
    return None

# ── Skills paths (para injetar guidelines nos prompts do Leo) ──
SKILL_FRONTEND_DESIGN = Path("/home/mia/.claude/skills/frontend-design/SKILL.md")
SKILL_UI_UX_PRO_MAX = Path("/home/mia/.claude/skills/ui-ux-pro-max/SKILL.md")
SKILL_LANDING_FUNIL = Path("/home/mia/.claude/skills/landing-funil/SKILL.md")
SKILL_LANDING_FUNIL_BASE_CSS = Path("/home/mia/.claude/skills/landing-funil/reference/base.css")

# Palavras-chave que indicam negócio digital (vs. local)
PALAVRAS_DIGITAL = {
    "curso", "mentoria", "infoproduto", "digital", "online",
    "afiliado", "ebook", "treinamento", "coaching",
}


def _read_skill(path: Path, limit: int | None = None) -> str:
    """Lê um arquivo de skill do disco. Retorna string vazia se falhar."""
    try:
        content = path.read_text(encoding="utf-8")
        if limit is not None:
            return content[:limit]
        return content
    except Exception as e:
        print(f"[analisador] falha lendo skill {path}: {e}")
        return ""


def detectar_segmento(dados: dict) -> str:
    """Classifica o lead como 'digital' (infoproduto/curso/etc) ou 'local'."""
    cat = (dados.get("categoria") or "").lower()
    types = " ".join(dados.get("types") or []).lower()
    combined = cat + " " + types
    if any(p in combined for p in PALAVRAS_DIGITAL):
        return "digital"
    return "local"


# ── DB helpers ─────────────────────────────────────────────
def _get_conn():
    conn = sqlite3.connect(str(DB_PATH))
    conn.row_factory = sqlite3.Row
    return conn


def ensure_table():
    """Cria tabela lead_analises se não existir."""
    conn = _get_conn()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS lead_analises (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            lead_nome TEXT NOT NULL,
            lead_telefone TEXT,
            lead_cidade TEXT,
            lead_categoria TEXT,
            lead_instagram TEXT,
            lead_site_atual TEXT,
            status TEXT DEFAULT 'aguardando',
            site_url TEXT,
            pdf_path TEXT,
            analise_json TEXT,
            erro TEXT,
            criado_em TEXT,
            concluido_em TEXT,
            user_id INTEGER
        )
    """)
    conn.commit()
    conn.close()


def criar_analise(lead_data: dict, user_id: int) -> int:
    ensure_table()
    conn = _get_conn()
    cur = conn.execute("""
        INSERT INTO lead_analises
            (lead_nome, lead_telefone, lead_cidade, lead_categoria,
             lead_instagram, lead_site_atual, status, criado_em, user_id)
        VALUES (?, ?, ?, ?, ?, ?, 'aguardando', ?, ?)
    """, (
        lead_data.get("nome") or "",
        lead_data.get("telefone") or "",
        lead_data.get("cidade") or "",
        lead_data.get("categoria") or "",
        lead_data.get("instagram") or "",
        lead_data.get("site_atual") or "",
        datetime.now().isoformat(),
        user_id,
    ))
    conn.commit()
    aid = cur.lastrowid
    conn.close()
    return aid


def update_analise(analise_id: int, **kwargs):
    if not kwargs:
        return
    campos = ["status", "site_url", "pdf_path", "analise_json", "erro", "concluido_em"]
    updates = {k: v for k, v in kwargs.items() if k in campos}
    if not updates:
        return
    sets = ", ".join(f"{k} = ?" for k in updates)
    vals = list(updates.values()) + [analise_id]
    conn = _get_conn()
    conn.execute(f"UPDATE lead_analises SET {sets} WHERE id = ?", vals)
    conn.commit()
    conn.close()


def get_analise(analise_id: int) -> dict | None:
    conn = _get_conn()
    row = conn.execute("SELECT * FROM lead_analises WHERE id = ?", (analise_id,)).fetchone()
    conn.close()
    return dict(row) if row else None


def listar_analises(user_id: int) -> list:
    ensure_table()
    conn = _get_conn()
    rows = conn.execute(
        "SELECT * FROM lead_analises WHERE user_id = ? ORDER BY id DESC LIMIT 100",
        (user_id,)
    ).fetchall()
    conn.close()
    return [dict(r) for r in rows]


# ── Utilidades ─────────────────────────────────────────────
def slugify(text: str) -> str:
    if not text:
        return "lead"
    nfkd = unicodedata.normalize("NFKD", text)
    only_ascii = nfkd.encode("ASCII", "ignore").decode("ASCII")
    only_ascii = only_ascii.lower()
    only_ascii = re.sub(r"[^a-z0-9]+", "-", only_ascii)
    only_ascii = only_ascii.strip("-")
    return only_ascii[:40] or "lead"


def porta_livre(inicio=9100, fim=9500) -> int:
    for p in range(inicio, fim + 1):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(("127.0.0.1", p))
                return p
            except OSError:
                continue
    raise RuntimeError("Sem porta livre no range")


# ── Google Places ──────────────────────────────────────────
def _google_find_place(nome: str, cidade: str) -> str | None:
    """Retorna place_id do estabelecimento."""
    try:
        r = requests.get(
            "https://maps.googleapis.com/maps/api/place/findplacefromtext/json",
            params={
                "input": f"{nome} {cidade}".strip(),
                "inputtype": "textquery",
                "fields": "place_id",
                "key": GOOGLE_PLACES_KEY,
                "language": "pt-BR",
            }, timeout=15
        )
        data = r.json()
        cands = data.get("candidates") or []
        if cands:
            return cands[0].get("place_id")
    except Exception:
        pass
    return None


def _google_place_details(place_id: str) -> dict:
    """Detalhes completos do lugar."""
    try:
        r = requests.get(
            "https://maps.googleapis.com/maps/api/place/details/json",
            params={
                "place_id": place_id,
                "fields": ("name,formatted_address,formatted_phone_number,website,"
                           "opening_hours,rating,user_ratings_total,reviews,photos,"
                           "types,url,international_phone_number"),
                "key": GOOGLE_PLACES_KEY,
                "language": "pt-BR",
            }, timeout=15
        )
        return r.json().get("result") or {}
    except Exception:
        return {}


def _google_photo_download(photo_reference: str, dest: Path, maxwidth: int = 1200) -> bool:
    """Baixa foto do Places (segue redirect)."""
    try:
        url = (
            "https://maps.googleapis.com/maps/api/place/photo"
            f"?maxwidth={maxwidth}&photoreference={photo_reference}&key={GOOGLE_PLACES_KEY}"
        )
        r = requests.get(url, timeout=30, allow_redirects=True)
        if r.status_code == 200 and r.content:
            dest.write_bytes(r.content)
            return True
    except Exception:
        pass
    return False


def coletar_google_places(nome: str, cidade: str) -> dict:
    """Retorna dict com dados do Google Places."""
    pid = _google_find_place(nome, cidade)
    if not pid:
        return {}
    det = _google_place_details(pid)
    if not det:
        return {}
    fotos_refs = [p.get("photo_reference") for p in (det.get("photos") or []) if p.get("photo_reference")]
    return {
        "nome_google": det.get("name") or "",
        "endereco": det.get("formatted_address") or "",
        "telefone": det.get("formatted_phone_number") or det.get("international_phone_number") or "",
        "site_google": det.get("website") or "",
        "rating": det.get("rating"),
        "total_reviews": det.get("user_ratings_total"),
        "reviews": det.get("reviews") or [],
        "horarios": (det.get("opening_hours") or {}).get("weekday_text") or [],
        "types": det.get("types") or [],
        "google_maps_url": det.get("url") or "",
        "fotos_refs": fotos_refs[:5],
        "place_id": pid,
    }


# ── Site atual / Instagram ─────────────────────────────────
def coletar_site_atual(url: str) -> dict:
    if not url:
        return {}
    if not url.startswith(("http://", "https://")):
        url = "https://" + url
    try:
        r = requests.get(url, timeout=12, headers={"User-Agent": "Mozilla/5.0"})
        html = r.text[:200000]
        title = ""
        m = re.search(r"<title>(.*?)</title>", html, re.I | re.S)
        if m:
            title = m.group(1).strip()[:200]
        desc = ""
        m = re.search(r'<meta\s+name=["\']description["\']\s+content=["\'](.+?)["\']', html, re.I)
        if m:
            desc = m.group(1).strip()[:300]
        return {"site_titulo": title, "site_descricao": desc, "site_status": r.status_code}
    except Exception as e:
        return {"site_erro": str(e)[:120]}


def buscar_instagram(nome: str, cidade: str) -> str:
    """Heurística: tenta encontrar @handle do negócio via busca DuckDuckGo."""
    try:
        q = quote_plus(f"{nome} {cidade} instagram")
        r = requests.get(
            f"https://duckduckgo.com/html/?q={q}",
            timeout=10,
            headers={"User-Agent": "Mozilla/5.0"}
        )
        text = r.text
        m = re.search(r'instagram\.com/([a-zA-Z0-9._]{3,30})', text)
        if m:
            handle = m.group(1)
            if handle.lower() not in ("p", "reel", "reels", "explore", "accounts"):
                return handle
    except Exception:
        pass
    return ""


# ── OpenAI copy ────────────────────────────────────────────
def gerar_copy_openai(dados: dict) -> dict:
    """Gera copy persuasiva usando GPT-4o. Retorna dict estruturado."""
    if not OPENAI_API_KEY:
        return _copy_fallback(dados)

    try:
        from openai import OpenAI
        client = OpenAI(api_key=OPENAI_API_KEY)

        reviews_text = ""
        if dados.get("reviews"):
            reviews_text = "\n".join(
                f"- {r.get('rating', '?')}/5: {r.get('text', '')[:200]}"
                for r in dados["reviews"][:5]
            )

        prompt = f"""Você é um copywriter sênior especialista em pequenos negócios locais brasileiros.

Gere copy persuasiva para uma landing page de demonstração deste negócio:

- Nome: {dados.get('nome_google') or dados.get('nome')}
- Cidade: {dados.get('cidade')}
- Categoria: {dados.get('categoria')}
- Endereço: {dados.get('endereco')}
- Telefone: {dados.get('telefone')}
- Rating Google: {dados.get('rating')} ({dados.get('total_reviews')} avaliações)
- Horários: {' | '.join(dados.get('horarios') or [])[:400]}

Avaliações reais recentes:
{reviews_text or '(sem avaliações)'}

Site atual (se existe): {dados.get('site_titulo') or ''} - {dados.get('site_descricao') or ''}

Devolva EXATAMENTE este JSON (sem texto fora, sem markdown):
{{
  "headline": "chamada principal impactante, até 12 palavras",
  "subheadline": "subheadline com promessa/benefício, até 25 palavras",
  "sobre": "parágrafo de 2-3 linhas apresentando o negócio, tom acolhedor e profissional",
  "servicos": [
    {{"titulo": "Serviço 1", "descricao": "descrição curta"}},
    {{"titulo": "Serviço 2", "descricao": "descrição curta"}},
    {{"titulo": "Serviço 3", "descricao": "descrição curta"}}
  ],
  "por_que_escolher": [
    "Motivo 1 em uma linha",
    "Motivo 2 em uma linha",
    "Motivo 3 em uma linha",
    "Motivo 4 em uma linha"
  ],
  "depoimentos": [
    {{"nome": "Nome plausível brasileiro", "texto": "depoimento verossímil baseado nas avaliações reais", "rating": 5}},
    {{"nome": "Outro nome", "texto": "outro depoimento", "rating": 5}}
  ],
  "cta_principal": "texto do botão principal (2-4 palavras)",
  "cta_secundario": "texto do botão secundário",
  "cor_primaria": "hex hex adequado ao segmento, ex #2E7D32",
  "cor_secundaria": "hex secundário complementar",
  "analise_oportunidades": [
    "Oportunidade 1 identificada olhando o negócio",
    "Oportunidade 2",
    "Oportunidade 3"
  ]
}}
"""
        resp = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": "Você devolve APENAS JSON válido, sem markdown, sem cerca de código."},
                {"role": "user", "content": prompt}
            ],
            temperature=0.7,
            response_format={"type": "json_object"},
            max_tokens=1500,
        )
        content = resp.choices[0].message.content.strip()
        return json.loads(content)
    except Exception as e:
        print(f"[analisador] erro openai: {e}")
        return _copy_fallback(dados)


def _copy_fallback(dados: dict) -> dict:
    nome = dados.get("nome_google") or dados.get("nome") or "Seu Negócio"
    cat = dados.get("categoria") or "atendimento"
    return {
        "headline": f"{nome}",
        "subheadline": f"Referência em {cat} em {dados.get('cidade', '')}",
        "sobre": f"Conheça {nome}. Atendimento de qualidade, profissionais experientes e resultados que fazem a diferença.",
        "servicos": [
            {"titulo": "Atendimento", "descricao": "Serviço profissional e dedicado."},
            {"titulo": "Qualidade", "descricao": "Padrão de excelência em cada detalhe."},
            {"titulo": "Confiança", "descricao": "Anos de mercado e clientes satisfeitos."},
        ],
        "por_que_escolher": [
            "Experiência comprovada",
            "Atendimento personalizado",
            "Localização acessível",
            "Preços justos",
        ],
        "depoimentos": [
            {"nome": "Ana Silva", "texto": "Atendimento nota mil. Recomendo!", "rating": 5},
            {"nome": "Carlos Souza", "texto": "Profissionais atenciosos e resultado excelente.", "rating": 5},
        ],
        "cta_principal": "Agendar agora",
        "cta_secundario": "Fale conosco",
        "cor_primaria": "#2E7D32",
        "cor_secundaria": "#1B5E20",
        "analise_oportunidades": [
            "Otimizar presença digital para atrair mais clientes locais",
            "Implantar sistema de agendamento online",
            "Estruturar campanhas de tráfego pago para escalar leads",
        ],
    }


# ── Copy Jonathan via OpenAI (fallback quando não há Anthropic) ────
def _gerar_copy_jonathan_openai(dados: dict) -> dict:
    """Gera copy com persona Jonathan usando OpenAI GPT-4o."""
    if not OPENAI_API_KEY:
        return _copy_fallback(dados)

    try:
        from openai import OpenAI
        client = OpenAI(api_key=OPENAI_API_KEY)

        reviews_text = ""
        if dados.get("reviews"):
            reviews_text = "\n".join(
                f"- {r.get('rating', '?')}/5: {r.get('text', '')[:200]}"
                for r in dados["reviews"][:5]
            )

        system_prompt = (
            "Você é Jonathan, copywriter sênior especialista em marketing digital "
            "para pequenos negócios brasileiros.\n"
            "Você escreve copy persuasiva, humana, específica para o segmento — nunca genérica ou robótica.\n"
            "Você usa gatilhos emocionais, linguagem próxima, e adapta o tom ao tipo de negócio.\n"
            "Você devolve APENAS JSON válido, sem markdown, sem texto extra."
        )

        user_prompt = f"""Gere copy persuasiva para uma landing page de demonstração deste negócio:

- Nome: {dados.get('nome_google') or dados.get('nome')}
- Cidade: {dados.get('cidade')}
- Categoria: {dados.get('categoria')}
- Endereço: {dados.get('endereco')}
- Telefone: {dados.get('telefone')}
- Rating Google: {dados.get('rating')} ({dados.get('total_reviews')} avaliações)
- Horários: {' | '.join(dados.get('horarios') or [])[:400]}

Avaliações reais recentes:
{reviews_text or '(sem avaliações)'}

Site atual (se existe): {dados.get('site_titulo') or ''} - {dados.get('site_descricao') or ''}

Devolva EXATAMENTE este JSON (sem texto fora, sem markdown):
{{
  "headline": "chamada principal impactante, até 12 palavras",
  "subheadline": "subheadline com promessa/benefício, até 25 palavras",
  "sobre": "parágrafo de 2-3 linhas apresentando o negócio, tom acolhedor e profissional",
  "servicos": [
    {{"titulo": "Serviço 1", "descricao": "descrição curta"}},
    {{"titulo": "Serviço 2", "descricao": "descrição curta"}},
    {{"titulo": "Serviço 3", "descricao": "descrição curta"}}
  ],
  "por_que_escolher": [
    "Motivo 1 em uma linha",
    "Motivo 2 em uma linha",
    "Motivo 3 em uma linha",
    "Motivo 4 em uma linha"
  ],
  "depoimentos": [
    {{"nome": "Nome plausível brasileiro", "texto": "depoimento verossímil baseado nas avaliações reais", "rating": 5}},
    {{"nome": "Outro nome", "texto": "outro depoimento", "rating": 5}}
  ],
  "cta_principal": "texto do botão principal (2-4 palavras)",
  "cta_secundario": "texto do botão secundário",
  "cor_primaria": "hex adequado ao segmento (ex: clínica estética tons rosé/nude/dourado; restaurante tons quentes; academia tons vibrantes)",
  "cor_secundaria": "hex secundário complementar",
  "analise_oportunidades": [
    "Oportunidade 1 identificada olhando o negócio",
    "Oportunidade 2",
    "Oportunidade 3"
  ]
}}
"""
        resp = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}
            ],
            temperature=0.7,
            response_format={"type": "json_object"},
            max_tokens=1500,
        )
        content = resp.choices[0].message.content.strip()
        return json.loads(content)
    except Exception as e:
        print(f"[analisador] erro Jonathan (openai fallback): {e}")
        return _copy_fallback(dados)


# ── Copy Jonathan (Anthropic - Haiku) ──────────────────────
def gerar_copy_jonathan(dados: dict) -> dict:
    """Gera copy persuasiva via persona do Jonathan (Anthropic Haiku).
    Se não houver chave Anthropic, usa OpenAI com a mesma persona.
    Fallback final: _copy_fallback."""
    client = _get_anthropic_client()
    if not client:
        return _gerar_copy_jonathan_openai(dados)

    try:

        reviews_text = ""
        if dados.get("reviews"):
            reviews_text = "\n".join(
                f"- {r.get('rating', '?')}/5: {r.get('text', '')[:200]}"
                for r in dados["reviews"][:5]
            )

        system_prompt = (
            "Você é Jonathan, copywriter sênior especialista em marketing digital "
            "para pequenos negócios brasileiros.\n"
            "Você escreve copy persuasiva, humana e que converte — nunca genérica ou robótica.\n"
            "Você devolve APENAS JSON válido, sem markdown, sem texto extra."
        )

        user_prompt = f"""Gere copy persuasiva para uma landing page de demonstração deste negócio:

- Nome: {dados.get('nome_google') or dados.get('nome')}
- Cidade: {dados.get('cidade')}
- Categoria: {dados.get('categoria')}
- Endereço: {dados.get('endereco')}
- Telefone: {dados.get('telefone')}
- Rating Google: {dados.get('rating')} ({dados.get('total_reviews')} avaliações)
- Horários: {' | '.join(dados.get('horarios') or [])[:400]}

Avaliações reais recentes:
{reviews_text or '(sem avaliações)'}

Site atual (se existe): {dados.get('site_titulo') or ''} - {dados.get('site_descricao') or ''}

Devolva EXATAMENTE este JSON (sem texto fora, sem markdown, sem cerca de código):
{{
  "headline": "chamada principal impactante, até 12 palavras",
  "subheadline": "subheadline com promessa/benefício, até 25 palavras",
  "sobre": "parágrafo de 2-3 linhas apresentando o negócio, tom acolhedor e profissional",
  "servicos": [
    {{"titulo": "Serviço 1", "descricao": "descrição curta"}},
    {{"titulo": "Serviço 2", "descricao": "descrição curta"}},
    {{"titulo": "Serviço 3", "descricao": "descrição curta"}}
  ],
  "por_que_escolher": [
    "Motivo 1 em uma linha",
    "Motivo 2 em uma linha",
    "Motivo 3 em uma linha",
    "Motivo 4 em uma linha"
  ],
  "depoimentos": [
    {{"nome": "Nome plausível brasileiro", "texto": "depoimento verossímil baseado nas avaliações reais", "rating": 5}},
    {{"nome": "Outro nome", "texto": "outro depoimento", "rating": 5}}
  ],
  "cta_principal": "texto do botão principal (2-4 palavras)",
  "cta_secundario": "texto do botão secundário",
  "cor_primaria": "hex adequado ao segmento, ex #2E7D32",
  "cor_secundaria": "hex secundário complementar",
  "analise_oportunidades": [
    "Oportunidade 1 identificada olhando o negócio",
    "Oportunidade 2",
    "Oportunidade 3"
  ]
}}
"""

        resp = client.messages.create(
            model="claude-haiku-4-5-20251001",
            max_tokens=2000,
            system=system_prompt,
            messages=[{"role": "user", "content": user_prompt}],
        )

        # Extrai texto (blocos podem ser TextBlock)
        raw = ""
        for block in resp.content:
            if hasattr(block, "text"):
                raw += block.text
        raw = raw.strip()

        # Remove eventual cerca de código
        if raw.startswith("```"):
            raw = re.sub(r"^```(?:json)?\s*", "", raw)
            raw = re.sub(r"\s*```$", "", raw)

        return json.loads(raw)
    except Exception as e:
        print(f"[analisador] erro Jonathan (anthropic): {e}")
        return _copy_fallback(dados)


# ── HTML Leo via OpenAI (fallback quando não há Anthropic) ─
def _gerar_html_leo_openai(copy: dict, dados: dict, imagens: list[str], segmento: str) -> str:
    """Gera HTML com persona Leo usando OpenAI GPT-4o + skills injetadas."""
    if not OPENAI_API_KEY:
        return gerar_html_site(copy, dados, imagens)

    try:
        from openai import OpenAI
        client = OpenAI(api_key=OPENAI_API_KEY)

        nome = dados.get("nome_google") or dados.get("nome") or "Negócio"
        cidade = dados.get("cidade") or ""
        categoria = dados.get("categoria") or ""
        endereco = dados.get("endereco") or ""
        telefone = dados.get("telefone") or dados.get("lead_telefone") or ""
        rating = dados.get("rating") or "—"
        total_reviews = dados.get("total_reviews") or 0
        horarios = dados.get("horarios") or []

        telefone_numeros = re.sub(r"\D", "", telefone or "")
        if telefone_numeros and not telefone_numeros.startswith("55"):
            telefone_numeros = "55" + telefone_numeros

        system_prompt = (
            "Você é Leo, desenvolvedor-designer sênior especialista em landing pages "
            "de alta conversão para negócios brasileiros.\n"
            "Você gera HTML completo, auto-contido, com CSS interno (sem link pra CSS externo exceto Google Fonts).\n"
            "O HTML deve ser profissional, visualmente impressionante, mobile-first, com favicon SVG inline.\n"
            'Referências de imagens SEMPRE como "imgs/NOME_ARQUIVO" (caminho relativo, nunca absoluto).\n'
            "Você NUNCA usa gradiente roxo/azul genérico, NUNCA usa grid idêntico de cards com "
            "ícone+título+texto, NUNCA usa glassmorphism decorativo.\n"
            "Você devolve APENAS o HTML completo, sem explicações, sem markdown, sem cerca de código."
        )

        if segmento == "digital":
            conteudo_landing_funil = _read_skill(SKILL_LANDING_FUNIL, limit=3000)
            conteudo_base_css = _read_skill(SKILL_LANDING_FUNIL_BASE_CSS)
            skills_bloco = f"""SKILL DE DESIGN (landing-funil editorial - use como referência principal):
{conteudo_landing_funil}

BASE CSS DO DESIGN SYSTEM (incorpore inline ou use as classes/tokens):
{conteudo_base_css}"""
            estilo_hint = "estilo editorial premium (bege + coral + navy) da skill landing-funil"
            secoes = "hero VSL-style, sobre, oferta/serviços, prova social, CTA final"
        else:
            conteudo_frontend = _read_skill(SKILL_FRONTEND_DESIGN, limit=2000)
            conteudo_uiux = _read_skill(SKILL_UI_UX_PRO_MAX, limit=2000)
            skills_bloco = f"""DIRETRIZES DE DESIGN (frontend-design skill):
{conteudo_frontend}

DIRETRIZES UI/UX (ui-ux-pro-max skill):
{conteudo_uiux}"""
            estilo_hint = "design distintivo e memorável, específico para o segmento"
            secoes = "hero, sobre, serviços, por que escolher, depoimentos, contato/CTA"

        user_prompt = f"""Crie uma landing page HTML COMPLETA e AUTO-CONTIDA para o seguinte negócio:

DADOS DO NEGÓCIO:
- Nome: {nome}
- Cidade: {cidade}
- Categoria: {categoria}
- Endereço: {endereco}
- Telefone: {telefone}
- Rating Google: {rating} ({total_reviews} avaliações)
- Horários: {' | '.join(horarios)[:400]}

COPY GERADA (use exatamente estes textos):
{json.dumps(copy, ensure_ascii=False, indent=2)}

IMAGENS DISPONÍVEIS (use como src relativo "imgs/NOME"):
{json.dumps(imagens)}

{skills_bloco}

REQUISITOS OBRIGATÓRIOS:
1. HTML completo (DOCTYPE, head, body), auto-contido, com CSS interno em <style>
2. Design ÚNICO e DISTINTIVO para ESTE negócio específico — não template genérico
3. {estilo_hint}
4. Paleta de cores ADEQUADA ao segmento "{categoria}":
   - Clínica estética/beleza: tons rosé, nude, dourado (nunca verde clínico genérico)
   - Restaurante/gastronomia: tons quentes, terrosos, apetitosos
   - Academia/fitness: tons vibrantes, energia
   - Escritório/advocacia: tons sóbrios, navy, dourado, marrom
   - Escolha a paleta certa, NÃO use "#2E7D32" verde genérico como padrão
5. Google Fonts via <link rel="preconnect"> e <link href="https://fonts.googleapis.com/..."> no <head>
   (fonte escolhida por você combinando com o segmento — NUNCA Arial ou Roboto genérico)
6. Favicon SVG inline no <head>
7. Mobile-first e totalmente responsivo
8. Imagens referenciadas como "imgs/foto_1.jpg" (caminho RELATIVO, nunca absoluto, nunca URL)
9. WhatsApp link com número real: https://wa.me/{telefone_numeros}
10. Seções: {secoes}
11. Sem gradiente roxo/azul genérico. Sem grid idêntico de cards. Sem glassmorphism decorativo.

Devolva APENAS o HTML completo. Sem explicação, sem markdown, sem cerca de código."""

        resp = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}
            ],
            temperature=0.8,
            max_tokens=4096,
        )
        raw = resp.choices[0].message.content.strip()

        # Remove eventual cerca de código
        if raw.startswith("```"):
            raw = re.sub(r"^```(?:html)?\s*", "", raw)
            raw = re.sub(r"\s*```$", "", raw)

        # Sanity check: HTML precisa ter estrutura mínima e tamanho razoável
        if "<html" not in raw.lower() or "</html>" not in raw.lower() or len(raw) < 1000:
            print("[analisador] Leo (openai) retornou HTML inválido/curto, fallback pro gerador antigo.")
            return gerar_html_site(copy, dados, imagens)

        return raw
    except Exception as e:
        print(f"[analisador] erro Leo (openai fallback): {e}")
        return gerar_html_site(copy, dados, imagens)


# ── HTML Leo (Anthropic - Sonnet) ──────────────────────────
def gerar_html_leo(copy: dict, dados: dict, imagens: list[str], segmento: str) -> str:
    """Gera HTML completo via persona do Leo (Anthropic Sonnet).
    Se não houver chave Anthropic, usa OpenAI com a mesma persona + skills.
    Fallback final: gerar_html_site."""
    client = _get_anthropic_client()
    if not client:
        return _gerar_html_leo_openai(copy, dados, imagens, segmento)

    try:

        nome = dados.get("nome_google") or dados.get("nome") or "Negócio"
        cidade = dados.get("cidade") or ""
        categoria = dados.get("categoria") or ""
        endereco = dados.get("endereco") or ""
        telefone = dados.get("telefone") or dados.get("lead_telefone") or ""
        rating = dados.get("rating") or "—"
        total_reviews = dados.get("total_reviews") or 0
        horarios = dados.get("horarios") or []

        telefone_numeros = re.sub(r"\D", "", telefone or "")
        if telefone_numeros and not telefone_numeros.startswith("55"):
            telefone_numeros = "55" + telefone_numeros

        system_prompt = (
            "Você é Leo, desenvolvedor-designer sênior especialista em landing pages e sites "
            "de alta conversão para pequenos negócios brasileiros.\n"
            "Você gera HTML completo, auto-contido, com CSS inline (sem dependências externas), "
            "altamente otimizado visualmente.\n"
            "O HTML deve ser profissional, belo, mobile-first, com favicon SVG inline e "
            'referências de imagens sempre como "imgs/NOME_ARQUIVO" (caminho relativo).\n'
            "Você devolve APENAS o HTML completo, sem explicações, sem markdown, sem cerca de código."
        )

        if segmento == "digital":
            conteudo_landing_funil = _read_skill(SKILL_LANDING_FUNIL, limit=4000)
            conteudo_base_css = _read_skill(SKILL_LANDING_FUNIL_BASE_CSS)

            user_prompt = f"""Crie uma landing page HTML COMPLETA no estilo editorial premium (bege + coral + navy) para:

DADOS DO NEGÓCIO:
- Nome: {nome}
- Cidade: {cidade}
- Categoria: {categoria}
- Endereço: {endereco}
- Telefone: {telefone}
- Rating Google: {rating} ({total_reviews} avaliações)
- Horários: {' | '.join(horarios)[:400]}

COPY GERADA (use exatamente estes textos):
{json.dumps(copy, ensure_ascii=False, indent=2)}

IMAGENS DISPONÍVEIS (use como src relativo "imgs/NOME"):
{json.dumps(imagens)}

SKILL DE DESIGN (landing-funil editorial):
{conteudo_landing_funil}

BASE CSS DO DESIGN SYSTEM (incorpore inline ou use as classes):
{conteudo_base_css}

REQUISITOS:
1. HTML completo, auto-contido, CSS inline
2. Estilo editorial bege/coral/navy conforme a skill landing-funil
3. Favicon SVG inline
4. Mobile-first
5. Imagens em "imgs/foto_1.jpg" (caminho relativo)
6. Link WhatsApp real: wa.me/{telefone_numeros}
7. Seções: hero VSL-style, sobre, oferta/serviços, prova social, CTA final
"""
        else:
            conteudo_frontend_design = _read_skill(SKILL_FRONTEND_DESIGN, limit=3000)
            conteudo_uiux = _read_skill(SKILL_UI_UX_PRO_MAX, limit=3000)

            user_prompt = f"""Crie uma landing page HTML COMPLETA e AUTO-CONTIDA para o seguinte negócio local:

DADOS DO NEGÓCIO:
- Nome: {nome}
- Cidade: {cidade}
- Categoria: {categoria}
- Endereço: {endereco}
- Telefone: {telefone}
- Rating Google: {rating} ({total_reviews} avaliações)
- Horários: {' | '.join(horarios)[:400]}

COPY GERADA (use exatamente estes textos):
{json.dumps(copy, ensure_ascii=False, indent=2)}

IMAGENS DISPONÍVEIS (use como src relativo "imgs/NOME"):
{json.dumps(imagens)}

DIRETRIZES DE DESIGN (frontend-design skill):
{conteudo_frontend_design}

DIRETRIZES UI/UX (ui-ux-pro-max skill):
{conteudo_uiux}

REQUISITOS OBRIGATÓRIOS:
1. HTML completo com DOCTYPE, head, body
2. Todo CSS inline (sem link para CSS externo)
3. Favicon SVG inline no head
4. Mobile-first e responsivo
5. Imagens referenciadas como "imgs/foto_1.jpg" (caminho relativo, nunca absoluto)
6. Link do WhatsApp com telefone real (wa.me/{telefone_numeros})
7. Design DISTINTIVO e MEMORÁVEL — não genérico
8. Seções: hero, sobre, serviços, por que escolher, depoimentos, contato/CTA
9. Sem dependências externas (sem Google Fonts via @import que precise de internet — use fontes do sistema ou font-face inline)
"""

        resp = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=8000,
            system=system_prompt,
            messages=[{"role": "user", "content": user_prompt}],
        )

        raw = ""
        for block in resp.content:
            if hasattr(block, "text"):
                raw += block.text
        raw = raw.strip()

        # Remove eventual cerca de código
        if raw.startswith("```"):
            raw = re.sub(r"^```(?:html)?\s*", "", raw)
            raw = re.sub(r"\s*```$", "", raw)

        # Sanity check: se não veio um HTML válido/curto demais, cai no fallback antigo
        if "<html" not in raw.lower() or "</html>" not in raw.lower() or len(raw) < 1000:
            print("[analisador] Leo (anthropic) retornou HTML inválido/curto, fallback pro gerador antigo.")
            return gerar_html_site(copy, dados, imagens)

        return raw
    except Exception as e:
        print(f"[analisador] erro Leo (anthropic): {e}")
        return gerar_html_site(copy, dados, imagens)


# ── Imagens ────────────────────────────────────────────────
def baixar_imagens(dados_place: dict, categoria: str, dest_dir: Path) -> list[str]:
    """Baixa até 3 imagens. Retorna lista de nomes de arquivo (relativo)."""
    dest_dir.mkdir(parents=True, exist_ok=True)
    imgs = []

    # 1) Google Photos
    for i, ref in enumerate((dados_place.get("fotos_refs") or [])[:3]):
        dest = dest_dir / f"foto_{i+1}.jpg"
        if _google_photo_download(ref, dest):
            imgs.append(dest.name)

    # 2) Fallback Unsplash source (sem key)
    faltam = 3 - len(imgs)
    if faltam > 0:
        query = quote_plus(categoria or "business")
        for i in range(faltam):
            dest = dest_dir / f"unsplash_{i+1}.jpg"
            try:
                url = f"https://source.unsplash.com/1200x800/?{query}&sig={int(time.time())+i}"
                r = requests.get(url, timeout=15, allow_redirects=True)
                if r.status_code == 200 and len(r.content) > 5000:
                    dest.write_bytes(r.content)
                    imgs.append(dest.name)
            except Exception:
                pass

    return imgs


# ── HTML site demo ─────────────────────────────────────────
def gerar_html_site(copy: dict, dados: dict, imagens: list[str]) -> str:
    """Gera HTML completo do site demo."""
    cor1 = copy.get("cor_primaria") or "#2E7D32"
    cor2 = copy.get("cor_secundaria") or "#1B5E20"
    nome = dados.get("nome_google") or dados.get("nome") or "Negócio"
    endereco = dados.get("endereco") or ""
    telefone = dados.get("telefone") or dados.get("lead_telefone") or ""
    horarios = dados.get("horarios") or []

    img_hero = imagens[0] if imagens else ""
    img_2 = imagens[1] if len(imagens) > 1 else img_hero
    img_3 = imagens[2] if len(imagens) > 2 else img_hero

    servicos_html = "".join(
        f"""<div class="servico-card">
              <h3>{s.get('titulo','')}</h3>
              <p>{s.get('descricao','')}</p>
            </div>"""
        for s in (copy.get("servicos") or [])[:3]
    )
    motivos_html = "".join(
        f"<li>{m}</li>" for m in (copy.get("por_que_escolher") or [])
    )
    dep_html = "".join(
        f"""<div class="dep-card">
              <div class="stars">{'★' * int(d.get('rating', 5))}</div>
              <p>"{d.get('texto','')}"</p>
              <div class="dep-nome">— {d.get('nome','')}</div>
            </div>"""
        for d in (copy.get("depoimentos") or [])
    )
    horarios_html = "".join(f"<li>{h}</li>" for h in horarios)

    wa = re.sub(r"\D", "", telefone or "")
    if wa and not wa.startswith("55"):
        wa = "55" + wa
    wa_link = f"https://wa.me/{wa}" if wa else "#"

    return f"""<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{nome} — {dados.get('cidade','')}</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><circle cx='50' cy='50' r='50' fill='{cor1}'/><text x='50' y='68' font-size='60' text-anchor='middle' fill='white' font-family='Arial'>{nome[:1].upper()}</text></svg>" />
<style>
  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
  body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: #1a1a1a; line-height: 1.6; }}
  .container {{ max-width: 1140px; margin: 0 auto; padding: 0 20px; }}
  header {{ background: white; padding: 16px 0; position: sticky; top: 0; z-index: 10; box-shadow: 0 2px 12px rgba(0,0,0,0.05); }}
  header .container {{ display: flex; align-items: center; justify-content: space-between; }}
  .logo {{ font-weight: 800; font-size: 22px; color: {cor1}; }}
  .btn {{ display: inline-block; padding: 12px 28px; background: {cor1}; color: white; text-decoration: none; border-radius: 8px; font-weight: 600; transition: transform 0.2s; }}
  .btn:hover {{ transform: translateY(-2px); background: {cor2}; }}
  .btn-outline {{ background: transparent; color: {cor1}; border: 2px solid {cor1}; }}
  .hero {{ background: linear-gradient(135deg, {cor1}, {cor2}); color: white; padding: 80px 0; position: relative; overflow: hidden; }}
  .hero::before {{ content: ""; position: absolute; inset: 0; background: url('imgs/{img_hero}') center/cover; opacity: 0.25; }}
  .hero .container {{ position: relative; z-index: 1; text-align: center; }}
  .hero h1 {{ font-size: clamp(32px, 5vw, 56px); font-weight: 800; margin-bottom: 20px; line-height: 1.1; }}
  .hero p {{ font-size: clamp(18px, 2vw, 22px); opacity: 0.95; max-width: 720px; margin: 0 auto 32px; }}
  .hero-cta {{ display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }}
  .hero-cta .btn {{ background: white; color: {cor1}; }}
  .hero-cta .btn.btn-outline {{ background: transparent; color: white; border-color: white; }}
  section {{ padding: 80px 0; }}
  section h2 {{ font-size: clamp(28px, 4vw, 40px); font-weight: 800; margin-bottom: 16px; text-align: center; color: {cor1}; }}
  section .lead {{ font-size: 18px; color: #555; text-align: center; max-width: 720px; margin: 0 auto 48px; }}
  .sobre-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: center; }}
  .sobre-img {{ border-radius: 16px; overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,0.15); }}
  .sobre-img img {{ width: 100%; height: 420px; object-fit: cover; display: block; }}
  .servicos-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 28px; }}
  .servico-card {{ background: white; padding: 32px; border-radius: 16px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); border-top: 4px solid {cor1}; transition: transform 0.2s; }}
  .servico-card:hover {{ transform: translateY(-6px); }}
  .servico-card h3 {{ color: {cor1}; font-size: 22px; margin-bottom: 12px; }}
  .servico-card p {{ color: #555; }}
  .porque {{ background: #f8f9fa; }}
  .porque ul {{ list-style: none; display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; max-width: 900px; margin: 0 auto; }}
  .porque li {{ background: white; padding: 20px 24px; border-radius: 12px; border-left: 4px solid {cor1}; font-weight: 500; color: #333; }}
  .porque li::before {{ content: "✓"; color: {cor1}; font-weight: 800; margin-right: 12px; font-size: 18px; }}
  .depoimentos-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 28px; }}
  .dep-card {{ background: white; padding: 32px; border-radius: 16px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); }}
  .dep-card .stars {{ color: #FFA000; font-size: 20px; margin-bottom: 12px; }}
  .dep-card p {{ font-style: italic; color: #444; margin-bottom: 16px; }}
  .dep-card .dep-nome {{ font-weight: 700; color: {cor1}; }}
  .contato {{ background: linear-gradient(135deg, {cor1}, {cor2}); color: white; }}
  .contato h2 {{ color: white; }}
  .contato .lead {{ color: rgba(255,255,255,0.9); }}
  .info-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 32px; max-width: 900px; margin: 0 auto 40px; }}
  .info-box {{ background: rgba(255,255,255,0.1); padding: 24px; border-radius: 12px; backdrop-filter: blur(10px); }}
  .info-box h4 {{ margin-bottom: 12px; font-size: 16px; opacity: 0.9; text-transform: uppercase; letter-spacing: 1px; }}
  .info-box ul {{ list-style: none; }}
  .info-box li {{ padding: 4px 0; font-size: 14px; }}
  .contato-cta {{ text-align: center; }}
  .contato-cta .btn {{ background: white; color: {cor1}; font-size: 18px; padding: 16px 40px; }}
  footer {{ background: #1a1a1a; color: #aaa; padding: 32px 0; text-align: center; font-size: 14px; }}
  @media (max-width: 768px) {{
    .sobre-grid {{ grid-template-columns: 1fr; }}
    .sobre-img img {{ height: 260px; }}
    section {{ padding: 56px 0; }}
    .hero {{ padding: 56px 0; }}
  }}
</style>
</head>
<body>

<header>
  <div class="container">
    <div class="logo">{nome}</div>
    <a href="{wa_link}" class="btn">{copy.get('cta_principal', 'Agendar')}</a>
  </div>
</header>

<section class="hero">
  <div class="container">
    <h1>{copy.get('headline', nome)}</h1>
    <p>{copy.get('subheadline', '')}</p>
    <div class="hero-cta">
      <a href="{wa_link}" class="btn">{copy.get('cta_principal', 'Agendar')}</a>
      <a href="#contato" class="btn btn-outline">{copy.get('cta_secundario', 'Fale conosco')}</a>
    </div>
  </div>
</section>

<section>
  <div class="container">
    <div class="sobre-grid">
      <div class="sobre-img">
        <img src="imgs/{img_2}" alt="{nome}" />
      </div>
      <div>
        <h2 style="text-align:left;">Sobre nós</h2>
        <p style="font-size:18px;color:#555;">{copy.get('sobre','')}</p>
        <a href="{wa_link}" class="btn" style="margin-top:24px;">{copy.get('cta_principal','Agendar')}</a>
      </div>
    </div>
  </div>
</section>

<section style="background:#f8f9fa;">
  <div class="container">
    <h2>Nossos serviços</h2>
    <p class="lead">O que oferecemos para você</p>
    <div class="servicos-grid">
      {servicos_html}
    </div>
  </div>
</section>

<section class="porque">
  <div class="container">
    <h2>Por que nos escolher</h2>
    <p class="lead">Razões que fazem a diferença</p>
    <ul>{motivos_html}</ul>
  </div>
</section>

<section>
  <div class="container">
    <h2>O que dizem nossos clientes</h2>
    <p class="lead">Depoimentos reais de quem confiou no nosso trabalho</p>
    <div class="depoimentos-grid">{dep_html}</div>
  </div>
</section>

<section class="contato" id="contato">
  <div class="container">
    <h2>Fale com a gente</h2>
    <p class="lead">Estamos prontos para atender você</p>
    <div class="info-grid">
      <div class="info-box">
        <h4>Endereço</h4>
        <p>{endereco or '—'}</p>
      </div>
      <div class="info-box">
        <h4>Telefone</h4>
        <p>{telefone or '—'}</p>
      </div>
      <div class="info-box">
        <h4>Horários</h4>
        <ul>{horarios_html or '<li>—</li>'}</ul>
      </div>
    </div>
    <div class="contato-cta">
      <a href="{wa_link}" class="btn">{copy.get('cta_principal', 'Agendar agora')}</a>
    </div>
  </div>
</section>

<footer>
  <div class="container">
    <p>© {datetime.now().year} {nome}. Todos os direitos reservados.</p>
    <p style="margin-top:8px;font-size:12px;opacity:0.6;">Página demonstrativa — Climb Digital</p>
  </div>
</footer>

</body>
</html>
"""


# ── Servidor HTTP local (systemd unit) ─────────────────────
def montar_servidor_site(analise_id: int, site_dir: Path, porta: int, slug: str) -> None:
    """Cria um systemd unit rodando http.server no dir do site."""
    unit_name = f"analise-{slug}-{analise_id}.service"
    unit_path = Path(f"/etc/systemd/system/{unit_name}")
    content = f"""[Unit]
Description=Analise Lead - {slug} #{analise_id}
After=network.target

[Service]
Type=simple
User=mia
WorkingDirectory={site_dir}
ExecStart=/usr/bin/python3 -m http.server {porta} --bind 0.0.0.0
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
"""
    tmp = Path(f"/tmp/{unit_name}")
    tmp.write_text(content)
    subprocess.run(["sudo", "mv", str(tmp), str(unit_path)], check=True)
    subprocess.run(["sudo", "systemctl", "daemon-reload"], check=True)
    subprocess.run(["sudo", "systemctl", "enable", "--now", unit_name], check=True)


# ── Traefik + Cloudflare ───────────────────────────────────
def registrar_traefik(slug: str, porta: int) -> str:
    """Adiciona rota Traefik. Retorna hostname."""
    import yaml
    host = f"{slug}.{DOMINIO_BASE}"
    service_name = f"analise-{slug}"

    with open(TRAEFIK_CONFIG) as f:
        cfg = yaml.safe_load(f)

    routers = cfg["http"]["routers"]
    services = cfg["http"]["services"]

    # Se já existe, atualiza a porta e retorna
    if service_name in services:
        services[service_name]["loadBalancer"]["servers"] = [{"url": f"http://172.18.0.1:{porta}"}]
    else:
        routers[f"{service_name}-http"] = {
            "entryPoints": ["http"],
            "middlewares": ["redirect-to-https"],
            "rule": f"Host(`{host}`)",
            "service": service_name,
        }
        routers[f"{service_name}-https"] = {
            "entryPoints": ["https"],
            "rule": f"Host(`{host}`)",
            "service": service_name,
            "tls": {"certResolver": "letsencrypt"},
        }
        services[service_name] = {
            "loadBalancer": {"servers": [{"url": f"http://172.18.0.1:{porta}"}]}
        }

    # Backup + escrita atômica (via sudo cat pipe)
    tmp = Path(f"/tmp/traefik-{int(time.time())}.yaml")
    with open(tmp, "w") as f:
        yaml.safe_dump(cfg, f, sort_keys=False, allow_unicode=True)
    subprocess.run(["sudo", "cp", str(TRAEFIK_CONFIG), f"{TRAEFIK_CONFIG}.bak.{int(time.time())}"], check=False)
    subprocess.run(["sudo", "cp", str(tmp), str(TRAEFIK_CONFIG)], check=True)
    tmp.unlink(missing_ok=True)
    return host


def registrar_cloudflare(slug: str) -> bool:
    """Cria registro CNAME para slug.agentesclimb.us apontando pro root."""
    if not CLOUDFLARE_DNS_TOKEN or not CLOUDFLARE_ZONE_ID:
        return False

    subdomain = slug
    fqdn = f"{subdomain}.{DOMINIO_BASE}"

    # Checa se já existe
    try:
        r = requests.get(
            f"https://api.cloudflare.com/client/v4/zones/{CLOUDFLARE_ZONE_ID}/dns_records",
            headers={"Authorization": f"Bearer {CLOUDFLARE_DNS_TOKEN}"},
            params={"name": fqdn},
            timeout=15
        )
        data = r.json()
        if data.get("result"):
            return True
    except Exception:
        pass

    payload = {
        "type": "A",
        "name": subdomain,
        "content": "168.231.106.207",   # IP público da VPS (mesmo pattern do agentesclimb.us)
        "ttl": 1,
        "proxied": True,
    }
    try:
        # Descobre IP dinâmico via lookup do próprio domínio raiz (fallback)
        try:
            import socket as sk
            ip_raiz = sk.gethostbyname(f"prospeccao.{DOMINIO_BASE}")
            payload["content"] = ip_raiz
        except Exception:
            pass

        r = requests.post(
            f"https://api.cloudflare.com/client/v4/zones/{CLOUDFLARE_ZONE_ID}/dns_records",
            headers={
                "Authorization": f"Bearer {CLOUDFLARE_DNS_TOKEN}",
                "Content-Type": "application/json"
            },
            json=payload,
            timeout=20
        )
        return r.json().get("success", False)
    except Exception as e:
        print(f"[analisador] erro CF: {e}")
        return False


# ── Screenshot (Playwright) ────────────────────────────────
def tirar_screenshot(url: str, dest: Path, timeout_ms: int = 20000) -> bool:
    try:
        from playwright.sync_api import sync_playwright
        with sync_playwright() as p:
            browser = p.chromium.launch(headless=True)
            page = browser.new_page(viewport={"width": 1280, "height": 900})
            page.goto(url, timeout=timeout_ms, wait_until="networkidle")
            page.screenshot(path=str(dest), full_page=False)
            browser.close()
        return True
    except Exception as e:
        print(f"[analisador] screenshot falhou: {e}")
        return False


# ── PDF dossiê ─────────────────────────────────────────────
def gerar_pdf_dossie(analise_id: int, dados: dict, copy: dict, site_url: str,
                     screenshot_path: Path | None, dest_pdf: Path) -> bool:
    try:
        from weasyprint import HTML
    except Exception as e:
        print(f"[analisador] weasyprint indisponível: {e}")
        return False

    nome = dados.get("nome_google") or dados.get("nome") or "Negócio"
    reviews = dados.get("reviews") or []
    reviews_html = "".join(
        f"""<div class="review">
              <div class="rev-head">
                <strong>{r.get('author_name','—')}</strong>
                <span class="stars">{'★' * int(r.get('rating', 0))}</span>
              </div>
              <p>{(r.get('text') or '')[:400]}</p>
            </div>"""
        for r in reviews[:5]
    ) or "<p><em>Sem avaliações públicas.</em></p>"

    oportunidades_html = "".join(
        f"<li>{o}</li>" for o in (copy.get("analise_oportunidades") or [])
    )

    screenshot_html = ""
    if screenshot_path and screenshot_path.exists():
        b64 = base64.b64encode(screenshot_path.read_bytes()).decode()
        screenshot_html = f'<img src="data:image/png;base64,{b64}" class="screenshot" />'

    cor1 = copy.get("cor_primaria") or "#2E7D32"

    html = f"""<!DOCTYPE html>
<html><head><meta charset="UTF-8" />
<style>
  @page {{ size: A4; margin: 20mm 15mm; @bottom-center {{ content: "Dossiê Climb Digital — página " counter(page); font-size: 9pt; color: #999; }} }}
  body {{ font-family: -apple-system, Arial, sans-serif; color: #222; line-height: 1.55; font-size: 11pt; }}
  h1 {{ color: {cor1}; font-size: 26pt; margin: 0 0 6pt; }}
  h2 {{ color: {cor1}; font-size: 15pt; margin: 22pt 0 8pt; border-bottom: 2px solid {cor1}; padding-bottom: 4pt; }}
  .subtitle {{ color: #666; font-size: 12pt; margin-bottom: 20pt; }}
  .grid {{ display: flex; gap: 12pt; margin: 12pt 0; }}
  .box {{ flex: 1; background: #f7f7f7; padding: 12pt; border-radius: 6pt; }}
  .box strong {{ display: block; color: {cor1}; font-size: 9pt; text-transform: uppercase; letter-spacing: 0.5pt; margin-bottom: 4pt; }}
  ul {{ margin: 8pt 0 8pt 18pt; }}
  li {{ margin: 4pt 0; }}
  .review {{ background: #fafafa; padding: 10pt; border-left: 3pt solid {cor1}; margin: 8pt 0; border-radius: 4pt; }}
  .rev-head {{ display: flex; justify-content: space-between; margin-bottom: 4pt; }}
  .stars {{ color: #FFA000; }}
  .screenshot {{ width: 100%; border: 1pt solid #ddd; border-radius: 6pt; margin: 10pt 0; }}
  .cta {{ background: {cor1}; color: white; padding: 14pt; border-radius: 8pt; margin-top: 16pt; text-align: center; }}
  .cta a {{ color: white; font-weight: bold; text-decoration: none; }}
  .meta {{ color: #888; font-size: 9pt; margin-top: 24pt; text-align: center; }}
</style></head>
<body>

<h1>Dossiê de Análise — {nome}</h1>
<div class="subtitle">{dados.get('cidade','')} • {dados.get('categoria','')} • {datetime.now().strftime('%d/%m/%Y')}</div>

<h2>Perfil do negócio</h2>
<div class="grid">
  <div class="box">
    <strong>Endereço</strong>
    {dados.get('endereco') or '—'}
  </div>
  <div class="box">
    <strong>Telefone</strong>
    {dados.get('telefone') or dados.get('lead_telefone') or '—'}
  </div>
</div>
<div class="grid">
  <div class="box">
    <strong>Avaliação Google</strong>
    {dados.get('rating') or '—'} / 5 ({dados.get('total_reviews') or 0} avaliações)
  </div>
  <div class="box">
    <strong>Site atual</strong>
    {dados.get('lead_site_atual') or dados.get('site_google') or '—'}
  </div>
</div>

<h2>Oportunidades identificadas</h2>
<ul>{oportunidades_html or '<li>—</li>'}</ul>

<h2>Avaliações reais (Google)</h2>
{reviews_html}

<h2>Site demonstrativo</h2>
<p>Preparamos uma página demonstrativa mostrando como o negócio pode se apresentar online:</p>
{screenshot_html}
<div class="cta">
  <a href="{site_url}">Ver site online → {site_url}</a>
</div>

<div class="meta">Gerado por Climb Digital • Análise #{analise_id}</div>
</body></html>
"""

    try:
        HTML(string=html).write_pdf(str(dest_pdf))
        return True
    except Exception as e:
        print(f"[analisador] erro pdf: {e}")
        return False


# ── Template dossiê v2 (oxblood/dourado/ivory) ────────────
TEMPLATE_DOSSIE_PATH = Path("/opt/mia/workspace/prospeccao_ativa/templates/dossie_template.html")


def _fmt_num(n) -> str:
    """Formata número: 1234 -> '1,2k' quando > 999."""
    try:
        n = int(n)
    except Exception:
        return str(n) if n else "0"
    if n >= 1000:
        v = n / 1000
        return f"{v:.1f}".replace(".", ",").rstrip("0").rstrip(",") + "k"
    return str(n)


def _handle_ig(url_or_handle: str) -> str:
    """Extrai handle @xxx de qualquer coisa (URL, @handle, texto)."""
    if not url_or_handle:
        return "—"
    s = str(url_or_handle).strip()
    if "instagram.com/" in s:
        s = s.split("instagram.com/")[-1]
    s = s.split("/")[0].split("?")[0].strip()
    if not s:
        return "—"
    return s if s.startswith("@") else f"@{s}"


def gerar_dossie_html(analise_id: int, dados: dict, copy: dict,
                      site_url: str, instagram_data: dict | None = None) -> Path | None:
    """
    Gera o HTML do dossiê v2 (template oxblood/dourado/ivory) preenchido
    com dados do lead. Retorna o path do HTML salvo, ou None se falhar.
    """
    if not TEMPLATE_DOSSIE_PATH.exists():
        print(f"[analisador] template não encontrado: {TEMPLATE_DOSSIE_PATH}")
        return None

    instagram_data = instagram_data or {}
    analise_dir = BASE_ANALISES / str(analise_id)
    analise_dir.mkdir(parents=True, exist_ok=True)

    # ── Dados básicos
    nome = dados.get("nome_google") or dados.get("nome") or "Negócio"
    cidade = dados.get("cidade") or ""
    categoria = dados.get("categoria") or ""
    endereco = dados.get("endereco") or dados.get("lead_endereco") or "—"
    telefone = dados.get("telefone") or dados.get("lead_telefone") or "—"
    rating = dados.get("rating")
    site_atual = dados.get("lead_site_atual") or dados.get("site_google") or ""
    reviews_raw = dados.get("reviews") or []

    instagram_url = dados.get("instagram") or ""
    handle = _handle_ig(instagram_url) if instagram_url else "—"
    bio = instagram_data.get("bio") or dados.get("descricao") or categoria or "Presença digital do negócio"
    ig_seguidores = _fmt_num(instagram_data.get("seguidores") or 0)
    ig_posts = _fmt_num(instagram_data.get("posts") or 0)
    ig_seguindo = _fmt_num(instagram_data.get("seguindo") or 0)

    # ── Data pt-BR
    meses = ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho",
             "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"]
    agora = datetime.now()
    cidade_data = f"{cidade} · {meses[agora.month - 1]} {agora.year}" if cidade else f"{meses[agora.month - 1]} {agora.year}"

    # ── Foto de perfil e screenshot (paths absolutos file://)
    foto_perfil = ""
    imgs_dir = analise_dir / "imgs"
    if imgs_dir.exists():
        for cand in ("foto_1.jpg", "foto_1.png", "foto_0.jpg", "foto_0.png"):
            fp = imgs_dir / cand
            if fp.exists():
                foto_perfil = f"file://{fp}"
                break
    if not foto_perfil:
        # placeholder cinza inline (evita broken image)
        foto_perfil = "data:image/svg+xml;base64," + base64.b64encode(
            b'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect fill="#8a6535" width="100" height="100"/></svg>'
        ).decode()

    screenshot_site = ""
    ss_path = analise_dir / "screenshot.png"
    if ss_path.exists():
        screenshot_site = f"file://{ss_path}"

    site_url_clean = site_url.replace("https://", "").replace("http://", "").rstrip("/")

    # ── Score de presença digital
    score_google = 10 if rating else 0
    score_site = 0  # padrão pra lead sem site profissional
    score_instagram = 8 if instagram_url else 0
    score_bio = 5 if (bio and len(str(bio)) > 20) else 0
    score_link = 5 if site_atual else 0
    score_agendamento = 2
    score_atendimento = 0
    score_crm = 0

    score_total = (score_google + score_site + score_instagram + score_bio
                   + score_link + score_agendamento + score_atendimento + score_crm)
    # potencial = quanto poderia chegar com otimização (máx de cada critério ativado)
    score_potencial = 10 + 20 + 15 + 10 + 15 + 10 + 10 + 10  # = 100
    # se hoje não tem instagram, o realista fica em ~92
    if score_total < 40:
        score_potencial = 92
    else:
        score_potencial = min(100, score_total + 55)

    def pct(v, tot):
        try:
            return int(round((v / tot) * 100))
        except Exception:
            return 0

    # ── Reviews HTML (top 3)
    if reviews_raw:
        reviews_parts = []
        for r in reviews_raw[:3]:
            author = (r.get("author_name") or "Cliente").strip()
            stars = "&#9733;" * int(r.get("rating", 5))
            text = (r.get("text") or "").strip().replace("\n", " ")
            if len(text) > 220:
                text = text[:217] + "..."
            reviews_parts.append(f"""
        <div class="review">
          <div class="review-head">
            <div class="review-author">{author}</div>
            <div class="review-stars">{stars}</div>
          </div>
          <div class="review-text">"{text}"</div>
        </div>""")
        reviews_html = "".join(reviews_parts)
    else:
        reviews_html = '<div class="review"><div class="review-text">Sem avaliações públicas registradas até o momento.</div></div>'

    # ── Pontos fortes / fracos (baseado nos dados)
    fortes = []
    fracos = []

    if rating and float(rating) >= 4.5:
        fortes.append("Avaliação Google acima de 4.5 estrelas")
    if endereco and endereco != "—":
        fortes.append("Endereço visível no perfil")
    if instagram_url:
        fortes.append("Presença ativa no Instagram")
    if bio and len(str(bio)) > 30:
        fortes.append("Bio descreve especialidades com clareza")
    if telefone and telefone != "—":
        fortes.append("Telefone de contato disponível")

    if not site_atual:
        fracos.append("Sem site profissional próprio")
    else:
        fracos.append("Site atual não capta leads com eficiência")
    fracos.append("Sem chamada para ação (CTA) clara na bio")
    if not instagram_url:
        fracos.append("Sem presença ativa no Instagram")
    else:
        fracos.append("Link da bio não direciona para agendamento")
    fracos.append("Sem uso estruturado de hashtags")

    # garante mínimo de 3, máximo de 4
    fortes = fortes[:4] or ["Negócio consolidado localmente"]
    fracos = fracos[:4]

    pontos_fortes_html = "".join(f"            <li>{p}</li>\n" for p in fortes).rstrip()
    pontos_fracos_html = "".join(f"            <li>{p}</li>\n" for p in fracos).rstrip()

    # ── Diagnóstico Instagram (texto livre)
    if instagram_url:
        diagnostico = (
            f"Frequência de postagem irregular, com uso baixo de Reels. "
            f"O benchmark do setor de {categoria.lower() or 'serviços'} para tráfego orgânico "
            "está em 30 a 50% de Reels. A bio traz informações relevantes, mas o link atual "
            "não direciona para uma página que capture contato ou faça agendamento."
        )
    else:
        diagnostico = (
            "Perfil de Instagram ainda não identificado publicamente. Presença digital "
            "concentrada em ferramentas passivas (Google Meu Negócio), sem canal ativo "
            "de captação e relacionamento com potenciais clientes."
        )

    # ── Estratégia 30 dias (4 blocos)
    estrategia_html = f"""    <div class="strategy-block">
      <div class="strategy-head">
        <div class="strategy-title">Presenca <em>Digital</em></div>
        <div class="strategy-week">Semana 1-2</div>
      </div>
      <ul class="strategy-items">
        <li><strong>Site profissional com agendamento online.</strong> Clientes que buscam "{categoria.lower() or 'seu serviço'} {cidade}" no Google hoje encontram concorrentes primeiro.</li>
        <li><strong>Link ativo na bio</strong> direcionando para agendamento ou WhatsApp com resposta rápida.</li>
      </ul>
    </div>

    <div class="strategy-block">
      <div class="strategy-head">
        <div class="strategy-title"><em>Conteudo</em></div>
        <div class="strategy-week">Semana 1-4</div>
      </div>
      <ul class="strategy-items">
        <li><strong>3 posts por semana:</strong> 1 educativo, 1 resultado (antes e depois), 1 depoimento de cliente.</li>
        <li><strong>Reels curtos (15-30s)</strong> têm 4x mais alcance orgânico que fotos estáticas.</li>
        <li><strong>Stories diários</strong> mantêm o perfil ativo no algoritmo.</li>
      </ul>
    </div>

    <div class="strategy-block">
      <div class="strategy-head">
        <div class="strategy-title"><em>Atendimento</em></div>
        <div class="strategy-week">Semana 2-3</div>
      </div>
      <ul class="strategy-items">
        <li><strong>Respostas automáticas inteligentes no WhatsApp.</strong> Clientes esperam retorno em menos de 1 hora, reduzindo perda de leads fora do horário.</li>
        <li><strong>Acompanhamento pós-atendimento</strong> aumenta a taxa de retorno em até 60%.</li>
      </ul>
    </div>

    <div class="strategy-block">
      <div class="strategy-head">
        <div class="strategy-title"><em>Fidelizacao</em></div>
        <div class="strategy-week">Semana 3-4</div>
      </div>
      <ul class="strategy-items">
        <li><strong>Histórico de clientes centralizado</strong> permite lembrar aniversários, retornos e indicações.</li>
        <li><strong>Programa de indicação</strong> ("traga uma amiga") digitalizado multiplica o boca-a-boca.</li>
      </ul>
    </div>"""

    # ── Subtítulo (categoria capitalizada)
    subtitulo = (categoria or "Presenca Digital").title()

    # ── Substituições
    replacements = {
        "{{NOME}}": nome,
        "{{SUBTITULO}}": subtitulo,
        "{{INSTAGRAM_HANDLE}}": handle,
        "{{ENDERECO}}": endereco,
        "{{TELEFONE}}": telefone,
        "{{CIDADE_DATA}}": cidade_data,
        "{{IG_SEGUIDORES}}": ig_seguidores,
        "{{IG_POSTS}}": ig_posts,
        "{{IG_SEGUINDO}}": ig_seguindo,
        "{{GOOGLE_RATING}}": f"{float(rating):.1f}" if rating else "—",
        "{{SCORE}}": str(score_total),
        "{{SCORE_POTENCIAL}}": str(score_potencial),
        "{{FOTO_PERFIL}}": foto_perfil,
        "{{SCREENSHOT_SITE}}": screenshot_site,
        "{{SITE_URL}}": site_url_clean,
        "{{IG_BIO}}": str(bio),
        "{{DIAGNOSTICO_INSTAGRAM}}": diagnostico,
        "{{REVIEWS_HTML}}": reviews_html,
        "{{PONTOS_FORTES_HTML}}": pontos_fortes_html,
        "{{PONTOS_FRACOS_HTML}}": pontos_fracos_html,
        "{{ESTRATEGIA_HTML}}": estrategia_html,
        # Score por barra + percentual
        "{{SCORE_GOOGLE}}": str(score_google),
        "{{SCORE_GOOGLE_PCT}}": str(pct(score_google, 10)),
        "{{SCORE_SITE}}": str(score_site),
        "{{SCORE_SITE_PCT}}": str(pct(score_site, 20)),
        "{{SCORE_INSTAGRAM}}": str(score_instagram),
        "{{SCORE_INSTAGRAM_PCT}}": str(pct(score_instagram, 15)),
        "{{SCORE_BIO}}": str(score_bio),
        "{{SCORE_BIO_PCT}}": str(pct(score_bio, 10)),
        "{{SCORE_LINK}}": str(score_link),
        "{{SCORE_LINK_PCT}}": str(pct(score_link, 15)),
        "{{SCORE_AGENDAMENTO}}": str(score_agendamento),
        "{{SCORE_AGENDAMENTO_PCT}}": str(pct(score_agendamento, 10)),
        "{{SCORE_ATENDIMENTO}}": str(score_atendimento),
        "{{SCORE_ATENDIMENTO_PCT}}": str(pct(score_atendimento, 10)),
        "{{SCORE_CRM}}": str(score_crm),
        "{{SCORE_CRM_PCT}}": str(pct(score_crm, 10)),
    }

    try:
        html = TEMPLATE_DOSSIE_PATH.read_text(encoding="utf-8")
        for k, v in replacements.items():
            html = html.replace(k, str(v))

        dest = analise_dir / "dossie.html"
        dest.write_text(html, encoding="utf-8")
        return dest
    except Exception as e:
        print(f"[analisador] erro gerar_dossie_html: {e}")
        return None


def gerar_pdf_playwright(html_path: Path, pdf_path: Path) -> bool:
    """Gera PDF a partir de HTML usando Playwright/Chromium."""
    try:
        from playwright.sync_api import sync_playwright
        with sync_playwright() as p:
            browser = p.chromium.launch(headless=True)
            page = browser.new_page()
            page.goto(f"file://{html_path}", timeout=30000, wait_until="networkidle")
            page.wait_for_timeout(2000)
            page.pdf(
                path=str(pdf_path),
                format="A4",
                print_background=True,
                margin={"top": "0", "bottom": "0", "left": "0", "right": "0"}
            )
            browser.close()
        return True
    except Exception as e:
        print(f"[analisador] erro playwright pdf: {e}")
        return False


# ── ORQUESTRADOR ───────────────────────────────────────────
def run_analise_lead(analise_id: int, lead_data: dict) -> None:
    """Roda a análise completa em background."""
    try:
        update_analise(analise_id, status="em_andamento")

        # Etapa 1: dados públicos
        place = coletar_google_places(lead_data.get("nome", ""), lead_data.get("cidade", ""))
        site_info = coletar_site_atual(lead_data.get("site_atual") or place.get("site_google") or "")
        insta = lead_data.get("instagram") or buscar_instagram(lead_data.get("nome", ""), lead_data.get("cidade", ""))

        dados_full = {
            **lead_data,
            **place,
            **site_info,
            "instagram": insta,
        }

        # Etapa 2: copy (Jonathan / Anthropic Haiku)
        copy = gerar_copy_jonathan(dados_full)

        # Etapa 3: imagens
        analise_dir = BASE_ANALISES / str(analise_id)
        imgs_dir = analise_dir / "imgs"
        site_dir = analise_dir / "site"
        site_imgs = site_dir / "imgs"
        site_dir.mkdir(parents=True, exist_ok=True)

        imagens = baixar_imagens(place, lead_data.get("categoria", ""), imgs_dir)
        # copia pras imgs do site
        site_imgs.mkdir(parents=True, exist_ok=True)
        for i in imagens:
            try:
                shutil.copy(imgs_dir / i, site_imgs / i)
            except Exception:
                pass

        # Etapa 4: site HTML + servidor + Traefik + Cloudflare
        segmento = detectar_segmento(dados_full)
        html = gerar_html_leo(copy, dados_full, imagens, segmento)
        (site_dir / "index.html").write_text(html, encoding="utf-8")

        slug_base = slugify(lead_data.get("nome", "lead"))
        slug = f"{slug_base}-{analise_id}"
        porta = porta_livre()

        try:
            montar_servidor_site(analise_id, site_dir, porta, slug)
        except Exception as e:
            print(f"[analisador] erro subindo servidor: {e}")

        host = ""
        try:
            host = registrar_traefik(slug, porta)
            # Traefik (EasyPanel/Docker) recarrega custom.yaml automaticamente via file watcher.
        except Exception as e:
            print(f"[analisador] erro traefik: {e}")

        try:
            registrar_cloudflare(slug)
        except Exception as e:
            print(f"[analisador] erro cloudflare: {e}")

        site_url = f"https://{host}" if host else f"http://127.0.0.1:{porta}"

        # Etapa 5: screenshot + PDF
        screenshot_path = analise_dir / "screenshot.png"
        # tenta local direto pra evitar espera do DNS/SSL
        local_url = f"http://127.0.0.1:{porta}"
        time.sleep(2)  # dá tempo do http.server subir
        tirar_screenshot(local_url, screenshot_path)

        pdf_path = analise_dir / "dossie.pdf"

        # Novo fluxo: template v2 (oxblood/dourado/ivory) via Playwright
        pdf_ok = False
        if TEMPLATE_DOSSIE_PATH.exists():
            html_path = gerar_dossie_html(
                analise_id, dados_full, copy, site_url, instagram_data={}
            )
            if html_path:
                pdf_ok = gerar_pdf_playwright(html_path, pdf_path)

        # Fallback: WeasyPrint com HTML genérico
        if not pdf_ok:
            print(f"[analisador] fallback pra gerar_pdf_dossie (WeasyPrint)")
            gerar_pdf_dossie(analise_id, dados_full, copy, site_url,
                             screenshot_path if screenshot_path.exists() else None,
                             pdf_path)

        # Persistir análise
        payload = {
            "copy": copy,
            "dados": {
                k: v for k, v in dados_full.items()
                if k not in ("reviews",)  # pra não estourar
            },
            "reviews": dados_full.get("reviews", []),
            "imagens": imagens,
            "site_url": site_url,
            "slug": slug,
            "porta": porta,
        }

        update_analise(
            analise_id,
            status="concluido",
            site_url=site_url,
            pdf_path=str(pdf_path) if pdf_path.exists() else None,
            analise_json=json.dumps(payload, ensure_ascii=False),
            concluido_em=datetime.now().isoformat(),
        )
    except Exception as e:
        import traceback
        tb = traceback.format_exc()
        print(f"[analisador] ERRO id={analise_id}: {e}\n{tb}")
        update_analise(analise_id, status="erro", erro=str(e)[:500],
                       concluido_em=datetime.now().isoformat())
