# bot_core.py — Núcleo del bot reutilizable para polling o webhook
# (Extraído de tu bot.py, SIN leer token ni ejecutar run_polling)
# Uso:
#   from bot_core import build_application
#   app = build_application("TU_TOKEN")
#   app.run_polling()  # o integrar en Flask/webhook

import os
import re
import json
import logging
from dataclasses import dataclass
from typing import Dict, Pattern, List
from pathlib import Path
from datetime import datetime

from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import (
    ApplicationBuilder, ContextTypes,
    CommandHandler, MessageHandler, CallbackQueryHandler, ConversationHandler,
    filters
)

# -------------------- Configuración e init --------------------
DATA_DIR = Path(__file__).parent / "data"

DEFAULT_CONFIG = {
    "website_url": "https://www.newintlcenter.org",
    "map_url": "https://www.google.com/maps?q=80+Maiden+Lane+13th+Floor+New+York+NY+10028",
    "default_lang": "es"
}

@dataclass
class Matcher:
    pattern: Pattern
    answer: str

I18N: Dict[str, dict] = {}
LANGS: Dict[str, str] = {}   # code -> display name
MATCHERS: Dict[str, List[Matcher]] = {}
CONFIG: Dict[str, str] = DEFAULT_CONFIG.copy()

# ---------- Texto al pie (siempre visible en cada respuesta) ----------
HINT_FALLBACKS = {
    "en": "💡 Type /menu to see options and /lang to change language.",
    "es": "💡 Escribe /menu para ver opciones y /lang para cambiar idioma.",
    "fr": "💡 Tape /menu pour voir les options et /lang pour changer de langue.",
    "ar": "💡 اكتب /menu لعرض الخيارات و /lang لتغيير اللغة.",
    "fa": "💡 ‎/menu را برای دیدن گزینه‌ها و /lang را برای تغییر زبان تایپ کنید.",
    "ru": "💡 Наберите /menu для опций и /lang для смены языка.",
    "uk": "💡 Введіть /menu, щоб побачити опції, та /lang, щоб змінити мову.",
}

def with_hint(s: str, lang: str = None) -> str:
    s = (s or "").replace("\\n", "\n").replace("\\r\\n", "\n")
    hint = None
    try:
        if lang and lang in I18N:
            hint_local = I18N[lang].get("strings", {}).get("hint")
            if isinstance(hint_local, str) and hint_local.strip():
                hint = hint_local
    except Exception:
        pass
    if not hint:
        hint = HINT_FALLBACKS.get(lang, HINT_FALLBACKS["en"])
    return f"{s}\n\n{hint}"

# ------------------------------ Utilidades JSON ------------------------------
def _load_json(path: Path) -> dict:
    with path.open("r", encoding="utf-8") as f:
        return json.load(f)

def load_config() -> Dict[str, str]:
    cfg_path = DATA_DIR / "config.json"
    cfg = DEFAULT_CONFIG.copy()
    if cfg_path.exists():
        try:
            cfg.update(_load_json(cfg_path))
        except Exception as e:
            logging.warning(f"No se pudo leer config.json: {e}")
    return cfg

def discover_languages() -> Dict[str, dict]:
    """Busca archivos data/i18n_*.json y devuelve {code: {name, i18n}}."""
    langs = {}
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    for p in DATA_DIR.glob("i18n_*.json"):
        code = p.stem.split("_", 1)[1]
        try:
            i18n = _load_json(p)
            name = i18n.get("name", code)
            langs[code] = {"name": name, "i18n": i18n}
        except Exception as e:
            logging.warning(f"No se pudo cargar {p.name}: {e}")
    return langs

