"""
WhatsApp State Machine — motor de estados para el flujo conversacional estructurado.

Reemplaza el flujo de AI libre por una máquina de estados determinista:
  idle → greeting/menu → [cotización | soporte | facturación]

Cada estado es una función registrada con el decorador @state(name).
"""

import re
import json
from datetime import datetime

# ─── Registro de estados ─────────────────────────────────────────────
_STATES = {}


def state(name):
    """Decorador para registrar un estado en la máquina de estados."""
    def decorator(func):
        _STATES[name] = func
        return func
    return decorator


class StateContext:
    """Contenedor de todo el contexto necesario para una transición."""
    def __init__(self, tenant_conn, master_conn, wa_config, tenant_id,
                 phone, customer_id=None, branch_id=None, media_kind='text',
                 media_base64=None, push_name=None):
        self.tenant_conn = tenant_conn
        self.master_conn = master_conn
        self.wa_config = wa_config
        self.tenant_id = tenant_id
        self.phone = phone
        self.customer_id = customer_id
        self.branch_id = branch_id
        self.media_kind = media_kind
        self.media_base64 = media_base64
        self.push_name = push_name


# ─── Punto de entrada principal ──────────────────────────────────────

def process_message(phone, text, current_state, state_data, context):
    """
    Procesar un mensaje entrante a través de la máquina de estados.

    Returns:
        (reply: str|None, next_state: str, next_state_data: dict)
    """
    handler = _STATES.get(current_state, _handle_unknown_state)
    return handler(text, state_data, context)


def _handle_unknown_state(text, state_data, ctx):
    """Fallback para estados corruptos o no registrados."""
    return _MSG_UNKNOWN_STATE, 'menu', state_data


# ─── Mensajes globales ───────────────────────────────────────────────

_MSG_UNKNOWN_STATE = (
    "Parece que hubo un problema. Volvamos al inicio. 🤔\n\n"
    "¿En qué puedo ayudarte?\n\n"
    "1️⃣ *Cotizar* refacciones\n"
    "2️⃣ *Soporte* — Hablar con la sucursal\n"
    "3️⃣ *Facturación* — Consultar o generar facturas\n\n"
    "Responde con el número de la opción."
)

_MSG_MENU = (
    "¿En qué puedo ayudarte hoy?\n\n"
    "1️⃣ *Cotizar* refacciones\n"
    "2️⃣ *Soporte* — Hablar con la sucursal\n"
    "3️⃣ *Facturación* — Consultar o generar facturas\n\n"
    "Responde con el número de la opción que necesites."
)

_MSG_MENU_INVALID = (
    "No entendí tu selección. 🤔\n\n"
    "Por favor responde con:\n"
    "• *1* para Cotizar\n"
    "• *2* para Soporte\n"
    "• *3* para Facturación"
)

_MSG_GREETING_KNOWN = "¡Hola {name}! 👋\nBienvenido a *{branch}*."
_MSG_GREETING_UNKNOWN = "¡Hola! 👋\nBienvenido a *{branch}*. Soy tu asistente virtual."

_MSG_IDENTIFY_PROMPT = "Para darte una mejor atención, ¿podrías indicarme tu *nombre* o *número de cliente*?"
_MSG_IDENTIFY_FOUND = "¡Perfecto! Te encontré en el sistema, {name}. ✅\n\nContinuamos con tu cotización."
_MSG_IDENTIFY_MULTIPLE = "Encontré varios clientes con datos similares:\n\n{options}\n\nResponde con el número del cliente correcto."
_MSG_IDENTIFY_NOT_FOUND = (
    "No te encontré en nuestro sistema. 😕\n\n"
    "¿Te gustaría registrarte para agilizar futuras compras?\n"
    "Responde *sí* para registrarte o *no* para continuar como invitado."
)

_MSG_QUOTE_SEARCH_PROMPT = (
    "¿Qué refacción estás buscando? 🔧\n\n"
    "Puedes decirme algo como:\n"
    "• \"Balatas para Tsuru 2015\"\n"
    "• \"Filtro de aceite Bosch\"\n"
    "• O envíame una *foto* de la pieza"
)

_MSG_EXPLORE_OFFER = (
    "🔍 Estoy buscando en nuestros catálogos técnicos...\n\n"
    "¿Te refieres a *{suggestion}*?\n\n"
    "Responde *sí* para confirmar o *no* para que busque otra opción."
)
_MSG_EXPLORE_RETRY = "Entendido. Déjame buscar otra alternativa... 🔎"
_MSG_EXPLORE_ERROR = (
    "Estoy teniendo problemas para identificar la pieza. 😕\n\n"
    "¿Podrías darme más detalles? Por ejemplo: marca del carro, modelo, año, "
    "o el síntoma que presenta."
)
_MSG_EXPLORE_CONFIRM_PROMPT = "¿Confirmas que es esta pieza? Responde *sí* o *no*."

_MSG_LEARNING_TRANSFER = (
    "No logré identificar la pieza con certeza después de explorar varias opciones. 😔\n\n"
    "He registrado tu búsqueda para mejorar nuestro servicio en el futuro.\n\n"
    "Te sugiero llamar directamente a la sucursal para que un asesor especializado te ayude:\n\n"
    "📞 *{phone}*\n\n"
    "Ellos podrán orientarte mejor con tu descripción."
)

_MSG_DELIVERY_PROMPT = "¿Deseas que te enviemos el pedido a domicilio? 🚚\n\nResponde *sí* o *no*."
_MSG_DELIVERY_ADDRESS_CONFIRM = (
    "¿Tu dirección de envío es la siguiente?\n\n"
    "📍 {address}\n\n"
    "Responde *sí* para confirmar o escribe la nueva dirección."
)
_MSG_DELIVERY_ADDRESS_REQUEST = (
    "¿A qué dirección te gustaría recibir el envío? 📍\n\n"
    "Por favor escribe la dirección completa incluyendo calle, número, colonia y código postal."
)
_MSG_DELIVERY_ADDRESS_SAVED = (
    "✅ Dirección registrada:\n\n"
    "📍 {address}\n\n"
    "Continuamos con tu cotización."
)

_MSG_SUPPORT = (
    "📞 *Soporte directo*\n\n"
    "Para una atención más personalizada, llámanos directamente:\n\n"
    "*{phone}*\n\n"
    "Horario: Lunes a Viernes 8:00 - 18:00, Sábados 8:00 - 14:00\n\n"
    "Un asesor especializado te atenderá con gusto."
)

