const WebSocket = require('ws');
const Redis = require('ioredis');
const axios = require('axios');

const wss = new WebSocket.Server({ port: 9080 });

const MAX_OFFERS = process.env.MAX_OFFERS ? parseInt(process.env.MAX_OFFERS, 10) : 10;
const CANCEL_API_URL = process.env.CANCEL_API_URL || process.env.CANCELAR_VIAJE_URL || 'https://api.deliverygoperu.com/taxi/cancelar_viaje_ws.php';
const FIREBASE_NOTIFY_URL = process.env.FIREBASE_NOTIFY_URL || 'https://api.deliverygoperu.com/taxi/notificar_cancelacion_viaje_ws_firebase.php';
const BACKEND_TOKEN = process.env.BACKEND_TOKEN || '2342423423423';

// Conexión a Redis con contraseña (lee de variables de entorno, fallback para pruebas)
const redis = new Redis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : 6379,
  password: process.env.REDIS_PASSWORD || '1500@aguservis',
});

redis.on('connect', () => {
  console.log('[REDIS] conectado a', `${redis.options.host}:${redis.options.port}`);
});
redis.on('ready', () => {
  console.log('[REDIS] listo (ready)');
});
redis.on('error', (err) => {
  console.error('[REDIS] error:', err && err.message ? err.message : err);
});
redis.on('close', () => {
  console.warn('[REDIS] conexión cerrada');
});

// Suscribirse a canales: nueva_orden y cancelar_viaje
redis.subscribe('nueva_orden', (err, count) => {
  if (err) {
    console.error('[REDIS] error suscribiendo a nueva_orden:', err.message || err);
    return;
  }
  console.log('[REDIS] Suscrito al canal nueva_orden. canales_suscritos=', count);
});
redis.subscribe('cancelar_viaje', (err, count) => {
  if (err) {
    console.error('[REDIS] error suscribiendo a cancelar_viaje:', err.message || err);
    return;
  }
  console.log('[REDIS] Suscrito al canal cancelar_viaje. canales_suscritos=', count);
});

redis.on('message', (channel, message) => {
  try {
    if (channel === 'nueva_orden') {
      console.log('[REDIS] Mensaje recibido en nueva_orden:', message);
      let data;
      try {
        data = JSON.parse(message);
      } catch (e) {
        console.warn('[REDIS] JSON inválido en nueva_orden:', e.message);
        return;
      }
      handleNuevaOrden(data);
      return;
    }

    if (channel === 'cancelar_viaje') {
      console.log('[REDIS] Mensaje recibido en cancelar_viaje:', message);
      let data;
      try {
        data = JSON.parse(message);
      } catch (e) {
        console.warn('[REDIS] JSON inválido en cancelar_viaje:', e.message);
        return;
      }
      // Esperamos payload: { id_viaje: "...", estado: N, motivo: "..." }
      const id_viaje = String(data.id_viaje || '');
      const estado = data.estado !== undefined ? data.estado : null;
      const motivo = data.motivo || '';

      if (!id_viaje) {
        console.warn('[REDIS] cancelar_viaje sin id_viaje en payload');
        return;
      }

      // Si motivo es 'rechazo_en_camino' (estado 7) lo tratamos como reintento:
      if (motivo === 'rechazo_en_camino' || Number(estado) === 7) {
        console.log(`[SERVER] cancelar_viaje recibido con motivo rechazo_en_camino para viaje ${id_viaje} -> reintentar asignación`);
        const viaje = viajes[id_viaje];
        if (!viaje) {
          console.log(`[SERVER] viaje ${id_viaje} no encontrado para reintento`);
          return;
        }
        // Guardar conductor anterior para pasar a rechazados
        const anterior = viaje.asignado_a;
        // quitar asignación
        viaje.asignado_a = null;
        // añadir al listado de rechazados
        if (anterior && !viaje.rechazados.includes(anterior)) viaje.rechazados.push(anterior);
        // limpiar timeout previo
        if (viaje.timeoutId) { try { clearTimeout(viaje.timeoutId); } catch(e) {} viaje.timeoutId = null; }
        // notificar al cliente que se está buscando nuevo conductor
        const wsCliente = clientes[String(viaje.cliente)];
        if (wsCliente && wsCliente.readyState === WebSocket.OPEN) {
          try {
            wsCliente.send(JSON.stringify({ evento: 'buscando_nuevo_conductor', id_viaje, motivo: 'rechazo_en_camino' }));
            console.log(`[SERVER] notificado cliente ${viaje.cliente} que se busca nuevo conductor para viaje ${id_viaje}`);
          } catch (e) { console.warn('[SERVER] error notificando cliente buscando nuevo conductor:', e.message || e); }
        }
        // iniciar re-búsqueda
        buscarYOtorgarNuevoConductor(id_viaje, viaje, anterior);
        return;
      }

      // Para otros motivos (cancelado_por_cliente, sin_conductores, etc.) -> cancelar definitivamente
      const motivoText = motivo || (estado !== null ? `estado_${estado}` : 'cancelado_por_backend');
      cancelarViaje(id_viaje, motivoText, estado);
      return;
    }
  } catch (err) {
    console.error('[REDIS] Error manejando mensaje:', err && err.message ? err.message : err);
  }
});

