import { Repository, FindOptionsWhere, ILike, In, Between, MoreThanOrEqual, LessThanOrEqual, IsNull } from 'typeorm';
import {
  OrdenTransporte,
  EstadoOrdenTransporte,
  ModalidadServicio,
  TipoEquipo,
  TipoCarga,
} from '../entities';

// =============================================================================
// INTERFACES Y TIPOS
// =============================================================================

export interface OrdenTransporteSearchParams {
  tenantId: string;
  search?: string;
  estado?: EstadoOrdenTransporte;
  estados?: EstadoOrdenTransporte[];
  clienteId?: string;
  shipperId?: string;
  consigneeId?: string;
  modalidad?: ModalidadServicio;
  tipoCarga?: TipoCarga;
  fechaDesde?: Date;
  fechaHasta?: Date;
  viajeId?: string;
  sinAsignar?: boolean;
  limit?: number;
  offset?: number;
  orderBy?: 'createdAt' | 'fechaRecoleccion' | 'fechaEntrega' | 'total';
  orderDir?: 'ASC' | 'DESC';
}

export interface CreateOrdenTransporteDto {
  clienteId: string;
  referenciaCliente?: string;
  modalidadServicio?: ModalidadServicio;
  tipoEquipo?: TipoEquipo;
  tipoCarga?: TipoCarga;
  prioridad?: 'NORMAL' | 'URGENTE' | 'CRITICA';

  // Shipper
  shipperId: string;
  shipperNombre: string;

  // Consignee
  consigneeId: string;
  consigneeNombre: string;

  // Origen
  origenDireccion: string;
  origenCiudad?: string;
  origenEstado?: string;
  origenCodigoPostal?: string;
  origenLatitud?: number;
  origenLongitud?: number;
  origenContacto?: string;
  origenTelefono?: string;
  fechaRecoleccionProgramada?: Date;

  // Destino
  destinoDireccion: string;
  destinoCiudad?: string;
  destinoEstado?: string;
  destinoCodigoPostal?: string;
  destinoLatitud?: number;
  destinoLongitud?: number;
  destinoContacto?: string;
  destinoTelefono?: string;
  fechaEntregaProgramada?: Date;

  // Carga
  descripcionCarga?: string;
  pesoKg?: number;
  volumenM3?: number;
  piezas?: number;
  pallets?: number;
  valorDeclarado?: number;

  // Requisitos
  requiereTemperatura?: boolean;
  temperaturaMin?: number;
  temperaturaMax?: number;
  requiereGps?: boolean;
  requiereEscolta?: boolean;
  instruccionesEspeciales?: string;

  // Tarifa
  tarifaId?: string;
  tarifaBase?: number;
  recargos?: number;
  descuentos?: number;

  observaciones?: string;
}

export interface UpdateOrdenTransporteDto extends Partial<CreateOrdenTransporteDto> {
  estado?: EstadoOrdenTransporte;
}

export interface OrdenTransporteTimeline {
  id: string;
  ordenId: string;
  estado: EstadoOrdenTransporte;
  estadoAnterior?: EstadoOrdenTransporte;
  fecha: Date;
  usuario: string;
  comentario?: string;
}

export interface CostosEstimados {
  ordenId: string;
  tarifaBase: number;
  combustible: number;
  peajes: number;
  viaticos: number;
  recargos: number;
  descuentos: number;
  subtotal: number;
  iva: number;
  total: number;
  distanciaEstimadaKm?: number;
  tiempoEstimadoHoras?: number;
}

export interface OrdenTransporteStats {
  total: number;
  porEstado: Record<EstadoOrdenTransporte, number>;
  porModalidad: Record<ModalidadServicio, number>;
  totalIngresos: number;
  promedioIngreso: number;
  otifRate: number; // On-Time In-Full rate
  tiempoPromedioEntregaHoras: number;
}

export interface DateRange {
  from: Date;
  to: Date;
}

/**
 * Contexto de servicio para operaciones con tenant y usuario
 */
export interface ServiceContext {
  tenantId: string;
  userId: string;
  correlationId?: string;
}

/**
 * DTO para asignar unidad y operador a una OT
 */
export interface AsignarUnidadDto {
  unidadId: string;
  operadorId: string;
  remolqueId?: string;
  fechaSalidaProgramada?: Date;
  fechaLlegadaProgramada?: Date;
}

/**
 * Resultado del calculo de tarifa
 */
export interface TarifaCalculada {
  ordenId: string;
  tarifaId?: string;
  tarifaCodigo?: string;
  laneId?: string;
  // Desglose de tarifa
  tarifaBase: number;
  montoVariable: number;
  // Recargos
  recargoCombustible: number;
  recargoManiobras: number;
  recargoEspera: number;
  recargoOtros: number;
  totalRecargos: number;
  // Descuentos
  descuentos: number;
  // Totales
  subtotal: number;
  iva: number;
  ivaRate: number;
  total: number;
  moneda: string;
  // Informacion adicional
  distanciaEstimadaKm?: number;
  tiempoEstimadoHoras?: number;
  minimoAplicado: boolean;
}

/**
 * DTO para validacion de documentos de operador
 */
export interface DocumentosOperadorStatus {
  operadorId: string;
  documentosVigentes: boolean;
  licenciaVigente: boolean;
  certificadoFisicoVigente: boolean;
  antidopingVigente: boolean;
  capacitacionMpVigente?: boolean;
  documentosVencidos: string[];
}

// =============================================================================
// SERVICIO ORDENES DE TRANSPORTE
// =============================================================================

export class OrdenesTransporteService {
  constructor(private readonly otRepository: Repository<OrdenTransporte>) {}

  // ===========================================================================
  // METODOS CRUD BASICOS
  // ===========================================================================