def load_faq(lang: str) -> Dict[str, str]:
    """
    Carga data/faq_<lang>.json si existe; si no, arma un fallback simple.
    Placeholders: {WEBSITE_URL}, {MAP_URL}, {ADDRESS}, {MAP_LABEL}, {PROGRAMS}
    """
    path = DATA_DIR / f"faq_{lang}.json"
    if path.exists():
        try:
            return _load_json(path)
        except Exception as e:
            logging.warning(f"No se pudo leer {path.name}: {e}")

    i18n = I18N.get(lang, {})
    website = CONFIG["website_url"]
    map_label = i18n.get("open_map_label", "🗺️ Map")

    return {
        ("horario|horarios|schedule" if lang != "en" else "schedule|time|when"):
            i18n.get("schedule_text", f"🗓️ Schedules change. See: {website}"),

        ("direcci[oó]n|ubicaci[oó]n|address|location" if lang == "es" else
         "adresse|localisation|address|location" if lang == "fr" else
         "location|address|where"):
            f"📍 {{ADDRESS}}\n{map_label}: {{MAP_URL}}\n🌐 {website}",

        ("programa|programas|courses" if lang == "es" else
         "programme|programmes|cours" if lang == "fr" else
         "program|programs|course|english"):
            "{PROGRAMS}",

        ("costos|precio|fees|pagar" if lang == "es" else
         "coût|cout|prix|frais|payer|fees|price|pay" if lang == "fr" else
         "cost|price|fees|pay"):
            i18n.get("free_text", "💵 Our courses are free.") + f" {website}"
    }

def build_matchers(lang: str) -> List[Matcher]:
    data = load_faq(lang)
    matchers: List[Matcher] = []
    for patt, ans in data.items():
        matchers.append(Matcher(pattern=re.compile(patt, flags=re.IGNORECASE), answer=ans))
    return matchers

# ------------------------------ UI helpers ------------------------------
def t(lang: str, key: str) -> str:
    i18n = I18N.get(lang, {})
    strings = i18n.get("strings", {}) if isinstance(i18n.get("strings"), dict) else {}

    default_strings = {
        "welcome": "Hi! I’m the *International Center* assistant. How can I help you today?",
        "help": "Type your question (e.g., *schedule*, *apply*, *location*). Use /menu or /lang.",
        "lang_set": "✅ Language updated.",
        "no_match": f"I couldn’t find an exact answer 🤔. Check {DEFAULT_CONFIG['website_url']} or use /menu.",
        "choose_language": "Choose language / Elige idioma / Choisissez la langue:",
        # ---- Fallbacks del formulario ----
        "enroll_intro": "📝 Por favor comparte tus datos para cursos.",
        "ask_name": "👤 Tu nombre completo:",
        "ask_phone": "📱 Tu teléfono (incluye código de país si puedes):",
        "ask_email": "📧 Tu correo electrónico (o escribe *omitir*):",
        "ask_country": "🌎 ¿De qué país eres?",
        "ask_lang": "🗣️ ¿Qué idioma hablas principalmente?",
        "ask_internet": "🌐 ¿Tienes acceso estable a Internet?",
        "btn_yes": "Sí",
        "btn_no": "No",
        "ask_status": "🏷️ ¿Cuál es tu estatus en el país?",
        "status_pend_asy": "Asilo pendiente",
        "status_asy": "Asilado/a",
        "status_tps": "TPS",
        "status_cit": "Ciudadano/a",
        "status_other": "Otro (especificar)",
        "ask_status_other": "✍️ Especifica tu estatus:",
        "ask_heard": "📣 ¿Cómo supiste de nosotros?",
        "heard_friend": "Un amigo",
        "heard_web": "Página web",
        "heard_press": "Prensa",
        "heard_radio": "Radio",
        "heard_tv": "TV",
        "heard_other": "Otro (especificar)",
        "ask_heard_other": "✍️ Cuéntanos cómo supiste de nosotros:",
        "enroll_thanks": "✅ ¡Gracias! Te contactaremos pronto."
    }

    val = strings.get(key)
    if isinstance(val, str):
        cleaned = val.strip()
        if cleaned and cleaned not in {"...", "…"}:
            return val
    return default_strings.get(key, key)

def _color_label(idx: int, label: str) -> str:
    colors = ["🟦", "🟨", "🟥", "🟩", "🟪", "🟧"]
    return f"{colors[idx % len(colors)]} {label}"

