# -*- coding: utf-8 -*-
"""
Proxy/caché entre WACRM (Supabase) y Odoo.
Expone endpoints REST para el frontend SKEEN.
"""

from odoo import http
from odoo.http import request
from .frontend import json_response, _parse_json_body
import os
import logging
import requests
import re
import uuid
from datetime import datetime

_logger = logging.getLogger(__name__)


def _get_supabase_config():
    """Lee configuración de Supabase desde variables de entorno o ir.config_parameter."""
    get_param = request.env['ir.config_parameter'].sudo().get_param
    url = os.environ.get('SUPABASE_URL') or get_param('skeen.supabase.url')
    key = os.environ.get('SUPABASE_SERVICE_ROLE_KEY') or get_param('skeen.supabase.service_role_key')
    if not url or not key:
        raise Exception('Falta configuración SUPABASE_URL o SUPABASE_SERVICE_ROLE_KEY')
    return url.rstrip('/'), key


def _supabase_request(method, table, params=None, json=None, headers_extra=None):
    url, key = _get_supabase_config()
    headers = {
        'apikey': key,
        'Authorization': f'Bearer {key}',
        'Content-Type': 'application/json',
        'Accept': 'application/json',
    }
    if headers_extra:
        headers.update(headers_extra)
    response = requests.request(method, f"{url}/rest/v1/{table}", headers=headers, params=params or {}, json=json, timeout=30)
    response.raise_for_status()
    return response.json() if response.text else {}


def _get_wacrm_config():
    """Lee configuración de WACRM API desde variables de entorno o ir.config_parameter."""
    get_param = request.env['ir.config_parameter'].sudo().get_param
    url = os.environ.get('WACRM_API_URL') or get_param('skeen.wacrm.api_url') or 'http://localhost:3000'
    key = os.environ.get('WACRM_API_KEY') or get_param('skeen.wacrm.api_key')
    if not url or not key:
        raise Exception('Falta configuración WACRM_API_URL o WACRM_API_KEY')
    return url.rstrip('/'), key


def _wacrm_request(method, path, json=None, params=None):
    url, key = _get_wacrm_config()
    headers = {
        'Authorization': f'Bearer {key}',
        'Content-Type': 'application/json',
        'Accept': 'application/json',
    }
    response = requests.request(method, f"{url}/api/v1{path}", headers=headers, json=json, params=params or {}, timeout=30)
    response.raise_for_status()
    return response.json()


def _get_hermes_bridge_config():
    """Lee configuración del bridge de Hermes."""
    get_param = request.env['ir.config_parameter'].sudo().get_param
    url = os.environ.get('HERMES_BRIDGE_URL') or get_param('skeen.hermes.bridge_url') or 'http://localhost:8090'
    return url.rstrip('/')


def _hermes_bridge_request(path, json=None):
    url = _get_hermes_bridge_config()
    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
    }
    response = requests.request('POST', f"{url}{path}", headers=headers, json=json or {}, timeout=30)
    response.raise_for_status()
    return response.json()


def _supabase_request_paginated(table, params=None, page_size=1000):
    """Itera sobre todas las páginas de resultados de PostgREST.

    PostgREST limita `limit` al máximo configurado (por defecto 1000).
    Usamos offset para traer todos los registros en lotes.
    """
    all_data = []
    offset = 0
    params = dict(params or {})
    params['limit'] = page_size
    params['order'] = params.get('order', 'id.asc')
    while True:
        params['offset'] = offset
        page = _supabase_request('GET', table, params)
        if not page:
            break
        all_data.extend(page)
        if len(page) < page_size:
            break
        offset += page_size
    return all_data


def _parse_datetime(value):
    if not value:
        return None
    try:
        from datetime import timezone
        dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
        if dt.tzinfo:
            dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
        return dt
    except Exception:
        return None