  /**
   * Crear nueva Orden de Transporte
   */
  async create(
    tenantId: string,
    dto: CreateOrdenTransporteDto,
    createdById: string
  ): Promise<OrdenTransporte> {
    const codigo = await this.generateCodigo(tenantId);

    // Calcular totales si se proporciona tarifa
    let subtotal = dto.tarifaBase || 0;
    subtotal += dto.recargos || 0;
    subtotal -= dto.descuentos || 0;
    const iva = subtotal * 0.16; // IVA 16% Mexico
    const total = subtotal + iva;

    const orden = this.otRepository.create({
      tenantId,
      codigo,
      numeroOt: codigo,
      clienteId: dto.clienteId,
      referenciaCliente: dto.referenciaCliente,
      modalidadServicio: dto.modalidadServicio || ModalidadServicio.FTL,
      tipoCarga: dto.tipoCarga || TipoCarga.GENERAL,

      // Shipper
      shipperId: dto.shipperId,
      shipperNombre: dto.shipperNombre,

      // Consignee
      consigneeId: dto.consigneeId,
      consigneeNombre: dto.consigneeNombre,

      // Origen
      origenDireccion: dto.origenDireccion,
      origenCiudad: dto.origenCiudad,
      origenEstado: dto.origenEstado,
      origenCodigoPostal: dto.origenCodigoPostal,
      origenLatitud: dto.origenLatitud,
      origenLongitud: dto.origenLongitud,
      origenContacto: dto.origenContacto,
      origenTelefono: dto.origenTelefono,
      fechaRecoleccionProgramada: dto.fechaRecoleccionProgramada,

      // Destino
      destinoDireccion: dto.destinoDireccion,
      destinoCiudad: dto.destinoCiudad,
      destinoEstado: dto.destinoEstado,
      destinoCodigoPostal: dto.destinoCodigoPostal,
      destinoLatitud: dto.destinoLatitud,
      destinoLongitud: dto.destinoLongitud,
      destinoContacto: dto.destinoContacto,
      destinoTelefono: dto.destinoTelefono,
      fechaEntregaProgramada: dto.fechaEntregaProgramada,

      // Carga
      descripcionCarga: dto.descripcionCarga,
      pesoKg: dto.pesoKg,
      volumenM3: dto.volumenM3,
      piezas: dto.piezas,
      pallets: dto.pallets,
      valorDeclarado: dto.valorDeclarado,

      // Requisitos
      requiereTemperatura: dto.requiereTemperatura || false,
      temperaturaMin: dto.temperaturaMin,
      temperaturaMax: dto.temperaturaMax,
      requiereGps: dto.requiereGps || false,
      requiereEscolta: dto.requiereEscolta || false,
      instruccionesEspeciales: dto.instruccionesEspeciales,

      // Tarifa
      tarifaId: dto.tarifaId,
      tarifaBase: dto.tarifaBase,
      recargos: dto.recargos || 0,
      descuentos: dto.descuentos || 0,
      subtotal,
      iva,
      total,

      observaciones: dto.observaciones,
      estado: EstadoOrdenTransporte.BORRADOR,
      createdById,
    });

    return this.otRepository.save(orden);
  }

  /**
   * Buscar orden por ID
   */
  async findById(tenantId: string, id: string): Promise<OrdenTransporte | null> {
    return this.otRepository.findOne({
      where: { id, tenantId },
    });
  }

  /**
   * Buscar orden por codigo
   */
  async findByCodigo(tenantId: string, codigo: string): Promise<OrdenTransporte | null> {
    return this.otRepository.findOne({
      where: { codigo, tenantId },
    });
  }

  /**
   * Listar ordenes con filtros y paginacion
   * Soporta dos firmas:
   * - findAll(tenantId: string, filters: object) - nueva firma
   * - findAll(params: { tenantId, ...filters }) - compatibilidad con controller existente
   */
  async findAll(
    tenantIdOrParams: string | OrdenTransporteSearchParams,
    filters: Omit<OrdenTransporteSearchParams, 'tenantId'> = {}
  ): Promise<{ data: OrdenTransporte[]; total: number; page: number; pageSize: number }> {
    // Soportar ambas firmas: findAll(tenantId, filters) y findAll({ tenantId, ...filters })
    let tenantId: string;
    let actualFilters: Omit<OrdenTransporteSearchParams, 'tenantId'>;

    if (typeof tenantIdOrParams === 'string') {
      tenantId = tenantIdOrParams;
      actualFilters = filters;
    } else {
      tenantId = tenantIdOrParams.tenantId;
      const { tenantId: _, ...rest } = tenantIdOrParams;
      actualFilters = rest;
    }

    const {
      search,
      estado,
      estados,
      clienteId,
      shipperId,
      consigneeId,
      modalidad,
      tipoCarga,
      fechaDesde,
      fechaHasta,
      viajeId,
      sinAsignar,
      limit = 50,
      offset = 0,
      orderBy = 'createdAt',
      orderDir = 'DESC',
    } = actualFilters;

    const where: FindOptionsWhere<OrdenTransporte>[] = [];
    const baseWhere: FindOptionsWhere<OrdenTransporte> = { tenantId, deletedAt: IsNull() };

    // Filtros de estado
    if (estado) {
      baseWhere.estado = estado;
    } else if (estados && estados.length > 0) {
      baseWhere.estado = In(estados);
    }

    // Filtros de cliente/shipper/consignee
    if (clienteId) baseWhere.clienteId = clienteId;
    if (shipperId) baseWhere.shipperId = shipperId;
    if (consigneeId) baseWhere.consigneeId = consigneeId;

    // Filtros de servicio
    if (modalidad) baseWhere.modalidadServicio = modalidad;
    if (tipoCarga) baseWhere.tipoCarga = tipoCarga;

    // Filtro de viaje
    if (viajeId) baseWhere.viajeId = viajeId;
    if (sinAsignar) baseWhere.viajeId = IsNull();

    // Filtros de fecha
    if (fechaDesde && fechaHasta) {
      baseWhere.fechaRecoleccionProgramada = Between(fechaDesde, fechaHasta);
    } else if (fechaDesde) {
      baseWhere.fechaRecoleccionProgramada = MoreThanOrEqual(fechaDesde);
    } else if (fechaHasta) {
      baseWhere.fechaRecoleccionProgramada = LessThanOrEqual(fechaHasta);
    }

    // Busqueda por texto
    if (search) {
      where.push(
        { ...baseWhere, codigo: ILike(`%${search}%`) },
        { ...baseWhere, numeroOt: ILike(`%${search}%`) },
        { ...baseWhere, referenciaCliente: ILike(`%${search}%`) },
        { ...baseWhere, shipperNombre: ILike(`%${search}%`) },
        { ...baseWhere, consigneeNombre: ILike(`%${search}%`) },
        { ...baseWhere, origenCiudad: ILike(`%${search}%`) },
        { ...baseWhere, destinoCiudad: ILike(`%${search}%`) }
      );
    } else {
      where.push(baseWhere);
    }

    // Mapeo de campo de ordenamiento
    const orderField = this.mapOrderField(orderBy);

    const [data, total] = await this.otRepository.findAndCount({
      where,
      take: limit,
      skip: offset,
      order: { [orderField]: orderDir },
    });

    return {
      data,
      total,
      page: Math.floor(offset / limit) + 1,
      pageSize: limit,
    };
  }

  /**
   * Buscar ordenes por estado
   */
  async findByStatus(
    tenantId: string,
    status: EstadoOrdenTransporte,
    limit: number = 100
  ): Promise<OrdenTransporte[]> {
    return this.otRepository.find({
      where: { tenantId, estado: status, deletedAt: IsNull() },
      order: { fechaRecoleccionProgramada: 'ASC' },
      take: limit,
    });
  }

  /**
   * Actualizar orden de transporte
   */
  async update(
    tenantId: string,
    id: string,
    dto: UpdateOrdenTransporteDto,
    updatedById: string
  ): Promise<OrdenTransporte | null> {
    const orden = await this.findById(tenantId, id);
    if (!orden) return null;

    // No permitir modificaciones en estados finales
    const estadosFinales = [
      EstadoOrdenTransporte.ENTREGADA,
      EstadoOrdenTransporte.FACTURADA,
      EstadoOrdenTransporte.CANCELADA,
    ];
    if (estadosFinales.includes(orden.estado)) {
      throw new Error(`No se puede modificar una OT en estado ${orden.estado}`);
    }

    // Recalcular totales si cambian valores de tarifa
    let subtotal = orden.subtotal;
    let iva = orden.iva;
    let total = orden.total;

    if (dto.tarifaBase !== undefined || dto.recargos !== undefined || dto.descuentos !== undefined) {
      subtotal = (dto.tarifaBase ?? orden.tarifaBase) || 0;
      subtotal += (dto.recargos ?? orden.recargos) || 0;
      subtotal -= (dto.descuentos ?? orden.descuentos) || 0;
      iva = subtotal * 0.16;
      total = subtotal + iva;
    }

    Object.assign(orden, {
      ...dto,
      subtotal,
      iva,
      total,
      updatedById,
    });

    return this.otRepository.save(orden);
  }