_MSG_INVOICE_FISCAL_PROMPT = (
    "Para generar tu factura necesito tus datos fiscales:\n\n"
    "1️⃣ RFC\n"
    "2️⃣ Razón social\n"
    "3️⃣ Uso CFDI (ej. G03)\n"
    "4️⃣ Código postal\n\n"
    "Puedes enviarme los datos en un solo mensaje. Ejemplo:\n\n"
    "RFC: ABCD010101A12\n"
    "Razón social: Mi Empresa S.A. de C.V.\n"
    "Uso CFDI: G03\n"
    "CP: 45130"
)

_MSG_ERROR_RETRY = (
    "Algo salió mal. Volvamos a intentarlo.\n\n"
    "¿Podrías indicarme tu *nombre* o *número de cliente*?"
)


# ─── Estados ─────────────────────────────────────────────────────────

@state('idle')
def handle_idle(text, state_data, ctx):
    """Estado inicial: saludo + menú."""
    from services.wa_customer import get_linked_customer, get_customer_by_id

    customer_id = get_linked_customer(ctx.phone, ctx.tenant_conn)
    branch_name = _get_branch_name(ctx.tenant_conn, ctx.branch_id)

    if customer_id:
        customer = get_customer_by_id(ctx.tenant_conn, customer_id)
        first_name = customer['name'].split()[0] if customer and customer.get('name') else 'Cliente'
        greeting = _MSG_GREETING_KNOWN.format(name=first_name, branch=branch_name)
    else:
        greeting = _MSG_GREETING_UNKNOWN.format(branch=branch_name)

    reply = greeting + "\n\n" + _MSG_MENU
    next_data = {'customer_id': customer_id, 'branch_name': branch_name}
    return reply, 'menu', next_data


@state('menu')
def handle_menu(text, state_data, ctx):
    """Esperar selección de menú (1, 2, 3)."""
    if not text:
        return _MSG_MENU_INVALID, 'menu', state_data

    t = text.strip().lower()

    if t in ('1', 'uno', 'cotizar', 'cotización', 'cotizacion', 'refacciones', 'partes'):
        return None, 'quote_identify', state_data

    if t in ('2', 'dos', 'soporte', 'ayuda', 'teléfono', 'telefono', 'llamar'):
        return None, 'support_phone', state_data

    if t in ('3', 'tres', 'facturación', 'facturacion', 'factura', 'cfdi'):
        return None, 'invoice_identify', state_data

    # Fallback: si parece descripción de parte (no saludo), redirigir a cotización
    if len(t) > 10 and not _is_greeting(t):
        state_data['preloaded_search'] = text.strip()
        return (
            "Entendido, parece que buscas una refacción. Te ayudo con eso. 🔧\n\n",
            'quote_identify',
            state_data
        )

    return _MSG_MENU_INVALID, 'menu', state_data


@state('support_phone')
def handle_support_phone(text, state_data, ctx):
    """Proporcionar teléfono de sucursal."""
    phone = _get_branch_phone(ctx.tenant_conn, ctx.branch_id)
    reply = _MSG_SUPPORT.format(phone=phone)
    return reply, 'support_done', state_data


@state('support_done')
def handle_support_done(text, state_data, ctx):
    """Estado de cierre, permite volver al menú."""
    if not text:
        return (
            "¿Hay algo más en lo que pueda ayudarte?\n\n"
            "Escribe *menú* para volver al inicio o indícame qué necesitas.",
            'support_done', state_data
        )

    t = text.strip().lower()

    if t in ('menu', 'menú', 'inicio', 'principal', 'opciones', 'volver', 'regresar'):
        return _MSG_MENU, 'menu', state_data

    if t in ('1', 'uno', 'cotizar'):
        return None, 'quote_identify', state_data

    if t in ('2', 'dos', 'soporte'):
        return None, 'support_phone', state_data

    if t in ('3', 'tres', 'facturación', 'facturacion'):
        return None, 'invoice_identify', state_data

    return (
        "¿Hay algo más en lo que pueda ayudarte?\n\n"
        "Escribe *menú* para volver al inicio o indícame qué necesitas.",
        'support_done',
        state_data
    )


# ─── Estados de Cotización ───────────────────────────────────────────

@state('quote_identify')
def handle_quote_identify(text, state_data, ctx):
    """Identificar cliente antes de cotizar."""
    from services.wa_customer import search_customers, link_wa_customer

    if state_data.get('customer_id'):
        if state_data.get('preloaded_search'):
            customer = _get_customer_by_id(ctx.tenant_conn, state_data['customer_id'])
            first_name = customer['name'].split()[0] if customer and customer.get('name') else 'Cliente'
            reply = f"Perfecto {first_name}, busco eso para ti... 🔍"
            return reply, 'quote_search', state_data
        return None, 'quote_search', state_data

    if text and not state_data.get('identify_prompted'):
        customers = search_customers(text.strip(), ctx.tenant_conn)

        if len(customers) == 1:
            state_data['customer_id'] = customers[0]['id']
            link_wa_customer(ctx.phone, customers[0]['id'], ctx.tenant_conn)
            return _MSG_IDENTIFY_FOUND.format(name=customers[0]['name']), 'quote_search', state_data

        if len(customers) > 1:
            state_data['customer_candidates'] = customers
            state_data['identify_prompted'] = True
            options = '\n'.join([f"{i+1}. {c['name']} (#{c['id']})" for i, c in enumerate(customers[:5])])
            return _MSG_IDENTIFY_MULTIPLE.format(options=options), 'quote_select_customer', state_data

        state_data['identify_prompted'] = True
        # Si el texto es solo dígitos (ej: selección de menú), no usarlo como nombre propuesto
        if text.strip().isdigit():
            state_data.pop('proposed_name', None)
        else:
            state_data['proposed_name'] = text.strip()
        return _MSG_IDENTIFY_NOT_FOUND, 'quote_register_new', state_data

    state_data['identify_prompted'] = True
    return _MSG_IDENTIFY_PROMPT, 'quote_identify', state_data


@state('quote_select_customer')
def handle_quote_select_customer(text, state_data, ctx):
    """Seleccionar entre múltiples clientes candidatos."""
    from services.wa_customer import link_wa_customer

    candidates = state_data.get('customer_candidates', [])
    if not candidates:
        return _MSG_ERROR_RETRY, 'quote_identify', {'identify_prompted': True}

    try:
        selection = int(text.strip())
        if 1 <= selection <= len(candidates):
            chosen = candidates[selection - 1]
            state_data['customer_id'] = chosen['id']
            del state_data['customer_candidates']
            link_wa_customer(ctx.phone, chosen['id'], ctx.tenant_conn)
            return _MSG_IDENTIFY_FOUND.format(name=chosen['name']), 'quote_search', state_data
    except (ValueError, IndexError):
        pass

    return f"Por favor responde con un número del 1 al {len(candidates)}.", 'quote_select_customer', state_data