// Estructuras para conexiones en memoria
const conductores = {};  // {id_conductor: ws}
const clientes = {};     // {id_cliente: ws}
const viajes = {};       // {id_viaje: {cliente, asignado_a, data, timeout, rechazados, timeoutId, currentOffered, canceled, offerAttempts, maxOffers, lastOfferedConductorInfo}}

// Helper: extrae lat/lng de un objeto con múltiples posibles nombres de campo
function extractLocationFromObj(obj) {
  if (!obj || typeof obj !== 'object') return null;
  const tryNum = (v) => {
    if (v === undefined || v === null) return null;
    const n = parseFloat(v);
    if (Number.isNaN(n)) return null;
    return n;
  };

  // posibles claves para origen/destino
  const latKeys = ['latitud_origen','latOrigen','lat_o','lat', 'latitude', 'latitud', 'origen_latitud', 'origin_lat'];
  const lngKeys = ['longitud_origen','lngOrigen','lon_o','lng', 'longitude', 'longitud', 'origen_longitud', 'origin_lng'];
  const latDKeys = ['latitud_destino','latDestino','lat_d','dest_lat','destino_latitud','destination_lat'];
  const lngDKeys = ['longitud_destino','lngDestino','lon_d','dest_lng','destino_longitud','destination_lng'];

  // attempt origin
  for (const lk of latKeys) {
    if (obj[lk] !== undefined) {
      for (const rg of lngKeys) {
        if (obj[rg] !== undefined) {
          const lat = tryNum(obj[lk]);
          const lng = tryNum(obj[rg]);
          if (lat !== null && lng !== null) return { latO: lat, lngO: lng, latD: null, lngD: null };
        }
      }
    }
  }

  // attempt dest
  for (const lk of latDKeys) {
    if (obj[lk] !== undefined) {
      for (const rg of lngDKeys) {
        if (obj[rg] !== undefined) {
          const lat = tryNum(obj[lk]);
          const lng = tryNum(obj[rg]);
          if (lat !== null && lng !== null) return { latO: null, lngO: null, latD: lat, lngD: lng };
        }
      }
    }
  }

  // sometimes payload has separate origin/destination objects
  if (obj.origen && typeof obj.origen === 'object') {
    const lat = tryNum(obj.origen.lat) || tryNum(obj.origen.latitude) || tryNum(obj.origen.latitud);
    const lng = tryNum(obj.origen.lng) || tryNum(obj.origen.longitude) || tryNum(obj.origen.longitud);
    if (lat !== null && lng !== null) {
      const dest = obj.destino || obj.destination || obj.dest;
      if (dest && typeof dest === 'object') {
        const latd = tryNum(dest.lat) || tryNum(dest.latitude) || tryNum(dest.latitud);
        const lngd = tryNum(dest.lng) || tryNum(dest.longitude) || tryNum(dest.longitud);
        if (latd !== null && lngd !== null) return { latO: lat, lngO: lng, latD: latd, lngD: lngd };
      }
      return { latO: lat, lngO: lng, latD: null, lngD: null };
    }
  }

  // fallback: check common single keys
  const latO = tryNum(obj.latitud_origen || obj.latOrigen || obj.lat_o || obj.lat || obj.latitude || obj.latitud);
  const lngO = tryNum(obj.longitud_origen || obj.lngOrigen || obj.lon_o || obj.lng || obj.longitude || obj.longitud);
  const latD = tryNum(obj.latitud_destino || obj.latDestino || obj.lat_d || obj.dest_lat || obj.destination_lat);
  const lngD = tryNum(obj.longitud_destino || obj.lngDestino || obj.lon_d || obj.dest_lng || obj.destination_lng);

  if (latO !== null && lngO !== null) return { latO, lngO, latD: latD || null, lngD: lngD || null };
  if (latD !== null && lngD !== null) return { latO: null, lngO: null, latD, lngD };

  return null;
}