  /**
   * Cancelar orden de transporte
   */
  async cancel(
    tenantId: string,
    id: string,
    reason: string,
    cancelledById: string
  ): Promise<OrdenTransporte | null> {
    const orden = await this.findById(tenantId, id);
    if (!orden) return null;

    // No se puede cancelar una OT en transito o ya entregada
    const estadosNoCancelables = [
      EstadoOrdenTransporte.EN_TRANSITO,
      EstadoOrdenTransporte.COMPLETADA,
      EstadoOrdenTransporte.ENTREGADA,
      EstadoOrdenTransporte.FACTURADA,
      EstadoOrdenTransporte.CANCELADA,
    ];
    if (estadosNoCancelables.includes(orden.estado)) {
      throw new Error(`No se puede cancelar una OT en estado ${orden.estado}`);
    }

    // Si esta asignada a un viaje, desasignar primero
    if (orden.viajeId) {
      orden.viajeId = undefined as unknown as string;
    }

    orden.estado = EstadoOrdenTransporte.CANCELADA;
    orden.observaciones = `${orden.observaciones || ''}\n[CANCELADA ${new Date().toISOString()}] Motivo: ${reason}`.trim();
    orden.updatedById = cancelledById;

    return this.otRepository.save(orden);
  }

  // ===========================================================================
  // ASIGNACION A VIAJES
  // ===========================================================================

  /**
   * Asignar orden a un viaje
   */
  async assignToViaje(
    tenantId: string,
    orderId: string,
    viajeId: string,
    assignedById: string
  ): Promise<OrdenTransporte | null> {
    const orden = await this.findById(tenantId, orderId);
    if (!orden) return null;

    // Solo se pueden asignar OTs confirmadas o pendientes
    const estadosAsignables = [
      EstadoOrdenTransporte.CONFIRMADA,
      EstadoOrdenTransporte.PENDIENTE,
      EstadoOrdenTransporte.SOLICITADA,
    ];
    if (!estadosAsignables.includes(orden.estado)) {
      throw new Error(`No se puede asignar una OT en estado ${orden.estado}`);
    }

    // Si ya esta asignada a otro viaje, validar
    if (orden.viajeId && orden.viajeId !== viajeId) {
      throw new Error(`La OT ya esta asignada al viaje ${orden.viajeId}`);
    }

    orden.viajeId = viajeId;
    orden.estado = EstadoOrdenTransporte.ASIGNADA;
    orden.updatedById = assignedById;

    return this.otRepository.save(orden);
  }

  /**
   * Desasignar orden de un viaje
   */
  async unassignFromViaje(
    tenantId: string,
    orderId: string,
    unassignedById: string
  ): Promise<OrdenTransporte | null> {
    const orden = await this.findById(tenantId, orderId);
    if (!orden) return null;

    // Solo se pueden desasignar OTs que esten asignadas pero no en proceso
    if (orden.estado === EstadoOrdenTransporte.EN_PROCESO ||
        orden.estado === EstadoOrdenTransporte.EN_TRANSITO) {
      throw new Error(`No se puede desasignar una OT que ya esta en proceso/transito`);
    }

    orden.viajeId = undefined as unknown as string;
    orden.estado = EstadoOrdenTransporte.CONFIRMADA;
    orden.updatedById = unassignedById;

    return this.otRepository.save(orden);
  }

  /**
   * Obtener ordenes asignadas a un viaje
   */
  async findByViaje(tenantId: string, viajeId: string): Promise<OrdenTransporte[]> {
    return this.otRepository.find({
      where: { tenantId, viajeId, deletedAt: IsNull() },
      order: { createdAt: 'ASC' },
    });
  }

  /**
   * Obtener ordenes sin asignar (disponibles para programacion)
   */
  async findUnassigned(tenantId: string, limit: number = 100): Promise<OrdenTransporte[]> {
    return this.otRepository.find({
      where: {
        tenantId,
        viajeId: IsNull(),
        estado: In([
          EstadoOrdenTransporte.CONFIRMADA,
          EstadoOrdenTransporte.PENDIENTE,
          EstadoOrdenTransporte.SOLICITADA,
        ]),
        deletedAt: IsNull(),
      },
      order: { fechaRecoleccionProgramada: 'ASC' },
      take: limit,
    });
  }

  // ===========================================================================
  // TIMELINE E HISTORIAL
  // ===========================================================================

  /**
   * Obtener timeline de la orden (historial de estados)
   * Nota: Esta es una implementacion simplificada. En produccion se debe
   * implementar una tabla de historial separada.
   */
  async getTimeline(tenantId: string, id: string): Promise<OrdenTransporteTimeline[]> {
    const orden = await this.findById(tenantId, id);
    if (!orden) return [];

    // Generar timeline basico con los datos disponibles
    const timeline: OrdenTransporteTimeline[] = [
      {
        id: `${orden.id}-created`,
        ordenId: orden.id,
        estado: EstadoOrdenTransporte.BORRADOR,
        fecha: orden.createdAt,
        usuario: orden.createdById,
        comentario: 'Orden creada',
      },
    ];

    // Si hay cambio de estado, agregar entrada
    if (orden.estado !== EstadoOrdenTransporte.BORRADOR) {
      timeline.push({
        id: `${orden.id}-current`,
        ordenId: orden.id,
        estado: orden.estado,
        estadoAnterior: EstadoOrdenTransporte.BORRADOR,
        fecha: orden.updatedAt,
        usuario: orden.updatedById || orden.createdById,
        comentario: `Estado cambiado a ${orden.estado}`,
      });
    }

    // Si esta asignada a viaje
    if (orden.viajeId) {
      timeline.push({
        id: `${orden.id}-assigned`,
        ordenId: orden.id,
        estado: EstadoOrdenTransporte.ASIGNADA,
        fecha: orden.updatedAt,
        usuario: orden.updatedById || orden.createdById,
        comentario: `Asignada a viaje ${orden.viajeId}`,
      });
    }

    return timeline.sort((a, b) => a.fecha.getTime() - b.fecha.getTime());
  }

  // ===========================================================================
  // CALCULOS DE COSTOS
  // ===========================================================================