@state('quote_register_new')
def handle_quote_register_new(text, state_data, ctx):
    """Registrar cliente nuevo o continuar como invitado."""
    from services.wa_customer import create_customer, link_wa_customer

    t = text.strip().lower()

    if t in ('si', 'sí', 'yes', 'ok', 'dale', 'va'):
        # Si estamos esperando el nombre real del usuario
        if state_data.get('awaiting_name'):
            name = text.strip()
            if len(name) < 2:
                return "Por favor proporciona un nombre válido (más de 2 caracteres).", 'quote_register_new', state_data
            state_data.pop('awaiting_name', None)
            customer_id = create_customer(ctx.tenant_conn, phone=ctx.phone, name=name)
            if customer_id:
                state_data['customer_id'] = customer_id
                link_wa_customer(ctx.phone, customer_id, ctx.tenant_conn)
            return f"¡Listo! Te registré como *{name}*. ✅\n\nContinuamos con tu cotización.", 'quote_search', state_data

        name = state_data.get('proposed_name', '').strip()
        # Si no hay nombre propuesto o parece ser un número de menú, pedir el nombre
        if not name or name.isdigit():
            state_data['awaiting_name'] = True
            return "¡Genial! ¿Cuál es tu *nombre completo*?", 'quote_register_new', state_data

        customer_id = create_customer(ctx.tenant_conn, phone=ctx.phone, name=name)
        if customer_id:
            state_data['customer_id'] = customer_id
            link_wa_customer(ctx.phone, customer_id, ctx.tenant_conn)
        return f"¡Listo! Te registré como *{name}*. ✅\n\nContinuamos con tu cotización.", 'quote_search', state_data

    if t in ('no', 'nope', 'nah', 'paso'):
        state_data['customer_id'] = None
        return (
            "De acuerdo, continuamos como invitado. 🎭\n\n"
            "Puedes registrarte en cualquier momento si lo deseas.",
            'quote_search', state_data
        )

    return "¿Te gustaría registrarte? Responde *sí* o *no*.", 'quote_register_new', state_data


@state('quote_search')
def handle_quote_search(text, state_data, ctx):
    """Recibir descripción de la pieza."""
    search_text = state_data.pop('preloaded_search', None) or text

    if not search_text or len(search_text.strip()) < 2:
        return _MSG_QUOTE_SEARCH_PROMPT, 'quote_search', state_data

    search_text = search_text.strip()
    state_data['search_description'] = search_text

    try:
        from services.ai_chat import chat, chat_with_image
        if ctx.media_kind == 'image' and ctx.media_base64:
            ai_resp = chat_with_image(
                user_message=search_text if search_text != '(imagen)' else 'Identifica esta parte automotriz y sugiere terminos de busqueda.',
                image_base64=ctx.media_base64,
                conversation_history=[],
                inventory_context=None,
            )
        else:
            ai_resp = chat(search_text, conversation_history=[], inventory_context=None)
        search_query = ai_resp.get('search_query')
        vehicle = ai_resp.get('vehicle')

        if vehicle and vehicle.get('brand'):
            state_data['vehicle'] = vehicle
            from services.wa_quotation import set_vehicle
            set_vehicle(ctx.tenant_conn, ctx.phone, vehicle)

        confidence = _assess_confidence(ai_resp, search_text)
        state_data['ai_confidence'] = confidence
        state_data['ai_search_query'] = search_query

        if confidence >= 0.7:
            return None, 'quote_inventory_check', state_data
        else:
            state_data['learning_cycle'] = 1
            return None, 'quote_explore', state_data

    except Exception as e:
        print(f"[WA-SM] AI error in quote_search: {e}")
        state_data['ai_search_query'] = search_text
        return None, 'quote_inventory_check', state_data


@state('quote_explore')
def handle_quote_explore(text, state_data, ctx):
    """Ruta de exploración cuando la AI no está segura."""
    cycle = state_data.get('learning_cycle', 1)

    if not state_data.get('explore_offered'):
        description = state_data.get('search_description', '')
        explore_prompt = _build_explore_prompt(description, state_data.get('vehicle'))

        try:
            from services.ai_chat import chat
            ai_resp = chat(explore_prompt, conversation_history=[], inventory_context=None)
            suggested_part = ai_resp.get('message', '')
            search_query = ai_resp.get('search_query', '')

            state_data['explore_offered'] = True
            state_data['explore_suggestion'] = suggested_part
            state_data['explore_search_query'] = search_query

            offered = state_data.get('offered_parts', [])
            offered.append({
                'cycle': cycle,
                'suggestion': suggested_part,
                'search_query': search_query,
                'timestamp': datetime.now().isoformat()
            })
            state_data['offered_parts'] = offered

            return _MSG_EXPLORE_OFFER.format(suggestion=suggested_part), 'quote_explore', state_data

        except Exception as e:
            print(f"[WA-SM] Explore AI error: {e}")
            return _MSG_EXPLORE_ERROR, 'quote_search', state_data

    t = text.strip().lower() if text else ''

    if t in ('si', 'sí', 'yes', 'ese', 'ese mero', 'correcto', 'va', 'dale'):
        state_data['ai_search_query'] = state_data.get('explore_search_query', '')
        state_data.pop('explore_offered', None)
        state_data.pop('explore_suggestion', None)
        return None, 'quote_inventory_check', state_data

    if t in ('no', 'nope', 'nah', 'otro', 'diferente'):
        if cycle < 2:
            state_data['learning_cycle'] = cycle + 1
            state_data.pop('explore_offered', None)
            state_data.pop('explore_suggestion', None)
            return _MSG_EXPLORE_RETRY, 'quote_explore', state_data
        else:
            return _trigger_learning_route(state_data, ctx)

    return _MSG_EXPLORE_CONFIRM_PROMPT, 'quote_explore', state_data