// Manejo de nueva orden (publicada por el API PHP)
function handleNuevaOrden(data) {
    const id_viaje = String(data.id_viaje || '');
    const id_conductor = data.id_conductor ? String(data.id_conductor) : null;
    const id_cliente = data.id_cliente ? String(data.id_cliente) : null;

    console.log(`[SERVER] handleNuevaOrden id_viaje=${id_viaje} id_cliente=${id_cliente} id_conductor_pref=${id_conductor}`);

    viajes[id_viaje] = {
        cliente: id_cliente,
        asignado_a: null,
        data: data,
        timeout: data.timeout || 15,
        rechazados: id_conductor ? [id_conductor] : [],
        timeoutId: null,
        currentOffered: null,
        canceled: false,
        offerAttempts: 0,            // cuantas ofertas se han hecho
        maxOffers: MAX_OFFERS,       // tope de ofertas
        lastOfferedConductorInfo: null
    };

    console.log('[SERVER] viajes[id_viaje] creado:', viajes[id_viaje]);
    ofrecerConductor(id_viaje, id_conductor);
}

// Helpers para solicitar nuevo conductor y notificar push
async function solicitarNuevoConductor(id_viaje, id_cliente, rechazados) {
  try {
    const resp = await axios.post('https://api.deliverygoperu.com/taxi/consultar_conductor_disponible.php', {
      id_viaje,
      token: BACKEND_TOKEN,
      id_cliente,
      rechazados
    }, { timeout: 8000 });

    if (resp.data && resp.data.status === "ok" && resp.data.id_conductor) {
      // devolvemos todo el objeto para que el caller lo guarde en viaje.lastOfferedConductorInfo
      return resp.data;
    }
    return null;
  } catch (err) {
    console.error('[SERVER] Error solicitando nuevo conductor:', err.message || err);
    return null;
  }
}

async function notificarPushConductor(id_viaje, id_conductor, extra = {}) {
  try {
    // extra puede contener lat/lng para push payload compat
    const body = {
      id_viaje,
      token: BACKEND_TOKEN,
      id_conductor,
      ...extra
    };
    await axios.post('https://api.deliverygoperu.com/taxi/notificar_viaje_taxista.php', body, { timeout: 8000 });
    console.log(`[SERVER] notificarPushConductor - push enviado a ${id_conductor}`);
  } catch (err) {
    console.error('[SERVER] Error notificando push al conductor:', err.message || err);
  }
}

// ---------------------------------------------------------------------
// scheduleOfferTimeout / finalCancelFlow / cancelarViaje remain unchanged...
// (omitted here for brevity in this snippet but they are the same as before)
// ---------------------------------------------------------------------

function scheduleOfferTimeout(id_viaje, id_conductor) {
  const viaje = viajes[id_viaje];
  if (!viaje) return;
  if (viaje.timeoutId) {
    try { clearTimeout(viaje.timeoutId); } catch(e) {}
    viaje.timeoutId = null;
  }
  const seconds = viaje.timeout || 15;
  console.log(`[SERVER] scheduleOfferTimeout: programando ${seconds}s para viaje ${id_viaje} conductor ${id_conductor}`);
  viaje.timeoutId = setTimeout(() => {
    const v = viajes[id_viaje];
    if (!v) return;
    if (v.canceled) {
      console.log(`[SERVER] scheduleOfferTimeout: viaje ${id_viaje} cancelado, omitiendo`);
      if (v.timeoutId) { try { clearTimeout(v.timeoutId); } catch(e) {} v.timeoutId = null; }
      delete viajes[id_viaje];
      return;
    }
    if (v.asignado_a) {
      console.log(`[SERVER] scheduleOfferTimeout: viaje ${id_viaje} ya asignado a ${v.asignado_a}, omitiendo`);
      if (v.timeoutId) { try { clearTimeout(v.timeoutId); } catch(e) {} v.timeoutId = null; }
      return;
    }
    if (id_conductor && !v.rechazados.includes(String(id_conductor))) v.rechazados.push(String(id_conductor));
    if (v.currentOffered === id_conductor) v.currentOffered = null;
    console.log(`[SERVER] scheduleOfferTimeout: conductor ${id_conductor} no respondió para viaje ${id_viaje}. Buscando nuevo...`);
    if (v.timeoutId) { try { clearTimeout(v.timeoutId); } catch(e) {} v.timeoutId = null; }
    buscarYOtorgarNuevoConductor(id_viaje, v, id_conductor);
  }, seconds * 1000);
}