  /**
   * Calcular costos estimados de la orden
   */
  async calculateCosts(tenantId: string, id: string): Promise<CostosEstimados | null> {
    const orden = await this.findById(tenantId, id);
    if (!orden) return null;

    // Calcular distancia si hay coordenadas
    let distanciaEstimadaKm: number | undefined;
    let tiempoEstimadoHoras: number | undefined;

    if (orden.origenLatitud && orden.origenLongitud &&
        orden.destinoLatitud && orden.destinoLongitud) {
      distanciaEstimadaKm = this.calculateDistance(
        orden.origenLatitud,
        orden.origenLongitud,
        orden.destinoLatitud,
        orden.destinoLongitud
      );
      // Velocidad promedio de 60 km/h considerando paradas
      tiempoEstimadoHoras = distanciaEstimadaKm / 60;
    }

    // Estimaciones basadas en distancia (si esta disponible)
    const tarifaBase = orden.tarifaBase || 0;

    // Costo combustible estimado: 2.5 litros/km * precio diesel (~$22/litro)
    const combustible = distanciaEstimadaKm ? distanciaEstimadaKm * 2.5 * 22 / 100 : 0;

    // Peajes estimados: promedio $2 por km en autopistas
    const peajes = distanciaEstimadaKm ? distanciaEstimadaKm * 0.5 : 0;

    // Viaticos: $500 por dia estimado
    const viaticos = tiempoEstimadoHoras ? Math.ceil(tiempoEstimadoHoras / 10) * 500 : 0;

    const recargos = orden.recargos || 0;
    const descuentos = orden.descuentos || 0;

    const subtotal = tarifaBase + combustible + peajes + viaticos + recargos - descuentos;
    const iva = subtotal * 0.16;
    const total = subtotal + iva;

    return {
      ordenId: orden.id,
      tarifaBase,
      combustible: Math.round(combustible * 100) / 100,
      peajes: Math.round(peajes * 100) / 100,
      viaticos,
      recargos,
      descuentos,
      subtotal: Math.round(subtotal * 100) / 100,
      iva: Math.round(iva * 100) / 100,
      total: Math.round(total * 100) / 100,
      distanciaEstimadaKm: distanciaEstimadaKm ? Math.round(distanciaEstimadaKm) : undefined,
      tiempoEstimadoHoras: tiempoEstimadoHoras ? Math.round(tiempoEstimadoHoras * 10) / 10 : undefined,
    };
  }

  // ===========================================================================
  // ESTADISTICAS
  // ===========================================================================

  /**
   * Obtener estadisticas de ordenes de transporte
   */
  async getStatistics(tenantId: string, dateRange: DateRange): Promise<OrdenTransporteStats> {
    const { from, to } = dateRange;

    // Obtener todas las ordenes del periodo
    const ordenes = await this.otRepository.find({
      where: {
        tenantId,
        createdAt: Between(from, to),
        deletedAt: IsNull(),
      },
    });

    // Conteo por estado
    const porEstado: Record<EstadoOrdenTransporte, number> = {
      [EstadoOrdenTransporte.BORRADOR]: 0,
      [EstadoOrdenTransporte.PENDIENTE]: 0,
      [EstadoOrdenTransporte.SOLICITADA]: 0,
      [EstadoOrdenTransporte.CONFIRMADA]: 0,
      [EstadoOrdenTransporte.ASIGNADA]: 0,
      [EstadoOrdenTransporte.EN_PROCESO]: 0,
      [EstadoOrdenTransporte.EN_TRANSITO]: 0,
      [EstadoOrdenTransporte.COMPLETADA]: 0,
      [EstadoOrdenTransporte.ENTREGADA]: 0,
      [EstadoOrdenTransporte.FACTURADA]: 0,
      [EstadoOrdenTransporte.CANCELADA]: 0,
    };

    // Conteo por modalidad
    const porModalidad: Record<ModalidadServicio, number> = {
      [ModalidadServicio.FTL]: 0,
      [ModalidadServicio.LTL]: 0,
      [ModalidadServicio.DEDICADO]: 0,
      [ModalidadServicio.EXPRESS]: 0,
      [ModalidadServicio.CONSOLIDADO]: 0,
    };

    let totalIngresos = 0;
    let entregadasATiempo = 0;
    let totalEntregadas = 0;
    let tiempoTotalEntrega = 0;

    for (const orden of ordenes) {
      porEstado[orden.estado]++;
      porModalidad[orden.modalidadServicio]++;
      totalIngresos += Number(orden.total) || 0;

      // Calcular OTIF (On-Time In-Full) para ordenes entregadas
      if (orden.estado === EstadoOrdenTransporte.ENTREGADA ||
          orden.estado === EstadoOrdenTransporte.FACTURADA) {
        totalEntregadas++;

        // Verificar si fue a tiempo (entregada antes de fecha programada)
        if (orden.fechaEntregaProgramada && orden.updatedAt <= orden.fechaEntregaProgramada) {
          entregadasATiempo++;
        }

        // Calcular tiempo de entrega
        if (orden.fechaRecoleccionProgramada) {
          const tiempoEntrega = orden.updatedAt.getTime() - orden.fechaRecoleccionProgramada.getTime();
          tiempoTotalEntrega += tiempoEntrega / (1000 * 60 * 60); // Convertir a horas
        }
      }
    }

    const total = ordenes.length;
    const otifRate = totalEntregadas > 0 ? (entregadasATiempo / totalEntregadas) * 100 : 0;
    const tiempoPromedioEntregaHoras = totalEntregadas > 0 ? tiempoTotalEntrega / totalEntregadas : 0;

    return {
      total,
      porEstado,
      porModalidad,
      totalIngresos: Math.round(totalIngresos * 100) / 100,
      promedioIngreso: total > 0 ? Math.round((totalIngresos / total) * 100) / 100 : 0,
      otifRate: Math.round(otifRate * 10) / 10,
      tiempoPromedioEntregaHoras: Math.round(tiempoPromedioEntregaHoras * 10) / 10,
    };
  }

  // ===========================================================================
  // CAMBIOS DE ESTADO
  // ===========================================================================

  /**
   * Confirmar orden (cambiar de BORRADOR/SOLICITADA a CONFIRMADA)
   */
  async confirm(
    tenantId: string,
    id: string,
    confirmedById: string
  ): Promise<OrdenTransporte | null> {
    return this.changeStatus(tenantId, id, EstadoOrdenTransporte.CONFIRMADA, confirmedById);
  }

  /**
   * Marcar orden en proceso
   */
  async startProcess(
    tenantId: string,
    id: string,
    startedById: string
  ): Promise<OrdenTransporte | null> {
    return this.changeStatus(tenantId, id, EstadoOrdenTransporte.EN_PROCESO, startedById);
  }

  /**
   * Marcar orden en transito
   */
  async startTransit(
    tenantId: string,
    id: string,
    startedById: string
  ): Promise<OrdenTransporte | null> {
    return this.changeStatus(tenantId, id, EstadoOrdenTransporte.EN_TRANSITO, startedById);
  }

  /**
   * Completar orden (carga entregada)
   */
  async complete(
    tenantId: string,
    id: string,
    completedById: string
  ): Promise<OrdenTransporte | null> {
    return this.changeStatus(tenantId, id, EstadoOrdenTransporte.COMPLETADA, completedById);
  }