@state('quote_inventory_check')
def handle_quote_inventory_check(text, state_data, ctx):
    """Buscar en inventario y mostrar resultados."""
    if not state_data.get('inventory_shown'):
        search_query = state_data.get('ai_search_query', '')
        vehicle = state_data.get('vehicle')

        enrichment, found_part = _enrich_wa_reply_with_part(
            search_query, vehicle, ctx.tenant_conn, ctx.master_conn
        )

        state_data['inventory_shown'] = True
        state_data['last_found_part'] = found_part

        if enrichment:
            if found_part:
                from services.wa_quotation import set_last_shown_part
                set_last_shown_part(ctx.tenant_conn, ctx.phone, found_part)
            reply = (
                "Aquí está lo que encontré en nuestro inventario:\n\n"
                f"{enrichment}\n\n"
                "¿Son las refacciones que necesitas?\n"
                "Responde *sí* para continuar o *no* para buscar otra."
            )
            return reply, 'quote_inventory_check', state_data
        else:
            reply = (
                "No tengo esa pieza exacta en stock actualmente. 😕\n\n"
                "¿Te gustaría que busque una alternativa similar o prefieres que la ordene por encargo?\n\n"
                "Responde *sí* para ver alternativas o *no* para regresar."
            )
            state_data['inventory_empty'] = True
            return reply, 'quote_inventory_check', state_data

    t = text.strip().lower() if text else ''

    if t in ('si', 'sí', 'yes', 'va', 'dale', 'correcto'):
        if state_data.get('inventory_empty'):
            # Si ya exploramos alternativas (quote_explore) y aún no hay stock,
            # no entrar en loop infinito — transferir a soporte.
            if state_data.get('offered_parts') and len(state_data['offered_parts']) > 0:
                return _trigger_learning_route(state_data, ctx)

            state_data.pop('inventory_shown', None)
            state_data.pop('inventory_empty', None)
            # Resetear flags de explore para que quote_explore genere una nueva sugerencia
            state_data.pop('explore_offered', None)
            state_data.pop('explore_suggestion', None)
            # Silent transition: el loop en whatsapp_bp.py ejecutará quote_explore
            # inmediatamente y generará la sugerencia sin esperar otro mensaje del usuario
            return None, 'quote_explore', state_data

        found_part = state_data.get('last_found_part')
        if found_part:
            state_data['confirmed_parts'] = state_data.get('confirmed_parts', [])
            state_data['confirmed_parts'].append(found_part)
        return None, 'quote_delivery_check', state_data

    if t in ('no', 'nope', 'nah', 'otro', 'diferente'):
        state_data.pop('inventory_shown', None)
        state_data.pop('last_found_part', None)
        state_data.pop('inventory_empty', None)
        return "De acuerdo. ¿Qué otra refacción estás buscando?", 'quote_search', state_data

    return "¿Confirmas que son estas refacciones? Responde *sí* o *no*.", 'quote_inventory_check', state_data


@state('quote_delivery_check')
def handle_quote_delivery_check(text, state_data, ctx):
    """Determinar si el cliente quiere envío a domicilio."""
    if not state_data.get('delivery_checked'):
        has_delivery = _check_branch_delivery(ctx.tenant_conn, ctx.branch_id)
        state_data['delivery_checked'] = True
        state_data['has_delivery'] = has_delivery

        if not has_delivery:
            return None, 'quote_emit', state_data

        return _MSG_DELIVERY_PROMPT, 'quote_delivery_check', state_data

    t = text.strip().lower() if text else ''

    if t in ('si', 'sí', 'yes', 'va', 'dale'):
        state_data['wants_delivery'] = True
        return None, 'quote_delivery_address', state_data

    if t in ('no', 'nope', 'nah', 'paso', 'recojo'):
        state_data['wants_delivery'] = False
        return None, 'quote_emit', state_data

    return "¿Deseas envío a domicilio? Responde *sí* o *no*.", 'quote_delivery_check', state_data


@state('quote_delivery_address')
def handle_quote_delivery_address(text, state_data, ctx):
    """Obtener o confirmar dirección de envío."""
    from services.wa_customer import get_customer_address, update_customer_address

    customer_id = state_data.get('customer_id')

    if not state_data.get('address_prompted'):
        state_data['address_prompted'] = True

        if customer_id:
            address = get_customer_address(ctx.tenant_conn, customer_id)
            if address:
                state_data['saved_address'] = address
                return _MSG_DELIVERY_ADDRESS_CONFIRM.format(address=address), 'quote_delivery_address', state_data

        return _MSG_DELIVERY_ADDRESS_REQUEST, 'quote_delivery_address', state_data

    t = text.strip().lower() if text else ''

    if state_data.get('saved_address') and t in ('si', 'sí', 'yes', 'correcto', 'va'):
        state_data['delivery_address'] = state_data['saved_address']
        return None, 'quote_emit', state_data

    if text and len(text.strip()) > 5:
        state_data['delivery_address'] = text.strip()
        if customer_id:
            update_customer_address(ctx.tenant_conn, customer_id, text.strip())
        return _MSG_DELIVERY_ADDRESS_SAVED.format(address=text.strip()), 'quote_emit', state_data

    if state_data.get('saved_address'):
        return "Responde *sí* para confirmar la dirección o escribe la nueva dirección.", 'quote_delivery_address', state_data

    return "Por favor proporciona una dirección válida para el envío.", 'quote_delivery_address', state_data


@state('quote_emit')
def handle_quote_emit(text, state_data, ctx):
    """Crear/actualizar cotización y presentarla."""
    if not state_data.get('quote_emitted'):
        state_data['quote_emitted'] = True

        confirmed_parts = state_data.get('confirmed_parts', [])
        if not confirmed_parts:
            return "No tienes refacciones seleccionadas. 😕\n\n¿Qué estás buscando?", 'quote_search', state_data

        from services.wa_quotation import (
            get_open_quotation, create_quotation, add_item_to_quotation,
            get_quotation_detail, format_quotation_wa
        )

        qid = get_open_quotation(ctx.tenant_conn, ctx.phone)
        if not qid:
            qid = create_quotation(ctx.tenant_conn, ctx.phone)

        customer_id = state_data.get('customer_id')
        if customer_id:
            cur = ctx.tenant_conn.cursor()
            cur.execute(
                "UPDATE quotations SET customer_id = %s WHERE id = %s",
                (customer_id, qid)
            )
            ctx.tenant_conn.commit()
            cur.close()

        for part in confirmed_parts:
            add_item_to_quotation(ctx.tenant_conn, qid, part, quantity=1)

        if state_data.get('wants_delivery') and state_data.get('delivery_address'):
            cur = ctx.tenant_conn.cursor()
            cur.execute(
                "UPDATE quotations SET notes = CONCAT(COALESCE(notes,''), ' | DELIVERY: ', %s) WHERE id = %s",
                (state_data['delivery_address'], qid)
            )
            ctx.tenant_conn.commit()
            cur.close()

        detail = get_quotation_detail(ctx.tenant_conn, qid)
        quote_text = format_quotation_wa(detail)

        reply = (
            f"{quote_text}\n\n"
            "¿Qué deseas hacer?\n"
            "• Escribe *agregar* para más productos\n"
            "• Escribe *enviar* para recibir tu cotización por imagen\n"
            "• Escribe *confirmar* para hacer el pedido\n"
            "• Escribe *limpiar* para empezar de nuevo"
        )
        return reply, 'quote_command_created', state_data

    return None, 'quote_command_created', state_data