async function finalCancelFlow(id_viaje, viaje, motivo, estado = null) {
  try {
    console.log(`[SERVER] finalCancelFlow para viaje ${id_viaje} motivo=${motivo} estado=${estado}`);
    const motivoToEstado = {
      'cancelado_por_cliente': 1,
      'sin_conductores': 2,
      'rechazo_en_camino': 7,
      'limite_ofertas_superado': 2,
      'sin_conductores_o_limite_excedido': 2
    };
    const estadoToSend = (estado !== null && estado !== undefined) ? estado : (motivoToEstado[motivo] !== undefined ? motivoToEstado[motivo] : 2);

    // Llamada al backend si existe
    if (CANCEL_API_URL) {
      try {
        const payload = { token: BACKEND_TOKEN, id_viaje, estado: estadoToSend, motivo };
        console.log(`[SERVER] Calling CANCEL_API_URL ${CANCEL_API_URL} with payload:`, payload);
        const resp = await axios.post(CANCEL_API_URL, payload, { timeout: 8000 });
        console.log(`[SERVER] Cancel API response for ${id_viaje}:`, resp.status, resp.data ? resp.data : '');
      } catch (err) {
        if (err && err.response) {
          console.warn('[SERVER] Error calling CANCEL_API_URL - response data:', err.response.data, 'status:', err.response.status);
        }
        console.warn('[SERVER] Error calling CANCEL_API_URL:', err.message || err);
      }
    } else {
      console.warn('[SERVER] CANCEL_API_URL no configurada, omitiendo llamada al backend');
    }

    // Notificaciones según estadoToSend (igual lógica que antes) ...
    // (omitted for brevity; keep same notifications logic as original)
    // Clean up and delete viajes[id_viaje] accordingly.

    // For brevity in this reply, keep rest unchanged in your copy.
    // In your codebase, this function remains identical to the prior version.
    // ...
  } catch (err) {
    console.error('[SERVER] finalCancelFlow error:', err && err.message ? err.message : err);
  }
}

function cancelarViaje(id_viaje, motivo = 'cancelado', estado = null) {
  // Keep the same implementation as in your existing server file.
  // It remains unchanged; for brevity not duplicated here.
  // Ensure that it uses the same notifications as before.
  // ...
  const viaje = viajes[id_viaje];
  if (!viaje) {
    console.log(`[SERVER] cancelarViaje: viaje ${id_viaje} no existe`);
    return;
  }
  viaje.canceled = true;
  if (viaje.timeoutId) {
    try { clearTimeout(viaje.timeoutId); } catch (e) {}
    viaje.timeoutId = null;
  }
  // ... rest same as before
  // For the full code, use your original cancelarViaje body (unchanged).
}

// ---------------- WebSocket message handlers and offer logic ----------------