def menu_keyboard(lang: str) -> InlineKeyboardMarkup:
    i18n = I18N.get(lang, {})
    m = i18n.get("menu", {})
    programs = _color_label(0, m.get("programs", "Programas"))
    schedule = _color_label(1, m.get("schedule", "Horarios"))
    location = _color_label(2, m.get("location", "Ubicación"))
    faq      = _color_label(5, m.get("faq", "FAQ"))
    enroll   = _color_label(3, m.get("enroll", "Interesado/a en cursos"))
    langlbl  = _color_label(4, m.get("language", "Language"))
    sitelbl  = m.get("site", "🌐 Sitio web")

    return InlineKeyboardMarkup([
        [InlineKeyboardButton(programs, callback_data="programs")],
        [InlineKeyboardButton(schedule, callback_data="schedule")],
        [InlineKeyboardButton(location, callback_data="location")],
        [InlineKeyboardButton(faq, callback_data="faq")],
        [InlineKeyboardButton(enroll, callback_data="enroll")],
        [InlineKeyboardButton(langlbl, callback_data="langmenu")],
        [InlineKeyboardButton(sitelbl, url=CONFIG["website_url"])]
    ])

def lang_keyboard() -> InlineKeyboardMarkup:
    row, rows = [], []
    for code, name in LANGS.items():
        row.append(InlineKeyboardButton(name, callback_data=f"setlang_{code}"))
        if len(row) == 3:
            rows.append(row); row = []
    if row:
        rows.append(row)
    return InlineKeyboardMarkup(rows)

def get_lang(context: ContextTypes.DEFAULT_TYPE) -> str:
    lang = context.user_data.get("lang")
    if lang in LANGS:
        return lang
    return CONFIG.get("default_lang", "en") if CONFIG.get("default_lang") in LANGS else next(iter(LANGS.keys()))

# ------------------------------- Handlers básicos ---------------------------------
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    lang = get_lang(context)
    await update.message.reply_text(
        with_hint(t(lang, "welcome").replace("{WEBSITE_URL}", CONFIG["website_url"]), lang),
        reply_markup=menu_keyboard(lang),
        parse_mode="Markdown"
    )

async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
    lang = get_lang(context)
    await update.message.reply_text(
        with_hint(t(lang, "help").replace("{WEBSITE_URL}", CONFIG["website_url"]), lang)
    )

async def menu_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
    lang = get_lang(context)
    await update.message.reply_text(with_hint("👇", lang), reply_markup=menu_keyboard(lang))

async def lang_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if context.args:
        code = context.args[0].lower()
        if code in LANGS:
            context.user_data["lang"] = code
            await update.message.reply_text(with_hint(t(code, "lang_set"), code))
            return
    current = get_lang(context)
    await update.message.reply_text(with_hint(t(current, "choose_language"), current), reply_markup=lang_keyboard())

# ------------------------------- Callback genérico --------------------------------
async def on_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    data = query.data
    await query.answer()

    if data.startswith("setlang_"):
        code = data.split("_", 1)[1]
        if code in LANGS:
            context.user_data["lang"] = code
            await query.edit_message_text(with_hint(t(code, "lang_set"), code))
        return

    lang = get_lang(context)
    i18n = I18N.get(lang, {})

    if data == "programs":
        text = i18n.get("programs_text", "📚 Programs. {WEBSITE_URL}")
        kb = InlineKeyboardMarkup([[InlineKeyboardButton(i18n.get("menu", {}).get("site", "🌐 Website"), url=CONFIG["website_url"])]])
        await query.edit_message_text(
            with_hint(text.replace("{WEBSITE_URL}", CONFIG["website_url"]), lang),
            reply_markup=kb,
            parse_mode="Markdown",
            disable_web_page_preview=True
        )

    elif data == "schedule":
        text = i18n.get("schedule_text", f"🗓️ See schedules on: {CONFIG['website_url']}")
        await query.edit_message_text(with_hint(text.replace("{WEBSITE_URL}", CONFIG["website_url"]), lang))

    elif data == "location":
        addr = i18n.get("address", "80 Maiden Lane, 13th Floor, New York, NY 10028")
        open_map_label = i18n.get("open_map_label", "🗺️ Open map")
        site_label = i18n.get("menu", {}).get("site", "🌐 Website")
        msg = f"📍 {addr}"
        kb = InlineKeyboardMarkup([
            [InlineKeyboardButton(open_map_label, url=CONFIG["map_url"])],
            [InlineKeyboardButton(site_label, url=CONFIG["website_url"])],
        ])
        await query.edit_message_text(with_hint(msg, lang), reply_markup=kb)

    elif data == "faq":
        hint = i18n.get("faq_hint", t(lang, "help"))
        await query.edit_message_text(with_hint(hint, lang), parse_mode="Markdown")

    elif data == "langmenu":
        await query.edit_message_text(
            with_hint(t(get_lang(context), "choose_language"), lang),
            reply_markup=lang_keyboard()
        )