@state('quote_command_created')
def handle_quote_command_created(text, state_data, ctx):
    """Escuchar comandos sobre la cotización existente."""
    t = text.strip().lower() if text else ''

    if t in ('agregar', 'más', 'mas', 'otro', 'otra', 'seguir'):
        state_data.pop('inventory_shown', None)
        state_data.pop('last_found_part', None)
        state_data.pop('inventory_empty', None)
        state_data.pop('quote_emitted', None)
        return "¿Qué otra refacción necesitas?", 'quote_search', state_data

    if t in ('enviar', 'mandar', 'pdf', 'imagen', 'foto'):
        try:
            from services.wa_quotation import get_open_quotation, get_quotation_detail
            from services.quote_image import generate_quote_image
            from services.whatsapp_service import send_image

            qid = get_open_quotation(ctx.tenant_conn, ctx.phone)
            if qid:
                detail = get_quotation_detail(ctx.tenant_conn, qid)
                quote_items = [{
                    'name': it.get('name', ''),
                    'sku': it.get('part_number', ''),
                    'qty': it.get('quantity', 1),
                    'price': float(it.get('unit_price', 0)),
                    'total': float(it.get('total', 0)),
                } for it in detail.get('items', [])]
                totals = {
                    'subtotal': float(detail.get('subtotal', 0)),
                    'tax': float(detail.get('tax_total', 0)),
                    'total': float(detail.get('total', 0)),
                }
                tenant_name = _get_tenant_business_name(ctx.tenant_conn)
                b64_img = generate_quote_image(quote_items, totals, tenant_name=tenant_name)
                img_result = send_image(
                    ctx.phone,
                    caption="Aquí está tu cotización 👇",
                    base64_image=b64_img,
                    bridge_url=ctx.wa_config.get('bridge_url')
                )
                if img_result.get('success'):
                    return (
                        "📎 *Te envié tu cotización en imagen.*\n\n"
                        "¿Qué deseas hacer?\n"
                        "• *agregar* — más productos\n"
                        "• *confirmar* — hacer el pedido\n"
                        "• *limpiar* — empezar de nuevo",
                        'quote_command_created',
                        state_data
                    )
        except Exception as e:
            print(f"[WA-SM] Quote image failed: {e}")

        from services.wa_quotation import get_open_quotation, get_quotation_detail, format_quotation_wa
        qid = get_open_quotation(ctx.tenant_conn, ctx.phone)
        if qid:
            detail = get_quotation_detail(ctx.tenant_conn, qid)
            return format_quotation_wa(detail), 'quote_command_created', state_data

    if t in ('confirmar', 'confirmo', 'acepto', 'si', 'sí', 'va', 'ordenar', 'pedir'):
        from services.wa_quotation import confirm_quotation
        qid = confirm_quotation(ctx.tenant_conn, ctx.phone)
        if qid:
            reply = (
                "✅ *¡Pedido confirmado!*\n\n"
                "Tu cotización fue registrada.\n"
                "Nos pondremos en contacto contigo para coordinar la entrega.\n\n"
                "¡Gracias por tu compra! 🙏"
            )
            clean_data = {
                'customer_id': state_data.get('customer_id'),
                'branch_name': state_data.get('branch_name'),
            }
            return reply, 'support_done', clean_data

    if t in ('limpiar', 'borrar', 'nueva', 'cancelar', 'reset'):
        from services.wa_quotation import clear_quotation
        clear_quotation(ctx.tenant_conn, ctx.phone)
        clean_data = {
            'customer_id': state_data.get('customer_id'),
            'branch_name': state_data.get('branch_name'),
        }
        return (
            "🗑️ Cotización limpiada.\n\n"
            "¿Qué refacción estás buscando?",
            'quote_search',
            clean_data
        )

    return (
        "No entendí. Puedes escribir:\n"
        "• *agregar* — para más productos\n"
        "• *enviar* — para recibir imagen\n"
        "• *confirmar* — para hacer el pedido\n"
        "• *limpiar* — para empezar de nuevo",
        'quote_command_created',
        state_data
    )


# ─── Estados de Facturación ──────────────────────────────────────────

@state('invoice_identify')
def handle_invoice_identify(text, state_data, ctx):
    """Identificar cliente antes de buscar facturas."""
    from services.wa_customer import search_customers, link_wa_customer

    if state_data.get('customer_id'):
        return None, 'invoice_search', state_data

    if not state_data.get('invoice_identify_prompted'):
        state_data['invoice_identify_prompted'] = True
        return "Para buscar tus facturas, ¿podrías indicarme tu *nombre* o *número de cliente*?", 'invoice_identify', state_data

    if text:
        customers = search_customers(text.strip(), ctx.tenant_conn)

        if len(customers) == 1:
            state_data['customer_id'] = customers[0]['id']
            link_wa_customer(ctx.phone, customers[0]['id'], ctx.tenant_conn)
            return f"¡Perfecto, {customers[0]['name']}! Buscando tus facturas... 📋", 'invoice_search', state_data

        if len(customers) > 1:
            state_data['invoice_customer_candidates'] = customers
            options = '\n'.join([f"{i+1}. {c['name']} (#{c['id']})" for i, c in enumerate(customers[:5])])
            return f"Encontré varios clientes:\n\n{options}\n\nResponde con el número correcto.", 'invoice_select_customer', state_data

        return (
            "No te encontré en el sistema. Sin un cliente registrado no puedo buscar facturas existentes.\n\n"
            "Si necesitas generar una factura nueva, puedo ayudarte a capturar tus datos fiscales.\n"
            "Responde *nueva* para generar una factura nueva o *menu* para volver.",
            'invoice_no_customer',
            state_data
        )

    return "Por favor indícame tu nombre o número de cliente.", 'invoice_identify', state_data


@state('invoice_select_customer')
def handle_invoice_select_customer(text, state_data, ctx):
    """Seleccionar cliente entre múltiples candidatos en facturación."""
    from services.wa_customer import link_wa_customer

    candidates = state_data.get('invoice_customer_candidates', [])
    try:
        selection = int(text.strip())
        if 1 <= selection <= len(candidates):
            chosen = candidates[selection - 1]
            state_data['customer_id'] = chosen['id']
            del state_data['invoice_customer_candidates']
            link_wa_customer(ctx.phone, chosen['id'], ctx.tenant_conn)
            return f"¡Perfecto, {chosen['name']}! Buscando tus facturas...", 'invoice_search', state_data
    except (ValueError, IndexError):
        pass

    return f"Responde con un número del 1 al {len(candidates)}.", 'invoice_select_customer', state_data


@state('invoice_no_customer')
def handle_invoice_no_customer(text, state_data, ctx):
    """Cliente no encontrado en facturación."""
    t = text.strip().lower() if text else ''

    if t in ('nueva', 'nueva factura', 'generar', 'sí', 'si', 'ok'):
        return None, 'invoice_capture_fiscal', state_data

    if t in ('menu', 'menú', 'inicio', 'volver'):
        return _MSG_MENU, 'menu', state_data

    return (
        "¿Qué deseas hacer?\n"
        "• *nueva* — Generar factura nueva\n"
        "• *menú* — Volver al inicio",
        'invoice_no_customer',
        state_data
    )