wss.on('connection', function connection(ws, req) {
    const params = {};
    if (req.url && req.url.indexOf('?') !== -1) {
      req.url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {
        params[key] = value;
      });
    }

    ws.tipo = params.tipo;
    ws.id = params.id ? String(params.id) : undefined;

    console.log(`[WS] Nueva conexión tipo=${ws.tipo} id=${ws.id} remote=${req.socket.remoteAddress}`);

    if (ws.tipo === 'conductor' && ws.id) {
        conductores[ws.id] = ws;
        console.log(`Conductor conectado: ${ws.id}`);
    } else if (ws.tipo === 'cliente' && ws.id) {
        clientes[ws.id] = ws;
        console.log(`Cliente conectado: ${ws.id}`);
    }

    ws.on('message', async function incoming(message) {
        let data;
        try {
            data = JSON.parse(message);
        } catch (e) {
            console.warn('[WS] JSON inválido:', e.message);
            return;
        }

        // Respuesta del conductor: aceptar/rechazar oferta
        if (data.action === 'respuesta_conductor' && ws.tipo === 'conductor') {
            const id_viaje = String(data.id_viaje);
            const respuesta = data.respuesta;
            const viaje = viajes[id_viaje];
            console.log(`[WS ACTION] respuesta_conductor id_viaje=${id_viaje} respuesta=${respuesta} conductor=${ws.id} viaje_found=${!!viaje}`);

            if (!viaje || viaje.asignado_a) {
                console.log('[SERVER] viaje no existe o ya asignado -> ignorando');
                return;
            }

            if (viaje.canceled) {
                try { ws.send(JSON.stringify({ evento: 'viaje_cancelado', id_viaje })); } catch (e) {}
                console.log(`[SERVER] respuesta ignorada porque viaje ${id_viaje} fue cancelado`);
                delete viajes[id_viaje];
                return;
            }

            if (respuesta === 'aceptar') {
                viaje.asignado_a = ws.id;
                if (viaje.timeoutId) {
                  clearTimeout(viaje.timeoutId);
                  viaje.timeoutId = null;
                }

                const nowTs = new Date().toISOString();
                const dataOriginal = viaje.data || {};
                const offeredInfo = viaje.lastOfferedConductorInfo || {};

                const conductorInfo = {
                  id: String(ws.id),
                  nombre: offeredInfo.nombre || dataOriginal.conductor || dataOriginal.conductor_nombre || dataOriginal.conductor_name || null,
                  telefono: offeredInfo.telefono || dataOriginal.conductor_telefono || dataOriginal.telf || null,
                  placa: offeredInfo.placa || dataOriginal.placa || null,
                  foto: offeredInfo.img_conductor || dataOriginal.img_conductor || dataOriginal.imagen_perfil || null,
                  img_vehiculo: offeredInfo.img_vehiculo || dataOriginal.img_vehiculo || dataOriginal.imagen_vehiculo || null,
                  color: offeredInfo.color || dataOriginal.color || null,
                  vehiculo: offeredInfo.vehiculo || dataOriginal.vehiculo || null,
                  rating: offeredInfo.rating || dataOriginal.conductor_rating || null
                };

                // NUEVO: intentar extraer lat/lng de offeredInfo o dataOriginal (varias claves posibles)
                function extractLocation(obj) {
                  if (!obj || typeof obj !== 'object') return null;
                  const tryNum = (v) => {
                    if (v === undefined || v === null) return null;
                    const n = parseFloat(v);
                    if (Number.isNaN(n)) return null;
                    return n;
                  };
                  const lat = tryNum(obj.latitud || obj.lat || obj.latitude || obj.latitud_origen || obj.latOrigen || obj.origin_lat);
                  const lng = tryNum(obj.longitud || obj.lng || obj.longitude || obj.longitud_origen || obj.lngOrigen || obj.origin_lng);
                  if (lat !== null && lng !== null) return { lat, lng };
                  return null;
                }

                const locFromOffered = extractLocation(offeredInfo);
                const locFromData = extractLocation(dataOriginal);
                const ubicacion_conductor = locFromOffered || locFromData || null;

                const payloadCliente = {
                  evento: 'conductor_encontrado',
                  id_viaje,
                  estado: 4, // conductor en camino / aceptó
                  timestamp: nowTs,
                  id_conductor: String(ws.id),
                  conductor: conductorInfo,
                  posicion_actual: dataOriginal.conductor_pos || null,
                  eta_segundos: dataOriginal.eta_segundos || null,
                  data: dataOriginal,
                  ubicacion_conductor: ubicacion_conductor
                };

                if (ubicacion_conductor) {
                  payloadCliente.latitud = ubicacion_conductor.lat;
                  payloadCliente.longitud = ubicacion_conductor.lng;
                }

                const wsCliente = clientes[String(viaje.cliente)];
                if (wsCliente && wsCliente.readyState === WebSocket.OPEN) {
                    try {
                        wsCliente.send(JSON.stringify(payloadCliente));
                        console.log(`[SERVER] enviado conductor_encontrado a cliente ${viaje.cliente} (ubicacion_incluida=${ubicacion_conductor ? 'si' : 'no'})`);
                    } catch (e) {
                        console.warn('[SERVER] error notificando cliente conductor_encontrado:', e.message || e);
                    }
                } else {
                    if (FIREBASE_NOTIFY_URL) {
                        try {
                            const pushBody = {
                                token: BACKEND_TOKEN,
                                id_viaje,
                                tipo: 'conductor_encontrado',
                                id_conductor: String(ws.id),
                                nombre: conductorInfo.nombre,
                                placa: conductorInfo.placa,
                                foto: conductorInfo.foto,
                                color: conductorInfo.color,
                                timestamp: nowTs,
                                estado: 4,
                                ubicacion_conductor: ubicacion_conductor
                            };
                            await axios.post(FIREBASE_NOTIFY_URL, pushBody, { timeout: 8000 });
                            console.log(`[SERVER] Solicitada notificación Firebase (conductor_encontrado) para viaje ${id_viaje}`);
                        } catch (err) {
                            console.warn('[SERVER] error solicitando push Firebase conductor_encontrado:', err.message || err);
                        }
                    } else {
                        console.warn('[SERVER] FIREBASE_NOTIFY_URL no configurada, no se pudo notificar al cliente (conductor_encontrado)');
                    }
                }

                return;
            } else {
                // rechazo del conductor
                viaje.rechazados.push(ws.id);
                if (viaje.currentOffered === ws.id) viaje.currentOffered = null;
                buscarYOtorgarNuevoConductor(id_viaje, viaje, ws.id);
            }
        }

        // Ubicación en tiempo real (conductor) -> reenviar al cliente si asignado
        if (data.action === 'ubicacion_conductor' && ws.tipo === 'conductor') {
            const id_viaje = String(data.id_viaje);
            const viaje = viajes[id_viaje];
            if (!viaje || viaje.asignado_a !== ws.id) {
                console.log('[SERVER] Ignorando ubicacion: viaje no existe o no asignado a este conductor');
                return;
            }
            if (viaje.canceled) {
              try { ws.send(JSON.stringify({ evento: 'viaje_cancelado', id_viaje })); } catch (e) {}
              const wsClienteCancel = clientes[String(viaje.cliente)];
              if (wsClienteCancel && wsClienteCancel.readyState === WebSocket.OPEN) {
                try { wsClienteCancel.send(JSON.stringify({ evento: 'viaje_cancelado', id_viaje })); } catch (e) {}
              }
              delete viajes[id_viaje];
              return;
            }

            const wsCliente = clientes[String(viaje.cliente)];
            if (wsCliente && wsCliente.readyState === WebSocket.OPEN) {
                wsCliente.send(JSON.stringify({
                    evento: 'ubicacion_conductor',
                    id_viaje,
                    id_conductor: ws.id,
                    latitud: data.latitud,
                    longitud: data.longitud
                }));
            } else {
                console.warn(`[SERVER] No se pudo enviar ubicacion -> cliente ${viaje.cliente} no conectado`);
            }
        }

        // Cliente puede registrar viaje en caso de fallo del backend
        if (data.action === 'cliente_registrar_viaje' && ws.tipo === 'cliente') {
            const id_viaje = String(data.id_viaje);
            const id_cliente = String(ws.id);
            if (!viajes[id_viaje]) {
                viajes[id_viaje] = {
                    cliente: id_cliente,
                    asignado_a: null,
                    data: data.data || {},
                    timeout: data.timeout || 15,
                    rechazados: [],
                    timeoutId: null,
                    currentOffered: null,
                    canceled: false,
                    offerAttempts: 0,
                    maxOffers: MAX_OFFERS,
                    lastOfferedConductorInfo: null
                };
                console.log(`[SERVER] viajes[${id_viaje}] creado via cliente_registrar_viaje`);
                try { ws.send(JSON.stringify({ evento: 'registro_ok', id_viaje })); } catch (e) {}
            } else {
                try { ws.send(JSON.stringify({ evento: 'registro_existente', id_viaje })); } catch (e) {}
            }
        }

        // Cliente cancela viaje (antes de ser aceptado)
        if (data.action === 'cancelar_viaje' && ws.tipo === 'cliente') {
          const id_viaje = String(data.id_viaje || '');
          const estado = data.estado !== undefined ? data.estado : null;
          console.log(`[WS ACTION] cancelar_viaje pedido por cliente=${ws.id} id_viaje=${id_viaje} estado=${estado}`);
          const viaje = viajes[id_viaje];
          if (!viaje) {
            try { ws.send(JSON.stringify({ evento: 'cancel_no_encontrado', id_viaje })); } catch (e) {}
            return;
          }
          if (String(viaje.cliente) !== String(ws.id)) {
            try { ws.send(JSON.stringify({ evento: 'cancel_no_autorizado', id_viaje })); } catch (e) {}
            return;
          }
          cancelarViaje(id_viaje, 'cancelado_por_cliente', estado);
          try { ws.send(JSON.stringify({ evento: 'cancel_confirmado', id_viaje })); } catch (e) {}
        }
    });

    ws.on('close', function () {
        if (ws.tipo === 'conductor' && ws.id) {
            delete conductores[ws.id];
            console.log(`Conductor desconectado: ${ws.id}`);
        }
        if (ws.tipo === 'cliente' && ws.id) {
            delete clientes[ws.id];
            console.log(`Cliente desconectado: ${ws.id}`);
        }
    });

    ws.on('error', (err) => {
        console.error('[WS] socket error id=' + ws.id + ' tipo=' + ws.tipo + ':', err && err.message ? err.message : err);
    });
});