# ------------------------------- Conversación: Interesado en cursos ----------------
ASK_NAME, ASK_PHONE, ASK_EMAIL, ASK_COUNTRY, ASK_LANG, ASK_INTERNET, ASK_STATUS, ASK_STATUS_OTHER, ASK_HEARD, ASK_HEARD_OTHER = range(10)
LEADS_FILE = DATA_DIR / "leads.csv"

EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
PHONE_RE = re.compile(r"^\+?\d[\d\s\-().]{6,}$")

def internet_kb(lang: str) -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup([
        [InlineKeyboardButton(t(lang, "btn_yes"), callback_data="internet_yes"),
         InlineKeyboardButton(t(lang, "btn_no"), callback_data="internet_no")]
    ])

def status_kb(lang: str) -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup([
        [InlineKeyboardButton(t(lang, "status_pend_asy"), callback_data="status_pend_asy")],
        [InlineKeyboardButton(t(lang, "status_asy"), callback_data="status_asy")],
        [InlineKeyboardButton(t(lang, "status_tps"), callback_data="status_tps")],
        [InlineKeyboardButton(t(lang, "status_cit"), callback_data="status_cit")],
        [InlineKeyboardButton(t(lang, "status_other"), callback_data="status_other")]
    ])

def heard_kb(lang: str) -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup([
        [InlineKeyboardButton(t(lang, "heard_friend"), callback_data="heard_friend")],
        [InlineKeyboardButton(t(lang, "heard_web"), callback_data="heard_web")],
        [InlineKeyboardButton(t(lang, "heard_press"), callback_data="heard_press")],
        [InlineKeyboardButton(t(lang, "heard_radio"), callback_data="heard_radio")],
        [InlineKeyboardButton(t(lang, "heard_tv"), callback_data="heard_tv")],
        [InlineKeyboardButton(t(lang, "heard_other"), callback_data="heard_other")],
    ])

YES_WORDS = {"si", "sí", "s", "yes", "y", "oui", "ja", "evet", "да", "так"}
NO_WORDS  = {"no", "n", "non", "nein", "hayir", "нет", "ні", "not"}

def parse_yes_no(text: str):
    t_ = (text or "").strip().lower().replace("í", "i")
    if t_ in YES_WORDS: return "yes"
    if t_ in NO_WORDS:  return "no"
    return None

STATUS_SYNONYMS = {
    "pend_asy": ["asilo pendiente", "pendiente asilo", "pending asylum", "solicitud de asilo"],
    "asy":      ["asilado", "asilada", "asylee", "refugiado", "refugiada", "asilo aprobado"],
    "tps":      ["tps"],
    "cit":      ["ciudadano", "ciudadana", "citizen", "us citizen"],
    "other":    ["otro", "otra", "otros", "other"]
}

def match_status(text: str):
    t_ = (text or "").strip().lower()
    for code, words in STATUS_SYNONYMS.items():
        if any(w in t_ for w in words):
            return code
    return None

HEARD_SYNONYMS = {
    "heard_friend": ["amigo", "una amiga", "friend", "familia", "conocido"],
    "heard_web":    ["web", "pagina", "página", "website", "site", "internet"],
    "heard_press":  ["prensa", "periodico", "periódico", "diario", "press", "news"],
    "heard_radio":  ["radio"],
    "heard_tv":     ["tv", "tele", "television", "televisión"],
    "heard_other":  ["otro", "other"],
}

def match_heard(text: str):
    t_ = (text or "").strip().lower()
    for code, words in HEARD_SYNONYMS.items():
        if any(w in t_ for w in words):
            return code
    return None

# Entry por comando
async def enroll_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
    lang = get_lang(context)
    await update.message.reply_text(with_hint(t(lang, "enroll_intro"), lang))
    await update.message.reply_text(with_hint(t(lang, "ask_name"), lang))
    context.chat_data["enroll_running"] = True
    return ASK_NAME