@state('invoice_search')
def handle_invoice_search(text, state_data, ctx):
    """Buscar facturas del cliente."""
    customer_id = state_data.get('customer_id')
    if not customer_id:
        return "Error: cliente no identificado.", 'invoice_identify', state_data

    cur = ctx.tenant_conn.cursor()
    cur.execute("""
        SELECT s.id, s.created_at, s.total,
               (SELECT COUNT(*) FROM sale_items WHERE sale_id = s.id) as item_count
        FROM sales s
        WHERE s.customer_id = %s AND s.status = 'completed'
        ORDER BY s.created_at DESC
        LIMIT 5
    """, (customer_id,))
    rows = cur.fetchall()
    cur.close()

    if not rows:
        return (
            "No encontré facturas de compras a tu nombre. 😕\n\n"
            "Si necesitas generar una factura para una compra reciente, "
            "puedo ayudarte a capturar tus datos fiscales y el ticket.\n\n"
            "Responde *sí* para continuar o *menú* para volver.",
            'invoice_capture_fiscal',
            state_data
        )

    lines = ["📋 *Tus facturas recientes:*\n"]
    for i, row in enumerate(rows, 1):
        sale_id, created_at, total, item_count = row
        if hasattr(created_at, 'strftime'):
            date_str = created_at.strftime('%d/%m/%Y')
        else:
            date_str = str(created_at)[:10]
        lines.append(f"{i}. Folio #{sale_id} — ${float(total):,.2f} — {date_str} ({item_count} artículos)")

    lines.append("\n6. *Otra* — Indicar cuál necesito")
    lines.append("\nResponde con el número de la factura que necesitas.")

    state_data['invoice_sales'] = [{
        'id': r[0], 'created_at': str(r[1]), 'total': float(r[2]), 'item_count': r[3]
    } for r in rows]

    return '\n'.join(lines), 'invoice_select', state_data


@state('invoice_select')
def handle_invoice_select(text, state_data, ctx):
    """Cliente selecciona una factura de la lista."""
    sales = state_data.get('invoice_sales', [])
    t = text.strip().lower() if text else ''

    if t in ('6', 'otra', 'otro', 'otra factura', 'buscar'):
        return "Indícame el folio, fecha aproximada o monto de la factura que necesitas.", 'invoice_custom_search', state_data

    try:
        selection = int(t)
        if 1 <= selection <= len(sales):
            sale = sales[selection - 1]
            state_data['selected_sale_id'] = sale['id']
            return _process_invoice_send(sale['id'], state_data, ctx)
    except (ValueError, IndexError):
        pass

    return (
        f"Por favor responde con un número del 1 al {len(sales)}, o *6* para buscar otra.",
        'invoice_select',
        state_data
    )


@state('invoice_custom_search')
def handle_invoice_custom_search(text, state_data, ctx):
    """Buscar factura por criterios alternativos."""
    if not text or len(text.strip()) < 2:
        return "Indícame el folio, fecha (dd/mm/aaaa) o monto de la factura.", 'invoice_custom_search', state_data

    criteria = text.strip()
    customer_id = state_data.get('customer_id')

    cur = ctx.tenant_conn.cursor()

    # 1. Buscar por ID exacto
    try:
        sale_id = int(criteria)
        cur.execute(
            "SELECT id, created_at, total FROM sales WHERE id = %s AND customer_id = %s AND status = 'completed'",
            (sale_id, customer_id)
        )
        row = cur.fetchone()
        if row:
            cur.close()
            state_data['selected_sale_id'] = row[0]
            return _process_invoice_send(row[0], state_data, ctx)
    except ValueError:
        pass

    # 2. Buscar por fecha
    for fmt in ('%d/%m/%Y', '%d-%m-%Y', '%Y-%m-%d'):
        try:
            from datetime import datetime
            date_obj = datetime.strptime(criteria, fmt)
            cur.execute(
                "SELECT id, created_at, total FROM sales WHERE DATE(created_at) = %s AND customer_id = %s AND status = 'completed' LIMIT 5",
                (date_obj.date(), customer_id)
            )
            rows = cur.fetchall()
            if len(rows) == 1:
                cur.close()
                state_data['selected_sale_id'] = rows[0][0]
                return _process_invoice_send(rows[0][0], state_data, ctx)
            elif len(rows) > 1:
                cur.close()
                lines = ["Encontré varias facturas en esa fecha:\n"]
                for i, r in enumerate(rows, 1):
                    lines.append(f"{i}. Folio #{r[0]} — ${float(r[2]):,.2f}")
                lines.append("\nResponde con el número.")
                state_data['invoice_custom_results'] = [{'id': r[0]} for r in rows]
                return '\n'.join(lines), 'invoice_custom_results', state_data
        except ValueError:
            continue

    # 3. Buscar por monto aproximado
    try:
        amount = float(criteria.replace(',', '').replace('$', ''))
        cur.execute(
            "SELECT id, created_at, total FROM sales WHERE ABS(total - %s) < 10 AND customer_id = %s AND status = 'completed' LIMIT 5",
            (amount, customer_id)
        )
        rows = cur.fetchall()
        if len(rows) == 1:
            cur.close()
            state_data['selected_sale_id'] = rows[0][0]
            return _process_invoice_send(rows[0][0], state_data, ctx)
        elif len(rows) > 1:
            cur.close()
            lines = ["Encontré varias facturas con ese monto:\n"]
            for i, r in enumerate(rows, 1):
                if hasattr(r[1], 'strftime'):
                    date_str = r[1].strftime('%d/%m/%Y')
                else:
                    date_str = str(r[1])[:10]
                lines.append(f"{i}. Folio #{r[0]} — ${float(r[2]):,.2f} — {date_str}")
            lines.append("\nResponde con el número.")
            state_data['invoice_custom_results'] = [{'id': r[0]} for r in rows]
            return '\n'.join(lines), 'invoice_custom_results', state_data
    except ValueError:
        pass

    cur.close()
    return (
        "No encontré facturas con ese criterio. 😕\n\n"
        "Intenta con:\n"
        "• Número de folio exacto\n"
        "• Fecha (dd/mm/aaaa)\n"
        "• Monto total (ej. 1234.50)\n\n"
        "O responde *nueva* si necesitas generar una factura nueva.",
        'invoice_custom_search',
        state_data
    )