async function ofrecerConductor(id_viaje, id_conductor) {
    const viaje = viajes[id_viaje];
    if (!viaje) {
        console.warn('[SERVER] ofrecerConductor - viaje no existe:', id_viaje);
        return null;
    }

    if (viaje.canceled) {
      console.log(`[SERVER] No ofrecer conductor: viaje ${id_viaje} está cancelado`);
      delete viajes[id_viaje];
      return null;
    }

    if (viaje.offerAttempts >= viaje.maxOffers) {
      console.log(`[SERVER] viaje ${id_viaje} alcanzó el máximo de ofertas (${viaje.offerAttempts}) -> finalizar`);
      await finalCancelFlow(id_viaje, viaje, 'limite_ofertas_superado', 2);
      return null;
    }

    if (!id_conductor) {
        viaje.offerAttempts = (viaje.offerAttempts || 0) + 1;
        console.log(`[SERVER] ofrecerConductor: no conductor inicial, offerAttempts=${viaje.offerAttempts}`);
        if (viaje.offerAttempts >= viaje.maxOffers) {
          await finalCancelFlow(id_viaje, viaje, 'limite_ofertas_superado', 2);
          return null;
        }

        const wsCliente = clientes[String(viaje.cliente)];
        if (wsCliente && wsCliente.readyState === WebSocket.OPEN) {
            wsCliente.send(JSON.stringify({ evento: 'sin_conductor', id_viaje }));
            console.log('[SERVER] Enviado sin_conductor al cliente', id_viaje);
        }
        delete viajes[id_viaje];
        return null;
    }

    const wsConductor = conductores[String(id_conductor)];
    if (wsConductor && wsConductor.readyState === WebSocket.OPEN) {
        viaje.currentOffered = id_conductor;
        viaje.offerAttempts = (viaje.offerAttempts || 0) + 1;
        console.log(`[SERVER] ofertando viaje ${id_viaje} al conductor ${id_conductor} (attempt ${viaje.offerAttempts}/${viaje.maxOffers})`);

        if (viaje.offerAttempts >= viaje.maxOffers) {
          console.log(`[SERVER] viaje ${id_viaje} alcanzó o superó maxOffers tras incrementar -> finalizar`);
          await finalCancelFlow(id_viaje, viaje, 'limite_ofertas_superado', 2);
          return null;
        }

        try {
          // EXTRA: extraer coordenadas desde viaje.data y promoverlas al payload raíz
          const dataPayload = viaje.data || {};
          const loc = extractLocationFromObj(dataPayload) || {};
          // Also check top-level fields in dataPayload for both origin and destination
          const latO = loc.latO || (dataPayload.latitud_origen ? parseFloat(dataPayload.latitud_origen) : null) || (dataPayload.latOrigen ? parseFloat(dataPayload.latOrigen) : null);
          const lngO = loc.lngO || (dataPayload.longitud_origen ? parseFloat(dataPayload.longitud_origen) : null) || (dataPayload.lngOrigen ? parseFloat(dataPayload.lngOrigen) : null);
          const latD = loc.latD || (dataPayload.latitud_destino ? parseFloat(dataPayload.latitud_destino) : null) || (dataPayload.latDestino ? parseFloat(dataPayload.latDestino) : null);
          const lngD = loc.lngD || (dataPayload.longitud_destino ? parseFloat(dataPayload.longitud_destino) : null) || (dataPayload.lngDestino ? parseFloat(dataPayload.lngDestino) : null);

          // Build payload; include coordinates explicitly so client (TripOverlay) can parse them reliably
          const payloadForConductor = {
              action: 'nuevo_viaje',
              id_viaje,
              cliente: viaje.data.id_cliente || viaje.cliente,
              direccion_origen: viaje.data.direccion_origen || viaje.data.direccion_origin || '',
              direccion_destino: viaje.data.direccion_destino || '',
              monto: viaje.data.monto || '',
              data: viaje.data,
              // Normalize coords at root level for client
              latitud_origen: latO !== undefined ? latO : null,
              longitud_origen: lngO !== undefined ? lngO : null,
              latitud_destino: latD !== undefined ? latD : null,
              longitud_destino: lngD !== undefined ? lngD : null
          };

          wsConductor.send(JSON.stringify(payloadForConductor));
          console.log(`[SERVER] Enviado oferta_viaje al conductor ${id_conductor} (coords_incluidas=${(latO && lngO && latD && lngD) ? 'si' : 'no'})`);
        } catch (e) {
          console.warn('[SERVER] error enviando oferta por WS:', e);
        }

        if (viaje.timeoutId) {
          try { clearTimeout(viaje.timeoutId); } catch(e) {}
          viaje.timeoutId = null;
        }

        scheduleOfferTimeout(id_viaje, id_conductor);
    } else {
        viaje.offerAttempts = (viaje.offerAttempts || 0) + 1;
        console.log(`[SERVER] conductor ${id_conductor} no conectado. Se intentó notificar push. offerAttempts=${viaje.offerAttempts}`);
        if (viaje.offerAttempts >= viaje.maxOffers) {
          await finalCancelFlow(id_viaje, viaje, 'limite_ofertas_superado', 2);
          return null;
        }

        // include coordinates into push extra if available
        const locData = extractLocationFromObj(viaje.data || {}) || {};
        const extraPush = {};
        if (locData.latO && locData.lngO) { extraPush.latitud_origen = locData.latO; extraPush.longitud_origen = locData.lngO; }
        if (locData.latD && locData.lngD) { extraPush.latitud_destino = locData.latD; extraPush.longitud_destino = locData.lngD; }

        await notificarPushConductor(id_viaje, id_conductor, extraPush);

        scheduleOfferTimeout(id_viaje, id_conductor);
    }

    return id_conductor;
}