  /**
   * Marcar como entregada (con POD)
   */
  async deliver(
    tenantId: string,
    id: string,
    deliveredById: string
  ): Promise<OrdenTransporte | null> {
    return this.changeStatus(tenantId, id, EstadoOrdenTransporte.ENTREGADA, deliveredById);
  }

  // ===========================================================================
  // ASIGNACION DE UNIDAD Y OPERADOR (MAI-003 REQUERIMIENTO)
  // ===========================================================================

  /**
   * Asignar unidad y operador a una orden de transporte.
   * Crea un viaje asociado y valida:
   * - Unidad disponible
   * - Operador con documentos vigentes
   * - Orden en estado CONFIRMADA
   *
   * @param id - ID de la orden de transporte
   * @param dto - Datos de asignacion (unidadId, operadorId, remolqueId opcional)
   * @param ctx - Contexto del servicio (tenantId, userId)
   * @returns Objeto con la orden actualizada y el viaje creado
   */
  async asignarUnidad(
    id: string,
    dto: AsignarUnidadDto,
    ctx: ServiceContext
  ): Promise<{ orden: OrdenTransporte; viajeId: string } | null> {
    const { tenantId, userId } = ctx;

    // 1. Obtener la orden
    const orden = await this.findById(tenantId, id);
    if (!orden) {
      throw new Error('Orden de transporte no encontrada');
    }

    // 2. Validar que la orden este en estado valido para asignacion
    const estadosAsignables = [
      EstadoOrdenTransporte.CONFIRMADA,
      EstadoOrdenTransporte.PENDIENTE,
      EstadoOrdenTransporte.SOLICITADA,
    ];
    if (!estadosAsignables.includes(orden.estado)) {
      throw new Error(
        `No se puede asignar unidad a una OT en estado ${orden.estado}. ` +
        `Estados permitidos: ${estadosAsignables.join(', ')}`
      );
    }

    // 3. Validar que la orden no tenga ya un viaje asignado
    if (orden.viajeId) {
      throw new Error(
        `La orden ya tiene un viaje asignado: ${orden.viajeId}. ` +
        `Desasigne primero el viaje actual.`
      );
    }

    // 4. Validar unidad disponible
    const unidadValidation = await this.validateUnidadDisponible(
      tenantId,
      dto.unidadId,
      dto.fechaSalidaProgramada || orden.fechaRecoleccionProgramada,
      dto.fechaLlegadaProgramada || orden.fechaEntregaProgramada
    );
    if (!unidadValidation.disponible) {
      throw new Error(
        `Unidad ${dto.unidadId} no disponible: ${unidadValidation.razon}`
      );
    }

    // 5. Validar documentos del operador
    const docsStatus = await this.validateDocumentosOperador(tenantId, dto.operadorId);
    if (!docsStatus.documentosVigentes) {
      throw new Error(
        `Operador ${dto.operadorId} tiene documentos vencidos: ${docsStatus.documentosVencidos.join(', ')}. ` +
        `Debe renovar los documentos antes de asignar viajes.`
      );
    }

    // 6. Generar codigo de viaje
    const year = new Date().getFullYear();
    const month = String(new Date().getMonth() + 1).padStart(2, '0');
    const viajeCount = await this.otRepository.manager.query(
      `SELECT COUNT(*) as count FROM transport.viajes WHERE tenant_id = $1`,
      [tenantId]
    );
    const viajeNumero = parseInt(viajeCount[0]?.count || '0', 10) + 1;
    const viajeCodigo = `VJ-${year}${month}-${String(viajeNumero).padStart(6, '0')}`;

    // 7. Crear viaje asociado
    const viajeInsertResult = await this.otRepository.manager.query(
      `INSERT INTO transport.viajes (
        tenant_id, codigo, numero_viaje, unidad_id, remolque_id, operador_id,
        cliente_id, origen_principal, origen_ciudad, destino_principal, destino_ciudad,
        distancia_estimada_km, tiempo_estimado_horas,
        fecha_salida_programada, fecha_llegada_programada, estado,
        created_by_id, created_at, updated_at
      ) VALUES (
        $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, 'PLANEADO', $16, NOW(), NOW()
      ) RETURNING id`,
      [
        tenantId,
        viajeCodigo,
        viajeCodigo,
        dto.unidadId,
        dto.remolqueId || null,
        dto.operadorId,
        orden.clienteId,
        orden.origenDireccion,
        orden.origenCiudad,
        orden.destinoDireccion,
        orden.destinoCiudad,
        this.calculateDistanceFromCoords(orden),
        this.calculateTimeFromDistance(this.calculateDistanceFromCoords(orden)),
        dto.fechaSalidaProgramada || orden.fechaRecoleccionProgramada,
        dto.fechaLlegadaProgramada || orden.fechaEntregaProgramada,
        userId,
      ]
    );

    const viajeId = viajeInsertResult[0]?.id;
    if (!viajeId) {
      throw new Error('Error al crear el viaje asociado');
    }

    // 8. Actualizar la orden con el viaje y cambiar estado
    orden.viajeId = viajeId;
    orden.estado = EstadoOrdenTransporte.ASIGNADA;
    orden.updatedById = userId;

    const ordenActualizada = await this.otRepository.save(orden);

    // 9. Registrar evento de tracking (si existe tabla)
    try {
      await this.otRepository.manager.query(
        `INSERT INTO tracking.eventos_tracking (
          tenant_id, viaje_id, orden_id, tipo_evento, descripcion,
          latitud, longitud, created_at, created_by_id
        ) VALUES ($1, $2, $3, 'ASIGNACION', $4, $5, $6, NOW(), $7)`,
        [
          tenantId,
          viajeId,
          orden.id,
          `Orden ${orden.codigo} asignada a viaje ${viajeCodigo}`,
          orden.origenLatitud,
          orden.origenLongitud,
          userId,
        ]
      );
    } catch {
      // Ignorar si la tabla de eventos no existe
    }

    return {
      orden: ordenActualizada,
      viajeId,
    };
  }

  /**
   * Validar si una unidad esta disponible para un rango de fechas
   */
  private async validateUnidadDisponible(
    tenantId: string,
    unidadId: string,
    fechaInicio?: Date,
    fechaFin?: Date
  ): Promise<{ disponible: boolean; razon?: string }> {
    // Verificar que la unidad existe y su estado
    const unidadResult = await this.otRepository.manager.query(
      `SELECT id, estado, activo FROM fleet.unidades WHERE id = $1 AND tenant_id = $2`,
      [unidadId, tenantId]
    );

    if (!unidadResult || unidadResult.length === 0) {
      return { disponible: false, razon: 'Unidad no encontrada' };
    }

    const unidad = unidadResult[0];
    if (!unidad.activo) {
      return { disponible: false, razon: 'Unidad inactiva' };
    }

    const estadosNoDisponibles = ['EN_VIAJE', 'EN_RUTA', 'EN_TALLER', 'BLOQUEADA', 'BAJA'];
    if (estadosNoDisponibles.includes(unidad.estado)) {
      return { disponible: false, razon: `Estado actual: ${unidad.estado}` };
    }

    // Verificar conflictos de viajes en el rango de fechas
    if (fechaInicio && fechaFin) {
      const conflictos = await this.otRepository.manager.query(
        `SELECT id, codigo, fecha_salida_programada, fecha_llegada_programada
         FROM transport.viajes
         WHERE tenant_id = $1
           AND unidad_id = $2
           AND estado NOT IN ('CANCELADO', 'CERRADO', 'FACTURADO', 'COBRADO')
           AND (
             (fecha_salida_programada <= $3 AND fecha_llegada_programada >= $3)
             OR (fecha_salida_programada <= $4 AND fecha_llegada_programada >= $4)
             OR (fecha_salida_programada >= $3 AND fecha_llegada_programada <= $4)
           )`,
        [tenantId, unidadId, fechaInicio, fechaFin]
      );

      if (conflictos && conflictos.length > 0) {
        return {
          disponible: false,
          razon: `Conflicto con viaje(s): ${conflictos.map((v: any) => v.codigo).join(', ')}`,
        };
      }
    }

    return { disponible: true };
  }