@state('invoice_custom_results')
def handle_invoice_custom_results(text, state_data, ctx):
    """Seleccionar de resultados de búsqueda alternativa."""
    results = state_data.get('invoice_custom_results', [])
    try:
        selection = int(text.strip())
        if 1 <= selection <= len(results):
            sale_id = results[selection - 1]['id']
            state_data['selected_sale_id'] = sale_id
            return _process_invoice_send(sale_id, state_data, ctx)
    except (ValueError, IndexError):
        pass

    return f"Responde con un número del 1 al {len(results)}.", 'invoice_custom_results', state_data


@state('invoice_capture_fiscal')
def handle_invoice_capture_fiscal(text, state_data, ctx):
    """Capturar datos fiscales para nueva factura."""
    if not state_data.get('fiscal_prompted'):
        state_data['fiscal_prompted'] = True
        return _MSG_INVOICE_FISCAL_PROMPT, 'invoice_capture_fiscal', state_data

    if not text or len(text.strip()) < 5:
        return "Por favor envíame tus datos fiscales completos.", 'invoice_capture_fiscal', state_data

    fiscal = _parse_fiscal_data(text.strip())

    if not fiscal.get('rfc'):
        return (
            "No pude identificar tu RFC. Por favor envíame los datos en este formato:\n\n"
            "RFC: XXXX000000XXX\n"
            "Razón social: Tu Empresa S.A. de C.V.\n"
            "Uso CFDI: G03\n"
            "CP: 12345",
            'invoice_capture_fiscal',
            state_data
        )

    state_data['fiscal_data'] = fiscal
    return (
        f"✅ Datos fiscales capturados:\n\n"
        f"RFC: {fiscal['rfc']}\n"
        f"Razón social: {fiscal['razon_social']}\n"
        f"Uso CFDI: {fiscal.get('uso_cfdi', 'G03')}\n"
        f"CP: {fiscal.get('cp', 'N/A')}\n\n"
        f"Ahora envíame una foto del ticket de compra.",
        'invoice_capture_ticket',
        state_data
    )


@state('invoice_capture_ticket')
def handle_invoice_capture_ticket(text, state_data, ctx):
    """Recibir foto del ticket de compra."""
    if ctx.media_kind == 'image' and ctx.media_base64:
        state_data['ticket_image_b64'] = ctx.media_base64
        return (
            "✅ Ticket recibido.\n\n"
            "Procesando tu factura... Esto puede tomar unos momentos.",
            'invoice_generate',
            state_data
        )

    return (
        "Por favor envíame una *foto* del ticket de compra para validar los datos. 📸\n\n"
        "Asegúrate de que se vean claramente los productos, montos y fecha.",
        'invoice_capture_ticket',
        state_data
    )


@state('invoice_generate')
def handle_invoice_generate(text, state_data, ctx):
    """Generar factura vía API SAT (placeholder en fase 1)."""
    return (
        "📄 *Factura en proceso*\n\n"
        "He recibido todos tus datos y el ticket. Tu solicitud fue enviada al área de facturación.\n\n"
        "Te contactaremos en cuanto tu factura esté lista (usualmente en menos de 24 horas hábiles).\n\n"
        "¿Necesitas algo más? Escribe *menú* para volver.",
        'invoice_send',
        state_data
    )


@state('invoice_send')
def handle_invoice_send(text, state_data, ctx):
    """Confirmar envío de factura y cerrar flujo."""
    return None, 'support_done', state_data


# ─── Funciones auxiliares ────────────────────────────────────────────

def _is_greeting(text):
    """Detectar si el texto es un saludo."""
    greetings = ('hola', 'buenos dias', 'buenas tardes', 'buenas noches',
                 'hey', 'que onda', 'saludos', 'buen dia', 'buena tarde',
                 'buena noche', 'qué tal', 'como estas', 'cómo estás')
    return text.strip().lower() in greetings


def _get_branch_name(tenant_conn, branch_id):
    """Obtener nombre de la sucursal."""
    if not tenant_conn:
        return 'Autopartes'
    try:
        cur = tenant_conn.cursor()
        if branch_id:
            cur.execute("SELECT name FROM branches WHERE id = %s", (branch_id,))
            row = cur.fetchone()
            if row and row[0]:
                cur.close()
                return row[0]
        cur.execute("SELECT value FROM tenant_config WHERE key = 'tenant_business_name'")
        row = cur.fetchone()
        cur.close()
        return row[0] if row and row[0] else 'Autopartes'
    except Exception as e:
        print(f"[WA-SM] get_branch_name error: {e}")
        return 'Autopartes'


def _get_branch_phone(tenant_conn, branch_id):
    """Obtener teléfono de la sucursal."""
    if not tenant_conn:
        return '(pendiente)'
    try:
        cur = tenant_conn.cursor()
        if branch_id:
            cur.execute("SELECT phone FROM branches WHERE id = %s", (branch_id,))
            row = cur.fetchone()
            if row and row[0]:
                cur.close()
                return row[0]
        cur.execute("SELECT value FROM tenant_config WHERE key = 'tenant_phone'")
        row = cur.fetchone()
        cur.close()
        return row[0] if row and row[0] else '(pendiente)'
    except Exception as e:
        print(f"[WA-SM] get_branch_phone error: {e}")
        return '(pendiente)'


def _get_tenant_business_name(tenant_conn):
    """Obtener nombre del negocio desde tenant_config."""
    if not tenant_conn:
        return 'Autopartes'
    try:
        cur = tenant_conn.cursor()
        cur.execute("SELECT value FROM tenant_config WHERE key = 'tenant_business_name'")
        row = cur.fetchone()
        cur.close()
        return row[0] if row and row[0] else 'Autopartes'
    except Exception:
        return 'Autopartes'


def _get_customer_by_id(tenant_conn, customer_id):
    """Wrapper interno para obtener cliente por ID."""
    from services.wa_customer import get_customer_by_id
    return get_customer_by_id(tenant_conn, customer_id)


def _check_branch_delivery(tenant_conn, branch_id):
    """Verificar si la sucursal tiene envío activo."""
    if not tenant_conn:
        return False
    try:
        cur = tenant_conn.cursor()
        cur.execute(
            "SELECT is_enabled FROM branch_delivery_config WHERE branch_id = %s",
            (branch_id,)
        )
        row = cur.fetchone()
        cur.close()
        if row:
            return row[0]

        cur = tenant_conn.cursor()
        cur.execute("SELECT value FROM tenant_config WHERE key = 'delivery_enabled'")
        row = cur.fetchone()
        cur.close()
        return row[0].lower() == 'true' if row and row[0] else False
    except Exception as e:
        print(f"[WA-SM] check_branch_delivery error: {e}")
        return False