async function buscarYOtorgarNuevoConductor(id_viaje, viaje, anterior_conductor) {
    console.log('[SERVER] buscarYOtorgarNuevoConductor inicio para', id_viaje, 'anterior:', anterior_conductor);
    if (!viaje || viaje.canceled) {
      console.log(`[SERVER] buscarYOtorgarNuevoConductor abortado: viaje ${id_viaje} cancelado o inexistente`);
      if (viaje && viaje.timeoutId) { try { clearTimeout(viaje.timeoutId); } catch(e) {} viaje.timeoutId = null; }
      delete viajes[id_viaje];
      return;
    }

    if ((viaje.offerAttempts || 0) >= viaje.maxOffers) {
      console.log(`[SERVER] buscarYOtorgarNuevoConductor: viaje ${id_viaje} alcanzó max offers (${viaje.offerAttempts}) -> finalizar`);
      await finalCancelFlow(id_viaje, viaje, 'limite_ofertas_superado', 2);
      return;
    }

    const conductorResp = await solicitarNuevoConductor(id_viaje, viaje.cliente, viaje.rechazados);
    console.log('[SERVER] nuevo_conductor obtenido:', conductorResp);
    if (conductorResp && conductorResp.id_conductor) {
        const nuevoId = String(conductorResp.id_conductor);
        // Guardamos info del conductor devuelta por la API para usarla luego si acepta
        viaje.lastOfferedConductorInfo = {
          id: nuevoId,
          nombre: conductorResp.conductor || conductorResp.name || null,
          telefono: conductorResp.conductor_telefono || null,
          img_conductor: conductorResp.img_conductor || null,
          img_vehiculo: conductorResp.img_vehiculo || null,
          placa: conductorResp.placa || null,
          color: conductorResp.color || null,
          vehiculo: conductorResp.vehiculo || null,
          rating: conductorResp.rating || null,
          // si la API devuelve lat/lng incluirlo para que podamos usarlo si el conductor acepta
          latitud: conductorResp.latitud || conductorResp.lat || conductorResp.latitude || null,
          longitud: conductorResp.longitud || conductorResp.lng || conductorResp.longitude || null
        };
        if (!viaje.rechazados.includes(nuevoId)) viaje.rechazados.push(nuevoId);
        await ofrecerConductor(id_viaje, nuevoId);
    } else {
        viaje.offerAttempts = (viaje.offerAttempts || 0) + 1;
        console.log(`[SERVER] No se encontró nuevo conductor para ${id_viaje}. offerAttempts=${viaje.offerAttempts}`);
        if (viaje.offerAttempts >= viaje.maxOffers) {
          await finalCancelFlow(id_viaje, viaje, 'sin_conductores_o_limite_excedido', 2);
          return;
        }

        const wsCliente = clientes[String(viaje.cliente)];
        if (wsCliente && wsCliente.readyState === WebSocket.OPEN) {
            try { wsCliente.send(JSON.stringify({ evento: 'sin_conductor', id_viaje })); } catch (e) { console.warn('[SERVER] error enviando sin_conductor:', e); }
            console.log('[SERVER] enviado sin_conductor al cliente por no haber nuevo conductor');
        }
        if (viaje.timeoutId) { try { clearTimeout(viaje.timeoutId); } catch(e) {} viaje.timeoutId = null; }
        delete viajes[id_viaje];
    }
}

process.on('uncaughtException', (err) => {
    console.error('[SERVER] uncaughtException', err && err.stack ? err.stack : err);
});

console.log('Servidor WebSocket TaxiGo corriendo en ws://localhost:9080');