  /**
   * Validar documentos vigentes del operador
   */
  private async validateDocumentosOperador(
    tenantId: string,
    operadorId: string
  ): Promise<DocumentosOperadorStatus> {
    const operadorResult = await this.otRepository.manager.query(
      `SELECT id, nombre, apellido_paterno, estado, activo,
              licencia_vigencia, certificado_fisico_vigencia,
              antidoping_vigencia, capacitacion_mp_vigencia,
              capacitacion_materiales_peligrosos
       FROM fleet.operadores
       WHERE id = $1 AND tenant_id = $2`,
      [operadorId, tenantId]
    );

    if (!operadorResult || operadorResult.length === 0) {
      return {
        operadorId,
        documentosVigentes: false,
        licenciaVigente: false,
        certificadoFisicoVigente: false,
        antidopingVigente: false,
        documentosVencidos: ['Operador no encontrado'],
      };
    }

    const operador = operadorResult[0];
    const hoy = new Date();
    const documentosVencidos: string[] = [];

    // Verificar estado activo
    if (!operador.activo) {
      documentosVencidos.push('Operador inactivo');
    }

    const estadosNoDisponibles = ['SUSPENDIDO', 'BAJA', 'VACACIONES', 'INCAPACIDAD'];
    if (estadosNoDisponibles.includes(operador.estado)) {
      documentosVencidos.push(`Estado: ${operador.estado}`);
    }

    // Verificar licencia
    const licenciaVigente = operador.licencia_vigencia && new Date(operador.licencia_vigencia) >= hoy;
    if (!licenciaVigente) {
      documentosVencidos.push('Licencia de conducir');
    }

    // Verificar certificado fisico
    const certificadoFisicoVigente = operador.certificado_fisico_vigencia &&
      new Date(operador.certificado_fisico_vigencia) >= hoy;
    if (!certificadoFisicoVigente) {
      documentosVencidos.push('Certificado fisico');
    }

    // Verificar antidoping
    const antidopingVigente = operador.antidoping_vigencia &&
      new Date(operador.antidoping_vigencia) >= hoy;
    if (!antidopingVigente) {
      documentosVencidos.push('Antidoping');
    }

    // Verificar capacitacion materiales peligrosos (si aplica)
    let capacitacionMpVigente: boolean | undefined;
    if (operador.capacitacion_materiales_peligrosos) {
      capacitacionMpVigente = operador.capacitacion_mp_vigencia &&
        new Date(operador.capacitacion_mp_vigencia) >= hoy;
      if (!capacitacionMpVigente) {
        documentosVencidos.push('Capacitacion materiales peligrosos');
      }
    }

    return {
      operadorId,
      documentosVigentes: documentosVencidos.length === 0,
      licenciaVigente,
      certificadoFisicoVigente,
      antidopingVigente,
      capacitacionMpVigente,
      documentosVencidos,
    };
  }

  // ===========================================================================
  // CALCULO DE TARIFA (MAI-003 REQUERIMIENTO)
  // ===========================================================================

  /**
   * Calcular tarifa para una orden de transporte.
   * Busca la tarifa aplicable por lane (origen-destino) y cliente.
   * Aplica recargos (combustible, maniobras, etc.) y calcula total con IVA.
   *
   * @param id - ID de la orden de transporte
   * @param ctx - Contexto del servicio (tenantId, userId)
   * @returns Desglose completo de la tarifa calculada
   */
  async calcularTarifa(id: string, ctx: ServiceContext): Promise<TarifaCalculada | null> {
    const { tenantId } = ctx;

    // 1. Obtener la orden
    const orden = await this.findById(tenantId, id);
    if (!orden) {
      throw new Error('Orden de transporte no encontrada');
    }

    // 2. Calcular distancia si hay coordenadas
    let distanciaEstimadaKm = this.calculateDistanceFromCoords(orden);
    let tiempoEstimadoHoras = this.calculateTimeFromDistance(distanciaEstimadaKm);

    // 3. Buscar tarifa aplicable
    const tarifa = await this.findTarifaAplicable(tenantId, {
      clienteId: orden.clienteId,
      origenCiudad: orden.origenCiudad,
      origenEstado: orden.origenEstado,
      destinoCiudad: orden.destinoCiudad,
      destinoEstado: orden.destinoEstado,
      modalidadServicio: orden.modalidadServicio,
      tipoCarga: orden.tipoCarga,
    });

    // 4. Calcular monto base
    let tarifaBase = 0;
    let montoVariable = 0;
    let tarifaId: string | undefined;
    let tarifaCodigo: string | undefined;
    let laneId: string | undefined;
    let minimoAplicado = false;
    let moneda = 'MXN';

    if (tarifa) {
      tarifaId = tarifa.id;
      tarifaCodigo = tarifa.codigo;
      laneId = tarifa.laneId;
      moneda = tarifa.moneda || 'MXN';
      tarifaBase = Number(tarifa.tarifaBase) || 0;

      // Calcular monto variable segun tipo de tarifa
      switch (tarifa.tipoTarifa) {
        case 'POR_KM':
          montoVariable = (distanciaEstimadaKm || 0) * (Number(tarifa.tarifaKm) || 0);
          break;
        case 'POR_TONELADA':
          montoVariable = ((orden.pesoKg || 0) / 1000) * (Number(tarifa.tarifaTonelada) || 0);
          break;
        case 'POR_M3':
          montoVariable = (orden.volumenM3 || 0) * (Number(tarifa.tarifaM3) || 0);
          break;
        case 'POR_PALLET':
          montoVariable = (orden.pallets || 0) * (Number(tarifa.tarifaPallet) || 0);
          break;
        case 'MIXTA':
          montoVariable = (distanciaEstimadaKm || 0) * (Number(tarifa.tarifaKm) || 0);
          montoVariable += ((orden.pesoKg || 0) / 1000) * (Number(tarifa.tarifaTonelada) || 0);
          break;
        default:
          montoVariable = 0;
      }

      // Aplicar minimo si existe
      const montoBase = tarifaBase + montoVariable;
      if (tarifa.minimoFacturar && montoBase < Number(tarifa.minimoFacturar)) {
        tarifaBase = Number(tarifa.minimoFacturar);
        montoVariable = 0;
        minimoAplicado = true;
      }
    } else {
      // Sin tarifa especifica, usar estimacion por defecto
      // Tarifa base de $15,000 MXN + $25/km
      tarifaBase = 15000;
      montoVariable = (distanciaEstimadaKm || 0) * 25;
    }

    // 5. Calcular recargos
    const recargoCombustible = this.calcularRecargoCombustible(
      distanciaEstimadaKm,
      orden.requiereTemperatura
    );
    const recargoManiobras = this.calcularRecargoManiobras(orden);
    const recargoEspera = 0; // Se calcula al momento de la entrega
    const recargoOtros = Number(orden.recargos) || 0;
    const totalRecargos = recargoCombustible + recargoManiobras + recargoEspera + recargoOtros;

    // 6. Aplicar descuentos
    const descuentos = Number(orden.descuentos) || 0;

    // 7. Calcular totales
    const subtotal = tarifaBase + montoVariable + totalRecargos - descuentos;
    const ivaRate = 0.16; // IVA Mexico 16%
    const iva = subtotal * ivaRate;
    const total = subtotal + iva;

    return {
      ordenId: orden.id,
      tarifaId,
      tarifaCodigo,
      laneId,
      tarifaBase: Math.round(tarifaBase * 100) / 100,
      montoVariable: Math.round(montoVariable * 100) / 100,
      recargoCombustible: Math.round(recargoCombustible * 100) / 100,
      recargoManiobras: Math.round(recargoManiobras * 100) / 100,
      recargoEspera: Math.round(recargoEspera * 100) / 100,
      recargoOtros: Math.round(recargoOtros * 100) / 100,
      totalRecargos: Math.round(totalRecargos * 100) / 100,
      descuentos: Math.round(descuentos * 100) / 100,
      subtotal: Math.round(subtotal * 100) / 100,
      iva: Math.round(iva * 100) / 100,
      ivaRate,
      total: Math.round(total * 100) / 100,
      moneda,
      distanciaEstimadaKm: distanciaEstimadaKm ? Math.round(distanciaEstimadaKm) : undefined,
      tiempoEstimadoHoras: tiempoEstimadoHoras ? Math.round(tiempoEstimadoHoras * 10) / 10 : undefined,
      minimoAplicado,
    };
  }