# Entry por botón del menú (callback)
async def enroll_entry_cb(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    lang = get_lang(context)
    context.chat_data["enroll_running"] = True
    intro   = t(lang, "enroll_intro") or "📝 Por favor comparte tus datos para cursos."
    first_q = t(lang, "ask_name")     or "👤 Tu nombre completo:"
    await query.message.reply_text(with_hint(f"{intro}\n\n{first_q}", lang))
    return ASK_NAME

async def ask_phone(update: Update, context: ContextTypes.DEFAULT_TYPE):
    context.user_data["lead_name"] = (update.message.text or "").strip()
    lang = get_lang(context)
    await update.message.reply_text(with_hint(t(lang, "ask_phone"), lang))
    return ASK_PHONE

async def ask_email(update: Update, context: ContextTypes.DEFAULT_TYPE):
    phone = (update.message.text or "").strip()
    lang = get_lang(context)
    if not PHONE_RE.match(phone):
        await update.message.reply_text(with_hint("Formato de teléfono inválido. Ej.: +1 212 555 1234", lang))
        return ASK_PHONE
    context.user_data["lead_phone"] = phone
    await update.message.reply_text(with_hint(t(lang, "ask_email"), lang), parse_mode="Markdown")
    return ASK_EMAIL

async def ask_country(update: Update, context: ContextTypes.DEFAULT_TYPE):
    email = (update.message.text or "").strip()
    lang = get_lang(context)
    if email.lower() in {"omitir", "ninguno", "no tengo", "skip", "none", "no"} or email == "":
        context.user_data["lead_email"] = ""
    else:
        if not EMAIL_RE.match(email):
            await update.message.reply_text(with_hint("Correo inválido. Escribe uno válido o *omitir*.", lang), parse_mode="Markdown")
            return ASK_EMAIL
        context.user_data["lead_email"] = email
    await update.message.reply_text(with_hint(t(lang, "ask_country"), lang))
    return ASK_COUNTRY

async def ask_language(update: Update, context: ContextTypes.DEFAULT_TYPE):
    context.user_data["lead_country"] = (update.message.text or "").strip()
    lang = get_lang(context)
    await update.message.reply_text(with_hint(t(lang, "ask_lang"), lang))
    return ASK_LANG

async def ask_internet(update: Update, context: ContextTypes.DEFAULT_TYPE):
    context.user_data["lead_pref_lang"] = (update.message.text or "").strip()
    lang = get_lang(context)
    await update.message.reply_text(with_hint(t(lang, "ask_internet"), lang), reply_markup=internet_kb(lang))
    return ASK_INTERNET

async def set_internet(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    has_net = "Sí" if query.data.endswith("yes") else "No"
    context.user_data["lead_internet"] = has_net
    lang = get_lang(context)
    await query.message.reply_text(with_hint(t(lang, "ask_status"), lang), reply_markup=status_kb(lang))
    return ASK_STATUS

async def set_internet_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
    lang = get_lang(context)
    choice = parse_yes_no(update.message.text)
    if not choice:
        await update.message.reply_text(
            with_hint("Responde *Sí* o *No* (o usa los botones).", lang),
            reply_markup=internet_kb(lang), parse_mode="Markdown"
        )
        return ASK_INTERNET
    context.user_data["lead_internet"] = "Sí" if choice == "yes" else "No"
    await update.message.reply_text(with_hint(t(lang, "ask_status"), lang), reply_markup=status_kb(lang))
    return ASK_STATUS

async def set_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    code = query.data.replace("status_", "")
    mapping = {
        "pend_asy": t(get_lang(context), "status_pend_asy"),
        "asy": t(get_lang(context), "status_asy"),
        "tps": t(get_lang(context), "status_tps"),
        "cit": t(get_lang(context), "status_cit"),
        "other": t(get_lang(context), "status_other"),
    }
    context.user_data["lead_status"] = mapping.get(code, code)
    lang = get_lang(context)
    if code == "other":
        await query.message.reply_text(with_hint(t(lang, "ask_status_other"), lang))
        return ASK_STATUS_OTHER
    else:
        await query.message.reply_text(with_hint(t(lang, "ask_heard"), lang), reply_markup=heard_kb(lang))
        return ASK_HEARD

async def set_status_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
    lang = get_lang(context)
    code = match_status(update.message.text)
    if not code:
        msg = ("Por favor elige una opción o escribe: *Asilo pendiente*, *Asilado*, *TPS*, "
               "*Ciudadano* u *Otro*.")
        await update.message.reply_text(with_hint(msg, lang), reply_markup=status_kb(lang), parse_mode="Markdown")
        return ASK_STATUS
    mapping = {
        "pend_asy": t(lang, "status_pend_asy"),
        "asy": t(lang, "status_asy"),
        "tps": t(lang, "status_tps"),
        "cit": t(lang, "status_cit"),
        "other": t(lang, "status_other"),
    }
    context.user_data["lead_status"] = mapping.get(code, code)
    if code == "other":
        await update.message.reply_text(with_hint(t(lang, "ask_status_other"), lang))
        return ASK_STATUS_OTHER
    else:
        await update.message.reply_text(with_hint(t(lang, "ask_heard"), lang), reply_markup=heard_kb(lang))
        return ASK_HEARD

async def ask_status_other_done(update: Update, context: ContextTypes.DEFAULT_TYPE):
    context.user_data["lead_status_other"] = (update.message.text or "").strip()
    lang = get_lang(context)
    await update.message.reply_text(with_hint(t(lang, "ask_heard"), lang), reply_markup=heard_kb(lang))
    return ASK_HEARD

async def set_heard_cb(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    code = query.data  # heard_friend, heard_web...
    lang = get_lang(context)
    labels = {
        "heard_friend": t(lang, "heard_friend"),
        "heard_web": t(lang, "heard_web"),
        "heard_press": t(lang, "heard_press"),
        "heard_radio": t(lang, "heard_radio"),
        "heard_tv": t(lang, "heard_tv"),
        "heard_other": t(lang, "heard_other"),
    }
    if code == "heard_other":
        await query.message.reply_text(with_hint(t(lang, "ask_heard_other"), lang))
        return ASK_HEARD_OTHER
    context.user_data["lead_heard"] = labels.get(code, code)
    await _finalize_enroll_via_message(query.message, context)
    return ConversationHandler.END

async def set_heard_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
    lang = get_lang(context)
    code = match_heard(update.message.text)
    if code is None:
        context.user_data["lead_heard"] = (update.message.text or "").strip()
        return await _finalize_enroll_via_message(update.message, context)
    if code == "heard_other":
        await update.message.reply_text(with_hint(t(lang, "ask_heard_other"), lang))
        return ASK_HEARD_OTHER
    labels = {
        "heard_friend": t(lang, "heard_friend"),
        "heard_web": t(lang, "heard_web"),
        "heard_press": t(lang, "heard_press"),
        "heard_radio": t(lang, "heard_radio"),
        "heard_tv": t(lang, "heard_tv"),
    }
    context.user_data["lead_heard"] = labels.get(code, code)
    return await _finalize_enroll_via_message(update.message, context)

async def heard_other_done(update: Update, context: ContextTypes.DEFAULT_TYPE):
    context.user_data["lead_heard"] = (update.message.text or "").strip()
    return await _finalize_enroll_via_message(update.message, context)

def _save_lead(context: ContextTypes.DEFAULT_TYPE, chat, user):
    LEADS_FILE.parent.mkdir(parents=True, exist_ok=True)
    new = not LEADS_FILE.exists()
    data = {
        "timestamp": datetime.utcnow().isoformat(),
        "chat_id": chat.id if chat else "",
        "user_id": user.id if user else "",
        "username": getattr(user, "username", "") if user else "",
        "name": context.user_data.get("lead_name", ""),
        "phone": context.user_data.get("lead_phone", ""),
        "email": context.user_data.get("lead_email", ""),
        "country": context.user_data.get("lead_country", ""),
        "pref_lang": context.user_data.get("lead_pref_lang", ""),
        "internet": context.user_data.get("lead_internet", ""),
        "status": context.user_data.get("lead_status", ""),
        "status_other": context.user_data.get("lead_status_other", ""),
        "heard": context.user_data.get("lead_heard", "")
    }
    line = ",".join([f"\"{str(data[k]).replace('\"','\"\"')}\"" for k in data.keys()])
    header = ",".join(data.keys())
    with LEADS_FILE.open("a", encoding="utf-8", newline="") as f:
        if new:
            f.write(header + "\n")
        f.write(line + "\n")
    logging.info("Lead guardado en %s: %s", str(LEADS_FILE.resolve()), data)

def _build_summary(context: ContextTypes.DEFAULT_TYPE) -> str:
    return (
        "✅ Datos guardados:\n"
        f"• Nombre: {context.user_data.get('lead_name','')}\n"
        f"• Teléfono: {context.user_data.get('lead_phone','')}\n"
        f"• Email: {context.user_data.get('lead_email','')}\n"
        f"• País: {context.user_data.get('lead_country','')}\n"
        f"• Idioma: {context.user_data.get('lead_pref_lang','')}\n"
        f"• Internet: {context.user_data.get('lead_internet','')}\n"
        f"• Estatus: {context.user_data.get('lead_status','')}\n"
        f"• ¿Cómo supo?: {context.user_data.get('lead_heard','')}"
    )

async def _finalize_enroll_via_message(message, context: ContextTypes.DEFAULT_TYPE):
    _save_lead(context, message.chat, message.from_user)
    lang = get_lang(context)
    await message.reply_text(with_hint(_build_summary(context), lang))
    await message.reply_text(with_hint(t(lang, "enroll_thanks"), lang))
    context.chat_data["enroll_running"] = False
    return ConversationHandler.END

# ------------------------------- Respuesta libre --------------------------------
def t_address(lang: str) -> str:
    i18n = I18N.get(lang, {})
    return i18n.get("address", "80 Maiden Lane, 13th Floor, New York, NY 10028")

def t_programs(lang: str) -> str:
    i18n = I18N.get(lang, {})
    return i18n.get("programs_text", "")

def answer_from_faq(text: str, lang: str) -> str:
    i18n = I18N.get(lang, {})
    programs = t_programs(lang)
    addr = t_address(lang)
    map_label = i18n.get("open_map_label", "🗺️ Map")

    for m in MATCHERS[lang]:
        if m.pattern.search(text):
            ans = m.answer
            return (ans
                    .replace("{WEBSITE_URL}", CONFIG["website_url"])
                    .replace("{MAP_URL}", CONFIG["map_url"])
                    .replace("{MAP_LABEL}", map_label)
                    .replace("{ADDRESS}", addr)
                    .replace("{PROGRAMS}", programs))
    return t(lang, "no_match").replace("{WEBSITE_URL}", CONFIG["website_url"])

async def on_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
    lang = get_lang(context)
    user_text = update.message.text or ""
    reply = answer_from_faq(user_text, lang)
    await update.message.reply_text(with_hint(reply, lang), disable_web_page_preview=True, parse_mode="Markdown")

# ------------------------------- Error handler ----------------------------
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
    logging.exception("⚠️ Exception while handling an update:", exc_info=context.error)

# --------------------------------- Init + builder ----------------------------------
def init_runtime():
    global CONFIG, I18N, LANGS, MATCHERS
    CONFIG = load_config()

    discovered = discover_languages()
    if not discovered:
        discovered = {
            "en": {"name": "English", "i18n": {
                "name": "English",
                "menu": {
                    "programs": "📚 Programs", "schedule": "🗓️ Schedule", "location": "📍 Location",
                    "faq": "❓ FAQ", "site": "🌐 Website", "language": "🌐 Language", "enroll": "📝 I’m interested"
                },
                "strings": {
                    "welcome": "Hi! I’m the *International Center* assistant.",
                    "help": "Type your question (e.g., *schedule*, *apply*, *location*). Use /menu or /lang.",
                    "lang_set": "✅ Language updated.",
                    "no_match": "I couldn’t find an exact answer 🤔. Check {WEBSITE_URL} or use /menu.",
                    "choose_language": "Choose language:",
                    "enroll_intro": "📝 Please share your details to sign up for courses.",
                    "ask_name": "👤 Your full name:",
                    "ask_phone": "📱 Your phone (with country code if possible):",
                    "ask_email": "📧 Your email (or type *skip*):",
                    "ask_country": "🌎 What country are you from?",
                    "ask_lang": "🗣️ What language do you speak mainly?",
                    "ask_internet": "🌐 Do you have a stable Internet connection?",
                    "btn_yes": "Yes",
                    "btn_no": "No",
                    "ask_status": "🏷️ What is your status in the country?",
                    "status_pend_asy": "Asylum pending",
                    "status_asy": "Asylee",
                    "status_tps": "TPS",
                    "status_cit": "Citizen",
                    "status_other": "Other (specify)",
                    "ask_status_other": "✍️ Please specify your status:",
                    "ask_heard": "📣 How did you hear about us?",
                    "heard_friend": "A friend",
                    "heard_web": "Website",
                    "heard_press": "Press",
                    "heard_radio": "Radio",
                    "heard_tv": "TV",
                    "heard_other": "Other (specify)",
                    "ask_heard_other": "✍️ Tell us how you heard about us:",
                    "enroll_thanks": "✅ Thank you! We’ll contact you soon."
                },
                "programs_text": "📚 Programs. {WEBSITE_URL}",
                "schedule_text": "🗓️ See schedules on: {WEBSITE_URL}",
                "address": "80 Maiden Lane, 13th Floor, New York, NY 10028",
                "open_map_label": "🗺️ Open map",
                "faq_hint": "Type keywords like: *schedule*, *apply*, *location*, *cost*."
            }}
        }

    I18N = {code: data["i18n"] for code, data in discovered.items()}
    LANGS = {code: data["name"] for code, data in discovered.items()}
    MATCHERS = {code: build_matchers(code) for code in LANGS.keys()}

def build_application(token: str):
    """Crea el Application con TODA tu lógica y lo devuelve (sin arrancarlo)."""
    if not token:
        raise ValueError("Missing Telegram token in build_application(token)")

    # No configuramos logging global aquí por si el caller ya lo hizo.
    init_runtime()

    app = ApplicationBuilder().token(token).build()

    # ---- Conversación de registro de interesados (PRIMERO) ----
    conv = ConversationHandler(
        entry_points=[
            CommandHandler("enroll", enroll_cmd),
            CallbackQueryHandler(enroll_entry_cb, pattern="^enroll$")
        ],
        states={
            ASK_NAME:        [MessageHandler(filters.TEXT & ~filters.COMMAND, ask_phone)],
            ASK_PHONE:       [MessageHandler(filters.TEXT & ~filters.COMMAND, ask_email)],
            ASK_EMAIL:       [MessageHandler(filters.TEXT & ~filters.COMMAND, ask_country)],
            ASK_COUNTRY:     [MessageHandler(filters.TEXT & ~filters.COMMAND, ask_language)],
            ASK_LANG:        [MessageHandler(filters.TEXT & ~filters.COMMAND, ask_internet)],

            ASK_INTERNET: [
                CallbackQueryHandler(set_internet, pattern="^internet_(yes|no)$"),
                MessageHandler(filters.TEXT & ~filters.COMMAND, set_internet_text),
            ],
            ASK_STATUS: [
                CallbackQueryHandler(set_status, pattern="^status_"),
                MessageHandler(filters.TEXT & ~filters.COMMAND, set_status_text),
            ],
            ASK_STATUS_OTHER: [MessageHandler(filters.TEXT & ~filters.COMMAND, ask_status_other_done)],

            ASK_HEARD: [
                CallbackQueryHandler(set_heard_cb, pattern="^heard_"),
                MessageHandler(filters.TEXT & ~filters.COMMAND, set_heard_text),
            ],
            ASK_HEARD_OTHER: [MessageHandler(filters.TEXT & ~filters.COMMAND, heard_other_done)],
        ],
        fallbacks=[
            CommandHandler(
                "cancel",
                lambda u, c: (
                    c.chat_data.__setitem__("enroll_running", False),
                    u.message.reply_text(with_hint("Operación cancelada.", get_lang(c)))
                )[1]
            )
        ],
        allow_reentry=True
    )
    app.add_handler(conv)

    # ---- Handlers básicos ----
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("help", help_cmd))
    app.add_handler(CommandHandler("menu", menu_cmd))
    app.add_handler(CommandHandler("lang", lang_cmd))

    # ---- Callback genérico y texto libre ----
    app.add_handler(CallbackQueryHandler(on_callback))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_text))

    # ---- Error handler ----
    app.add_error_handler(error_handler)

    return app