class SkeenWacrmProxyController(http.Controller):
    """Endpoints proxy WACRM para el frontend."""

    # ============================================================
    # Sincronización
    # ============================================================
    @http.route('/skeen/frontend/v1/wacrm/sync', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
    def sync_wacrm(self, **kw):
        """Sincroniza contactos, conversaciones, mensajes, pipelines y deals desde Supabase hacia Odoo."""
        try:
            result = self._do_sync()
            return json_response({'status': 'success', 'result': result})
        except Exception as e:
            _logger.exception('Error sincronizando WACRM')
            return json_response({'status': 'error', 'message': str(e)}, 500)

    def _do_sync(self):
        account_id = None

        # 1. Contacts (paginado por si PostgREST limita a 1000 filas)
        contacts_data = _supabase_request_paginated('contacts', {'select': '*'})
        contacts_map = {}
        for item in contacts_data:
            if not account_id and item.get('account_id'):
                account_id = item['account_id']
            vals = {
                'external_id': item['id'],
                'account_id': item.get('account_id'),
                'phone': item.get('phone'),
                'name': item.get('name') or item.get('phone'),
                'email': item.get('email'),
                'company': item.get('company'),
                'avatar_url': item.get('avatar_url'),
                'created_at': _parse_datetime(item.get('created_at')),
                'updated_at': _parse_datetime(item.get('updated_at')),
            }
            existing = request.env['skeen.wacrm.contact'].sudo().search([('external_id', '=', item['id'])], limit=1)
            if existing:
                existing.write(vals)
                contacts_map[item['id']] = existing
            else:
                contacts_map[item['id']] = request.env['skeen.wacrm.contact'].sudo().create(vals)

        # 2. Conversations
        conversations_data = _supabase_request_paginated('conversations', {'select': '*'})
        conversations_map = {}
        for item in conversations_data:
            vals = {
                'external_id': item['id'],
                'contact_id': contacts_map.get(item.get('contact_id'), request.env['skeen.wacrm.contact']).id if contacts_map.get(item.get('contact_id')) else False,
                'status': item.get('status', 'open'),
                'assigned_agent_id': item.get('assigned_agent_id') or None,
                'assigned_agent': str(item.get('assigned_agent_id')) if item.get('assigned_agent_id') else None,
                'last_message_text': item.get('last_message_text'),
                'last_message_at': _parse_datetime(item.get('last_message_at')),
                'unread_count': item.get('unread_count', 0),
                'created_at': _parse_datetime(item.get('created_at')),
                'updated_at': _parse_datetime(item.get('updated_at')),
            }
            existing = request.env['skeen.wacrm.conversation'].sudo().search([('external_id', '=', item['id'])], limit=1)
            if existing:
                existing.write(vals)
                conversations_map[item['id']] = existing
            else:
                conversations_map[item['id']] = request.env['skeen.wacrm.conversation'].sudo().create(vals)

        # 3. Messages (últimos 5000 paginados)
        messages_data = _supabase_request_paginated(
            'messages',
            {'select': '*', 'order': 'created_at.desc'},
            page_size=1000
        )[:5000]
        for item in messages_data:
            conversation = conversations_map.get(item.get('conversation_id'))
            if not conversation:
                continue
            vals = {
                'external_id': item['id'],
                'conversation_id': conversation.id,
                'sender_type': {'contact': 'customer', 'user': 'agent'}.get(item.get('sender_type', 'customer'), item.get('sender_type', 'customer')),
                'content_type': item.get('content_type', 'text'),
                'content_text': item.get('content_text') or item.get('content'),
                'media_url': item.get('media_url'),
                'template_name': item.get('template_name'),
                'status': item.get('status', 'sent'),
                'created_at': _parse_datetime(item.get('created_at')),
            }
            existing = request.env['skeen.wacrm.message'].sudo().search([('external_id', '=', item['id'])], limit=1)
            if existing:
                existing.write(vals)
            else:
                request.env['skeen.wacrm.message'].sudo().create(vals)

        # 4. Pipelines & Stages
        pipelines_data = _supabase_request_paginated('pipelines', {'select': '*'})
        pipelines_map = {}
        stages_map = {}
        for item in pipelines_data:
            vals = {
                'external_id': item['id'],
                'name': item.get('name'),
                'created_at': _parse_datetime(item.get('created_at')),
            }
            existing = request.env['skeen.wacrm.pipeline'].sudo().search([('external_id', '=', item['id'])], limit=1)
            if existing:
                existing.write(vals)
                pipelines_map[item['id']] = existing
            else:
                pipelines_map[item['id']] = request.env['skeen.wacrm.pipeline'].sudo().create(vals)

        stages_data = _supabase_request_paginated('pipeline_stages', {'select': '*'})
        for item in stages_data:
            vals = {
                'external_id': item['id'],
                'pipeline_id': pipelines_map.get(item.get('pipeline_id'), request.env['skeen.wacrm.pipeline']).id if pipelines_map.get(item.get('pipeline_id')) else False,
                'name': item.get('name'),
                'position': item.get('position', 0),
                'color': item.get('color', '#3b82f6'),
            }
            existing = request.env['skeen.wacrm.stage'].sudo().search([('external_id', '=', item['id'])], limit=1)
            if existing:
                existing.write(vals)
                stages_map[item['id']] = existing
            else:
                stages_map[item['id']] = request.env['skeen.wacrm.stage'].sudo().create(vals)

        # 5. Deals
        # Mapa profiles.id → user_id (Supabase guarda assigned_to como profiles.id;
        # localmente usamos user_id, consistente con conversations.assigned_agent_id)
        profiles_usermap = {}
        try:
            for p in _supabase_request('GET', 'profiles', params={'select': 'id,user_id'}):
                profiles_usermap[p['id']] = p['user_id']
        except Exception:
            pass
        deals_data = _supabase_request_paginated('deals', {'select': '*'})
        for item in deals_data:
            vals = {
                'external_id': item['id'],
                'title': item.get('title') or 'Sin título',
                'contact_id': contacts_map.get(item.get('contact_id'), request.env['skeen.wacrm.contact']).id if contacts_map.get(item.get('contact_id')) else False,
                'conversation_id': item.get('conversation_id'),
                'pipeline_id': pipelines_map.get(item.get('pipeline_id'), request.env['skeen.wacrm.pipeline']).id if pipelines_map.get(item.get('pipeline_id')) else False,
                'stage_id': stages_map.get(item.get('stage_id'), request.env['skeen.wacrm.stage']).id if stages_map.get(item.get('stage_id')) else False,
                'value': float(item.get('value', 0) or 0),
                'currency': item.get('currency', 'USD'),
                'status': item.get('status', 'open'),
                'expected_close_date': item.get('expected_close_date'),
                'notes': item.get('notes'),
                'assigned_to': profiles_usermap.get(item.get('assigned_to')) if item.get('assigned_to') else None,
                'created_at': _parse_datetime(item.get('created_at')),
                'updated_at': _parse_datetime(item.get('updated_at')),
            }
            existing = request.env['skeen.wacrm.deal'].sudo().search([('external_id', '=', item['id'])], limit=1)
            if existing:
                existing.write(vals)
            else:
                request.env['skeen.wacrm.deal'].sudo().create(vals)

        # 6. Auto-crear deals: contactos con conversación sin deal abierto.
        # Se crean EN SUPABASE (la app WACRM nativa los ve) y se espejan local.
        # Idempotente: un deal abierto por contacto.
        deals_created = 0
        try:
            if conversations_map and pipelines_map and account_id:
                pipeline = sorted(pipelines_map.values(), key=lambda p: p.name or '')[0]
                stage = request.env['skeen.wacrm.stage'].sudo().search(
                    [('pipeline_id', '=', pipeline.id)], order='position asc', limit=1)
                # user_id requerido por Supabase: el primer profile (owner de la cuenta)
                owner = _supabase_request('GET', 'profiles', params={
                    'select': 'user_id', 'order': 'created_at.asc', 'limit': 1})
                owner_id = owner[0]['user_id'] if owner else None
                if stage and owner_id:
                    Deal = request.env['skeen.wacrm.deal'].sudo()
                    # Un deal por contacto: no crear si ya tiene cualquier deal (open o ganado)
                    contacts_with_deals = {str(d.get('contact_id')) for d in deals_data if d.get('contact_id')}
                    for conv in conversations_map.values():
                        contact = conv.contact_id
                        if not contact:
                            continue
                        if contact.external_id in contacts_with_deals:
                            continue
                        if Deal.search([('contact_id', '=', contact.id)], limit=1):
                            continue
                        new_id = str(uuid.uuid4())
                        try:
                            _supabase_request('POST', 'deals', json={
                                'id': new_id,
                                'user_id': owner_id,
                                'account_id': account_id,
                                'pipeline_id': pipeline.external_id,
                                'stage_id': stage.external_id,
                                'contact_id': contact.external_id,
                                'conversation_id': conv.external_id,
                                'title': contact.name or contact.phone or 'Lead WhatsApp',
                                'value': 0,
                                'currency': 'MXN',
                                'status': 'open',
                            }, headers_extra={'Prefer': 'return=minimal'})
                            Deal.create({
                                'external_id': new_id,
                                'title': contact.name or contact.phone or 'Lead WhatsApp',
                                'contact_id': contact.id,
                                'conversation_id': conv.external_id,
                                'pipeline_id': pipeline.id,
                                'stage_id': stage.id,
                                'value': 0,
                                'currency': 'MXN',
                                'status': 'open',
                            })
                            contacts_with_deals.add(contact.external_id)
                            deals_created += 1
                        except Exception as e:
                            _logger.warning('No se pudo auto-crear deal para contacto %s: %s', contact.external_id, e)
        except Exception as e:
            _logger.warning('Auto-deals omitidos: %s', e)

        return {
            'contacts': len(contacts_data),
            'conversations': len(conversations_data),
            'messages': len(messages_data),
            'pipelines': len(pipelines_data),
            'stages': len(stages_data),
            'deals': len(deals_data),
            'deals_created': deals_created,
            'account_id': account_id,
        }

    # ============================================================
    # Mensajes
    # ============================================================
    @http.route('/skeen/frontend/v1/wacrm/messages', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
    def send_message(self, **kw):
        try:
            data = _parse_json_body()
            conversation_id = data.get('conversation_id')
            to = data.get('to')
            text = data.get('text')
            assigned_agent_id = data.get('assigned_agent_id')

            if not text:
                return json_response({'status': 'error', 'message': 'Falta texto del mensaje'}, 400)
            if not conversation_id and not to:
                return json_response({'status': 'error', 'message': 'Falta conversation_id o destinatario'}, 400)

            # Resolver teléfono de destino
            phone = to
            conv_external_id = conversation_id
            if conversation_id and not phone:
                conv = request.env['skeen.wacrm.conversation'].sudo().search([('external_id', '=', conversation_id)], limit=1)
                if conv and conv.contact_id:
                    phone = conv.contact_id.phone
                elif conv:
                    phone = conv.contact_phone

            # Normalizar a E.164
            if phone and not phone.startswith('+'):
                digits = re.sub(r'\D', '', phone)
                phone = '+' + digits.lstrip('0') if digits else ''

            if not phone:
                return json_response({'status': 'error', 'message': 'No se pudo resolver el teléfono destino'}, 400)

            # Enviar mensaje vía bridge de Hermes (tiene la API key real de WACRM)
            result = _hermes_bridge_request('/send-message', {
                'conversation_id': conv_external_id,
                'text': text,
                'assigned_agent_id': assigned_agent_id,
            })

            return json_response({'status': 'success', 'message': 'Mensaje enviado', 'result': result})
        except Exception as e:
            _logger.exception('Error enviando mensaje WACRM')
            return json_response({'status': 'error', 'message': str(e)}, 500)

    @http.route('/skeen/frontend/v1/wacrm/messages', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
    def list_messages(self, **kw):
        try:
            domain = []
            if kw.get('conversation_id'):
                conv = request.env['skeen.wacrm.conversation'].sudo().search([('external_id', '=', kw.get('conversation_id'))], limit=1)
                if conv:
                    domain.append(('conversation_id', '=', conv.id))
            if kw.get('contact_id'):
                contact = request.env['skeen.wacrm.contact'].sudo().search([('external_id', '=', kw.get('contact_id'))], limit=1)
                if contact:
                    domain.append(('contact_id', '=', contact.id))
            if kw.get('search'):
                search = kw.get('search')
                domain += ['|', ('content_text', 'ilike', search), ('contact_name', 'ilike', search)]

            messages = request.env['skeen.wacrm.message'].sudo().search(domain, order='created_at desc', limit=200)
            return json_response({
                'status': 'success',
                'messages': [self._message_to_dict(m) for m in messages],
            })
        except Exception as e:
            _logger.exception('Error listando mensajes WACRM')
            return json_response({'status': 'error', 'message': str(e)}, 500)

    @http.route('/skeen/frontend/v1/wacrm/conversations', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
    def list_conversations(self, **kw):
        try:
            domain = []
            if kw.get('search'):
                search = kw.get('search')
                domain += ['|', ('contact_name', 'ilike', search), ('contact_phone', 'ilike', search)]
            conversations = request.env['skeen.wacrm.conversation'].sudo().search(domain, order='last_message_at desc', limit=200)
            return json_response({
                'status': 'success',
                'conversations': [self._conversation_to_dict(c) for c in conversations],
            })
        except Exception as e:
            _logger.exception('Error listando conversaciones WACRM')
            return json_response({'status': 'error', 'message': str(e)}, 500)

    @http.route('/skeen/frontend/v1/wacrm/members', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
    def list_members(self, **kw):
        try:
            rows = _supabase_request(
                'GET',
                'profiles',
                params={'select': 'user_id,full_name,email,avatar_url,account_role,created_at', 'order': 'created_at.asc'}
            )
            members = [{
                'id': r.get('user_id'),
                'profile_id': r.get('id'),
                'name': r.get('full_name') or r.get('email') or r.get('user_id'),
                'email': r.get('email'),
                'avatar_url': r.get('avatar_url'),
                'role': r.get('account_role') or 'user',
            } for r in rows]
            return json_response({'status': 'success', 'members': members})
        except Exception as e:
            _logger.exception('Error listando miembros WACRM')
            return json_response({'status': 'error', 'message': str(e)}, 500)

    @http.route('/skeen/frontend/v1/wacrm/conversations/<string:external_id>/assign', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
    def assign_conversation(self, external_id, **kw):
        try:
            data = _parse_json_body()
            assigned_agent_id = data.get('assigned_agent_id')

            # Actualizar directamente en Supabase
            update = {'assigned_agent_id': assigned_agent_id, 'updated_at': datetime.utcnow().isoformat() + 'Z'}
            _supabase_request(
                'PATCH',
                'conversations',
                params={'id': f'eq.{external_id}'},
                json=update,
                headers_extra={'Prefer': 'return=minimal'}
            )

            # Actualizar cache local en Odoo
            conv = request.env['skeen.wacrm.conversation'].sudo().search([('external_id', '=', external_id)], limit=1)
            if conv:
                conv.write({
                    'assigned_agent_id': assigned_agent_id or None,
                    'assigned_agent': str(assigned_agent_id) if assigned_agent_id else None,
                })

            return json_response({'status': 'success', 'assigned_agent_id': assigned_agent_id})
        except Exception as e:
            _logger.exception('Error asignando conversación WACRM')
            return json_response({'status': 'error', 'message': str(e)}, 500)

    @http.route('/skeen/frontend/v1/wacrm/conversations/<string:external_id>/assigned', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
    def is_conversation_assigned(self, external_id, **kw):
        try:
            conv = request.env['skeen.wacrm.conversation'].sudo().search([('external_id', '=', external_id)], limit=1)
            if not conv:
                return json_response({'status': 'success', 'assigned': False})
            return json_response({
                'status': 'success',
                'assigned': bool(conv.assigned_agent_id),
                'assigned_agent_id': conv.assigned_agent_id or None,
                'assigned_agent_name': conv.assigned_agent or None,
            })
        except Exception as e:
            _logger.exception('Error consultando asignación WACRM')
            return json_response({'status': 'error', 'message': str(e)}, 500)

    # ============================================================
    # Leads / Deals
    # ============================================================
    @http.route('/skeen/frontend/v1/wacrm/pipelines', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
    def list_pipelines(self, **kw):
        try:
            pipelines = request.env['skeen.wacrm.pipeline'].sudo().search([], order='name asc')
            return json_response({
                'status': 'success',
                'pipelines': [{
                    'id': p.id,
                    'external_id': p.external_id,
                    'name': p.name,
                    'stages': [{
                        'id': s.id,
                        'external_id': s.external_id,
                        'name': s.name,
                        'position': s.position,
                        'color': s.color or '#3b82f6',
                    } for s in request.env['skeen.wacrm.stage'].sudo().search([('pipeline_id', '=', p.id)], order='position asc')]
                } for p in pipelines],
            })
        except Exception as e:
            _logger.exception('Error listando pipelines WACRM')
            return json_response({'status': 'error', 'message': str(e)}, 500)

    @http.route('/skeen/frontend/v1/wacrm/leads', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
    def list_leads(self, **kw):
        try:
            domain = []
            if kw.get('status'):
                domain.append(('status', '=', kw.get('status')))
            if kw.get('pipeline_id'):
                pipe = request.env['skeen.wacrm.pipeline'].sudo().search([('external_id', '=', kw.get('pipeline_id'))], limit=1)
                if pipe:
                    domain.append(('pipeline_id', '=', pipe.id))
            if kw.get('search'):
                search = kw.get('search')
                domain += ['|', ('title', 'ilike', search), ('contact_name', 'ilike', search)]

            deals = request.env['skeen.wacrm.deal'].sudo().search(domain, order='created_at desc', limit=200)
            return json_response({
                'status': 'success',
                'leads': [self._deal_to_dict(d) for d in deals],
            })
        except Exception as e:
            _logger.exception('Error listando leads WACRM')
            return json_response({'status': 'error', 'message': str(e)}, 500)

    @http.route('/skeen/frontend/v1/wacrm/leads/<int:lead_id>/status', type='http', auth='none', methods=['PUT'], csrf=False)
    def update_lead_status(self, lead_id, **kw):
        try:
            deal = request.env['skeen.wacrm.deal'].sudo().browse(lead_id)
            if not deal.exists():
                return json_response({'status': 'error', 'message': 'Lead no encontrado'}, 404)
            data = _parse_json_body()
            update_remote = {}
            if data.get('status') in ('open', 'won', 'lost'):
                deal.write({'status': data.get('status')})
                update_remote['status'] = data.get('status')
            if data.get('stage_id'):
                stage = request.env['skeen.wacrm.stage'].sudo().search([('external_id', '=', data.get('stage_id'))], limit=1)
                if stage:
                    deal.write({'stage_id': stage.id})
                    update_remote['stage_id'] = stage.external_id
            if 'assigned_to' in data:
                # La UI manda user_id; el FK remoto deals.assigned_to apunta a profiles.id
                assigned = data.get('assigned_to') or None
                deal.write({'assigned_to': assigned})
                if assigned:
                    prof = _supabase_request('GET', 'profiles', params={
                        'select': 'id', 'user_id': f'eq.{assigned}', 'limit': 1})
                    update_remote['assigned_to'] = prof[0]['id'] if prof else None
                else:
                    update_remote['assigned_to'] = None
            if update_remote:
                # Reflejar en Supabase para que el próximo sync no lo revierta
                update_remote['updated_at'] = datetime.utcnow().isoformat() + 'Z'
                _supabase_request(
                    'PATCH', 'deals',
                    params={'id': f'eq.{deal.external_id}'},
                    json=update_remote,
                    headers_extra={'Prefer': 'return=minimal'})
            return json_response({'status': 'success', 'lead': self._deal_to_dict(deal)})
        except Exception as e:
            return json_response({'status': 'error', 'message': str(e)}, 500)

    @http.route('/skeen/frontend/v1/wacrm/leads/<int:lead_id>/convert', type='http', auth='none', methods=['POST', 'OPTIONS'], csrf=False)
    def convert_lead(self, lead_id, **kw):
        """Convierte el lead en paciente SKEEN (crea o liga por teléfono) y lo marca ganado"""
        try:
            deal = request.env['skeen.wacrm.deal'].sudo().browse(lead_id)
            if not deal.exists():
                return json_response({'status': 'error', 'message': 'Lead no encontrado'}, 404)
            # Idempotente: ya convertido
            if deal.partner_id:
                return json_response({
                    'status': 'success',
                    'lead': self._deal_to_dict(deal),
                    'patient_id': deal.partner_id.id,
                    'created': False,
                })
            contact = deal.contact_id
            if not contact:
                return json_response({'status': 'error', 'message': 'El lead no tiene contacto'}, 400)
            # Normalizar teléfono (52 + 10 dígitos, como el padrón de pacientes)
            digits = re.sub(r'\D', '', contact.phone or '')
            if len(digits) == 10:
                phone = '52' + digits
            else:
                phone = digits
            if not phone:
                return json_response({'status': 'error', 'message': 'El contacto no tiene teléfono'}, 400)
            Partner = request.env['res.partner'].sudo()
            partner = Partner.search([('is_patient', '=', True), '|', ('phone', '=', phone), ('mobile', '=', phone)], limit=1)
            created = False
            if not partner:
                partner = Partner.create({
                    'name': contact.name or phone,
                    'phone': phone,
                    'is_patient': True,
                    'source': 'whatsapp',
                })
                created = True
            deal.write({'partner_id': partner.id, 'status': 'won'})
            _supabase_request(
                'PATCH', 'deals',
                params={'id': f'eq.{deal.external_id}'},
                json={'status': 'won', 'updated_at': datetime.utcnow().isoformat() + 'Z'},
                headers_extra={'Prefer': 'return=minimal'})
            return json_response({
                'status': 'success',
                'lead': self._deal_to_dict(deal),
                'patient_id': partner.id,
                'created': created,
            })
        except Exception as e:
            return json_response({'status': 'error', 'message': str(e)}, 500)

    def _message_to_dict(self, m):
        return {
            'id': m.id,
            'external_id': m.external_id,
            'conversation_id': m.conversation_id.external_id if m.conversation_id else None,
            'contact_name': m.contact_id.name if m.contact_id else '',
            'contact_phone': m.contact_id.phone if m.contact_id else '',
            'sender_type': m.sender_type,
            'content_type': m.content_type,
            'content_text': m.content_text or '',
            'media_url': m.media_url or '',
            'template_name': m.template_name or '',
            'status': m.status,
            'created_at': m.created_at.strftime('%Y-%m-%d %H:%M:%S') if m.created_at else None,
        }

    def _conversation_to_dict(self, c):
        return {
            'id': c.id,
            'external_id': c.external_id,
            'contact_id': c.contact_id.external_id if c.contact_id else None,
            'contact_name': c.contact_name,
            'contact_phone': c.contact_phone,
            'status': c.status,
            'assigned_agent': c.assigned_agent or '',
            'assigned_agent_id': c.assigned_agent_id or '',
            'last_message_text': c.last_message_text or '',
            'last_message_at': c.last_message_at.strftime('%Y-%m-%d %H:%M:%S') if c.last_message_at else None,
            'unread_count': c.unread_count,
            'created_at': c.created_at.strftime('%Y-%m-%d %H:%M:%S') if c.created_at else None,
        }

    def _deal_to_dict(self, d):
        return {
            'id': d.id,
            'external_id': d.external_id,
            'title': d.title,
            'contact_id': d.contact_id.external_id if d.contact_id else None,
            'contact_name': d.contact_name,
            'contact_phone': d.contact_phone,
            'pipeline': d.pipeline_id.name if d.pipeline_id else '',
            'pipeline_id': d.pipeline_id.id if d.pipeline_id else None,
            'stage': d.stage_id.name if d.stage_id else '',
            'stage_id': d.stage_id.id if d.stage_id else None,
            'stage_color': d.stage_id.color if d.stage_id else '#3b82f6',
            'value': d.value,
            'currency': d.currency,
            'status': d.status,
            'expected_close_date': d.expected_close_date.strftime('%Y-%m-%d') if d.expected_close_date else None,
            'notes': d.notes or '',
            'assigned_to': d.assigned_to or '',
            'partner_id': d.partner_id.id if d.partner_id else None,
            'conversation_id': d.conversation_id or '',
            'created_at': d.created_at.strftime('%Y-%m-%d %H:%M:%S') if d.created_at else None,
        }