  /**
   * Buscar la tarifa aplicable para una orden
   */
  private async findTarifaAplicable(
    tenantId: string,
    params: {
      clienteId?: string;
      origenCiudad?: string;
      origenEstado?: string;
      destinoCiudad?: string;
      destinoEstado?: string;
      modalidadServicio?: string;
      tipoCarga?: string;
    }
  ): Promise<any | null> {
    const hoy = new Date().toISOString().split('T')[0];

    // 1. Buscar lane que coincida con origen-destino
    let laneId: string | null = null;
    if (params.origenCiudad && params.destinoCiudad) {
      const laneResult = await this.otRepository.manager.query(
        `SELECT id FROM billing.lanes
         WHERE tenant_id = $1
           AND activa = true
           AND (
             (origen_ciudad ILIKE $2 AND destino_ciudad ILIKE $3)
             OR (origen_estado = $4 AND destino_estado = $5)
           )
         ORDER BY
           CASE WHEN origen_ciudad ILIKE $2 AND destino_ciudad ILIKE $3 THEN 0 ELSE 1 END
         LIMIT 1`,
        [tenantId, params.origenCiudad, params.destinoCiudad, params.origenEstado, params.destinoEstado]
      );
      if (laneResult && laneResult.length > 0) {
        laneId = laneResult[0].id;
      }
    }

    // 2. Buscar tarifa por prioridad: cliente+lane > cliente > lane > general
    const tarifaQuery = `
      SELECT t.*, l.id as lane_id, l.origen_ciudad, l.destino_ciudad
      FROM billing.tarifas t
      LEFT JOIN billing.lanes l ON t.lane_id = l.id
      WHERE t.tenant_id = $1
        AND t.activa = true
        AND t.fecha_inicio <= $2
        AND (t.fecha_fin IS NULL OR t.fecha_fin >= $2)
        AND (
          -- Prioridad 1: cliente + lane especificos
          (t.cliente_id = $3 AND t.lane_id = $4)
          -- Prioridad 2: cliente especifico, cualquier lane
          OR (t.cliente_id = $3 AND t.lane_id IS NULL)
          -- Prioridad 3: lane especifica, cualquier cliente
          OR (t.cliente_id IS NULL AND t.lane_id = $4)
          -- Prioridad 4: tarifa general
          OR (t.cliente_id IS NULL AND t.lane_id IS NULL)
        )
      ORDER BY
        CASE
          WHEN t.cliente_id = $3 AND t.lane_id = $4 THEN 1
          WHEN t.cliente_id = $3 AND t.lane_id IS NULL THEN 2
          WHEN t.cliente_id IS NULL AND t.lane_id = $4 THEN 3
          ELSE 4
        END,
        t.created_at DESC
      LIMIT 1`;

    const tarifaResult = await this.otRepository.manager.query(tarifaQuery, [
      tenantId,
      hoy,
      params.clienteId || null,
      laneId,
    ]);

    return tarifaResult && tarifaResult.length > 0 ? tarifaResult[0] : null;
  }

  /**
   * Calcular recargo por combustible
   */
  private calcularRecargoCombustible(distanciaKm?: number, requiereRefrigeracion?: boolean): number {
    if (!distanciaKm || distanciaKm <= 0) return 0;

    // Factor de ajuste por precio de diesel (fuel surcharge)
    // Precio base referencia: $22 MXN/litro
    // Consumo promedio: 2.5 km/litro
    const precioDieselActual = 24; // MXN/litro (puede venir de configuracion)
    const precioDieselBase = 22;
    const consumoPromedio = 2.5; // km/litro

    const litrosEstimados = distanciaKm / consumoPromedio;
    const costoBaseCombus = litrosEstimados * precioDieselBase;
    const costoActualCombus = litrosEstimados * precioDieselActual;
    let recargo = costoActualCombus - costoBaseCombus;

    // Recargo adicional por refrigeracion (30% mas consumo)
    if (requiereRefrigeracion) {
      recargo *= 1.3;
    }

    return Math.max(0, recargo);
  }

  /**
   * Calcular recargo por maniobras de carga/descarga
   */
  private calcularRecargoManiobras(orden: OrdenTransporte): number {
    let recargo = 0;

    // Maniobras por piezas o pallets
    if (orden.piezas && orden.piezas > 100) {
      recargo += (orden.piezas - 100) * 5; // $5 por pieza extra despues de 100
    }

    if (orden.pallets && orden.pallets > 10) {
      recargo += (orden.pallets - 10) * 150; // $150 por pallet extra despues de 10
    }

    // Carga peligrosa tiene recargo adicional
    if (orden.tipoCarga === TipoCarga.PELIGROSA) {
      recargo += 3000; // Recargo fijo por manejo especial
    }

    // Carga sobredimensionada
    if (orden.tipoCarga === TipoCarga.SOBREDIMENSIONADA) {
      recargo += 5000; // Recargo por permisos especiales
    }

    return recargo;
  }