def _assess_confidence(ai_resp, original_text):
    """Evaluar confianza de la AI en su interpretación."""
    search_query = ai_resp.get('search_query', '') or ''
    if not search_query:
        return 0.0

    if re.search(r'[A-Z0-9]{5,}', original_text.upper()):
        return 0.95

    words = search_query.split()
    if len(words) <= 1:
        return 0.4

    if ai_resp.get('vehicle', {}).get('brand') and len(words) >= 2:
        return 0.85

    return 0.6


def _build_explore_prompt(description, vehicle=None):
    """Construir prompt de exploración para la AI."""
    vehicle_str = ""
    if vehicle:
        vehicle_str = f"Vehículo: {vehicle.get('brand','')} {vehicle.get('model','')} {vehicle.get('year','')}. "

    return (
        f"El cliente busca una refacción automotriz pero la descripción es ambigua.\n"
        f"{vehicle_str}\n"
        f"Descripción del cliente: '{description}'\n\n"
        f"Tu tarea: Identificar la pieza MÁS PROBABLE que el cliente necesita. "
        f"Responde en JSON: {{\"message\":\"nombre de la pieza sugerida\", "
        f"\"search_query\":\"términos en inglés para buscar en catálogo\", "
        f"\"vehicle\":{{\"brand\":\"...\",\"model\":\"...\",\"year\":\"...\"}}}}\n\n"
        f"La 'message' debe ser una sugerencia clara y específica en español, "
        f"como 'Balata de freno delantera cerámica' o 'Filtro de aceite Mann W712/80'."
    )


def _trigger_learning_route(state_data, ctx):
    """Registrar sesión no resuelta y transferir a soporte."""
    try:
        from services.wa_learning import register_unresolved_search
        register_unresolved_search(
            phone=ctx.phone,
            customer_id=state_data.get('customer_id'),
            description=state_data.get('search_description', ''),
            offered_parts=state_data.get('offered_parts', []),
            tenant_conn=ctx.tenant_conn
        )
    except Exception as e:
        print(f"[WA-SM] Learning registration failed: {e}")

    phone_branch = _get_branch_phone(ctx.tenant_conn, ctx.branch_id)
    reply = _MSG_LEARNING_TRANSFER.format(phone=phone_branch)

    clean_data = {
        'customer_id': state_data.get('customer_id'),
        'branch_name': state_data.get('branch_name'),
    }
    return reply, 'support_done', clean_data


def _enrich_wa_reply_with_part(search_query, vehicle, tenant_conn, master_conn):
    """Wrapper para la función existente en whatsapp_bp (import local para evitar circular)."""
    from blueprints.whatsapp_bp import _enrich_wa_reply_with_part as enrich
    return enrich(search_query, vehicle, tenant_conn, master_conn)


def _parse_fiscal_data(text):
    """Extraer RFC, razón social, uso CFDI y CP de texto libre."""
    result = {}

    rfc_match = re.search(r'[A-ZÑ&]{3,4}[0-9]{6}[A-ZÑ&0-9]{3}', text.upper())
    if rfc_match:
        result['rfc'] = rfc_match.group(0)

    rs_patterns = [
        r'(?:raz[oó]n social|nombre|empresa)[\s:]+([^\n]+)',
        r'(?:razon social)[\s:]+([^\n]+)',
    ]
    for pat in rs_patterns:
        match = re.search(pat, text, re.IGNORECASE)
        if match:
            result['razon_social'] = match.group(1).strip()
            break

    uso_match = re.search(r'(?:uso\s*cfdi|cfdi)[\s:]+([A-Z0-9]{3})', text, re.IGNORECASE)
    if uso_match:
        result['uso_cfdi'] = uso_match.group(1).upper()

    cp_match = re.search(r'(?:cp|c\.p\.|código postal)[\s:]+(\d{5})', text, re.IGNORECASE)
    if cp_match:
        result['cp'] = cp_match.group(1)

    if not result.get('razon_social'):
        lines = [l.strip() for l in text.split('\n') if l.strip()]
        for line in lines:
            if not re.search(r'[A-ZÑ&]{3,4}[0-9]{6}', line) and not re.match(r'\d{5}$', line):
                if len(line) > 3:
                    result['razon_social'] = line
                    break

    return result


def _process_invoice_send(sale_id, state_data, ctx):
    """Buscar CFDI existente o encolar generación."""
    cur = ctx.tenant_conn.cursor()

    cur.execute("""
        SELECT id, status, xml_signed, uuid_fiscal, provisional_folio
        FROM cfdi_queue
        WHERE sale_id = %s AND type = 'ingreso' AND status IN ('stamped', 'signed')
        ORDER BY created_at DESC LIMIT 1
    """, (sale_id,))
    row = cur.fetchone()

    if row and row[2]:
        cur.close()
        return (
            f"📄 *Factura encontrada*\n\n"
            f"Folio fiscal: {row[4] or row[3] or 'N/A'}\n"
            f"UUID: {row[3] or 'N/A'}\n\n"
            f"Te la envío en un momento...\n\n"
            f"¿Necesitas algo más? Escribe *menú* para volver.",
            'invoice_send',
            state_data
        )

    cur.close()
    return (
        "📄 Estoy generando tu factura...\n\n"
        "Esto puede tomar unos momentos. Te la enviaré en cuanto esté lista.\n\n"
        "¿Necesitas algo más? Escribe *menú* para volver.",
        'invoice_send',
        state_data
    )


# ─── Persistencia de sesión (para uso desde whatsapp_bp) ─────────────

def get_session(tenant_conn, phone):
    """Cargar sesión WA desde DB."""
    from services.wa_quotation import _ensure_sessions_table
    _ensure_sessions_table(tenant_conn)

    cur = tenant_conn.cursor()
    cur.execute("""
        SELECT state, state_data, customer_id, updated_at
        FROM whatsapp_sessions WHERE phone = %s
    """, (phone,))
    row = cur.fetchone()
    cur.close()

    if row:
        return {
            'state': row[0] or 'idle',
            'state_data': row[1] if isinstance(row[1], dict) else (json.loads(row[1]) if row[1] else {}),
            'customer_id': row[2],
            'updated_at': row[3],
        }
    return {'state': 'idle', 'state_data': {}, 'customer_id': None, 'updated_at': None}


def save_session(tenant_conn, phone, state, state_data):
    """Persistir estado de sesión."""
    from services.wa_quotation import _ensure_sessions_table
    _ensure_sessions_table(tenant_conn)

    cur = tenant_conn.cursor()
    cur.execute("""
        INSERT INTO whatsapp_sessions (phone, state, state_data, updated_at)
        VALUES (%s, %s, %s, NOW())
        ON CONFLICT (phone) DO UPDATE SET
            state = EXCLUDED.state,
            state_data = EXCLUDED.state_data,
            updated_at = NOW()
    """, (phone, state, json.dumps(state_data)))
    tenant_conn.commit()
    cur.close()