  /**
   * Calcular distancia desde coordenadas de la orden
   */
  private calculateDistanceFromCoords(orden: OrdenTransporte): number | undefined {
    if (orden.origenLatitud && orden.origenLongitud &&
        orden.destinoLatitud && orden.destinoLongitud) {
      return this.calculateDistance(
        orden.origenLatitud,
        orden.origenLongitud,
        orden.destinoLatitud,
        orden.destinoLongitud
      );
    }
    return undefined;
  }

  /**
   * Calcular tiempo estimado desde distancia
   */
  private calculateTimeFromDistance(distanciaKm?: number): number | undefined {
    if (!distanciaKm) return undefined;
    // Velocidad promedio de 60 km/h considerando paradas
    return distanciaKm / 60;
  }

  /**
   * Cambiar estado con validacion de transiciones
   */
  private async changeStatus(
    tenantId: string,
    id: string,
    newStatus: EstadoOrdenTransporte,
    changedById: string
  ): Promise<OrdenTransporte | null> {
    const orden = await this.findById(tenantId, id);
    if (!orden) return null;

    if (!this.isValidTransition(orden.estado, newStatus)) {
      throw new Error(
        `Transicion de estado invalida de ${orden.estado} a ${newStatus}`
      );
    }

    orden.estado = newStatus;
    orden.updatedById = changedById;

    return this.otRepository.save(orden);
  }

  // ===========================================================================
  // HELPERS PRIVADOS
  // ===========================================================================

  private async generateCodigo(tenantId: string): Promise<string> {
    const year = new Date().getFullYear();
    const month = String(new Date().getMonth() + 1).padStart(2, '0');
    const count = await this.otRepository.count({ where: { tenantId } });
    return `OT-${year}${month}-${String(count + 1).padStart(6, '0')}`;
  }

  private isValidTransition(from: EstadoOrdenTransporte, to: EstadoOrdenTransporte): boolean {
    const transitions: Record<EstadoOrdenTransporte, EstadoOrdenTransporte[]> = {
      [EstadoOrdenTransporte.BORRADOR]: [
        EstadoOrdenTransporte.PENDIENTE,
        EstadoOrdenTransporte.SOLICITADA,
        EstadoOrdenTransporte.CONFIRMADA,
        EstadoOrdenTransporte.CANCELADA,
      ],
      [EstadoOrdenTransporte.PENDIENTE]: [
        EstadoOrdenTransporte.SOLICITADA,
        EstadoOrdenTransporte.CONFIRMADA,
        EstadoOrdenTransporte.CANCELADA,
      ],
      [EstadoOrdenTransporte.SOLICITADA]: [
        EstadoOrdenTransporte.CONFIRMADA,
        EstadoOrdenTransporte.CANCELADA,
      ],
      [EstadoOrdenTransporte.CONFIRMADA]: [
        EstadoOrdenTransporte.ASIGNADA,
        EstadoOrdenTransporte.CANCELADA,
      ],
      [EstadoOrdenTransporte.ASIGNADA]: [
        EstadoOrdenTransporte.EN_PROCESO,
        EstadoOrdenTransporte.EN_TRANSITO,
        EstadoOrdenTransporte.CONFIRMADA, // Desasignacion
        EstadoOrdenTransporte.CANCELADA,
      ],
      [EstadoOrdenTransporte.EN_PROCESO]: [
        EstadoOrdenTransporte.EN_TRANSITO,
        EstadoOrdenTransporte.COMPLETADA,
      ],
      [EstadoOrdenTransporte.EN_TRANSITO]: [
        EstadoOrdenTransporte.COMPLETADA,
        EstadoOrdenTransporte.ENTREGADA,
      ],
      [EstadoOrdenTransporte.COMPLETADA]: [
        EstadoOrdenTransporte.ENTREGADA,
      ],
      [EstadoOrdenTransporte.ENTREGADA]: [
        EstadoOrdenTransporte.FACTURADA,
      ],
      [EstadoOrdenTransporte.FACTURADA]: [],
      [EstadoOrdenTransporte.CANCELADA]: [],
    };

    return transitions[from]?.includes(to) ?? false;
  }

  private mapOrderField(orderBy: string): string {
    const mapping: Record<string, string> = {
      createdAt: 'createdAt',
      fechaRecoleccion: 'fechaRecoleccionProgramada',
      fechaEntrega: 'fechaEntregaProgramada',
      total: 'total',
    };
    return mapping[orderBy] || 'createdAt';
  }

  /**
   * Calcular distancia entre dos coordenadas usando formula de Haversine
   */
  private calculateDistance(
    lat1: number,
    lon1: number,
    lat2: number,
    lon2: number
  ): number {
    const R = 6371; // Radio de la Tierra en km
    const dLat = this.toRad(lat2 - lat1);
    const dLon = this.toRad(lon2 - lon1);
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) *
      Math.sin(dLon / 2) * Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    // Multiplicar por 1.3 para considerar que las rutas no son linea recta
    return R * c * 1.3;
  }

  private toRad(deg: number): number {
    return deg * (Math.PI / 180);
  }

  // ===========================================================================
  // ALIAS METHODS - Backward compatibility with existing controllers
  // ===========================================================================

  /** @deprecated Use findById instead */
  async findOne(tenantId: string, id: string) {
    return this.findById(tenantId, id);
  }

  /** @deprecated Use findByCodigo instead */
  async findByNumeroOt(tenantId: string, codigo: string) {
    return this.findByCodigo(tenantId, codigo);
  }

  /** @deprecated Use specific state transition methods instead */
  async cambiarEstado(tenantId: string, id: string, nuevoEstado: EstadoOrdenTransporte, updatedById: string) {
    const orden = await this.findById(tenantId, id);
    if (!orden) return null;
    orden.estado = nuevoEstado;
    orden.updatedById = updatedById;
    return this.otRepository.save(orden);
  }

  /** @deprecated Use confirm instead */
  async confirmar(tenantId: string, id: string, confirmedById: string) {
    return this.confirm(tenantId, id, confirmedById);
  }

  /** @deprecated Use assignToViaje instead */
  async asignar(tenantId: string, orderId: string, viajeId: string, assignedById: string) {
    return this.assignToViaje(tenantId, orderId, viajeId, assignedById);
  }

  /** @deprecated Use cancel instead */
  async cancelar(tenantId: string, id: string, motivo: string, cancelledById: string) {
    return this.cancel(tenantId, id, motivo, cancelledById);
  }

  /** @deprecated Use findUnassigned instead */
  async getOtsPendientes(tenantId: string, limit?: number) {
    return this.findUnassigned(tenantId, limit);
  }

  /** @deprecated Use findAll with clienteId filter instead */
  async getOtsCliente(clienteId: string, tenantId: string, limit?: number) {
    return this.findAll(tenantId, { clienteId, limit: limit || 100 });
  }

  /** @deprecated Use findUnassigned instead */
  async getOtsParaProgramacion(tenantId: string, _fecha?: Date) {
    return this.findUnassigned(tenantId, 100);
  }
}
