/**
 * SegmentoRuta Service
 * ERP Transportistas
 *
 * Business logic for route segment calculation and billing.
 * Adapted from erp-mecanicas-diesel MMD-014 GPS Integration
 * Module: MAI-006 Tracking
 */

import { Repository, DataSource } from 'typeorm';
import { SegmentoRuta, TipoSegmento } from '../entities/segmento-ruta.entity';
import { PosicionGps } from '../entities/posicion-gps.entity';

// DTOs
export interface CreateSegmentoRutaDto {
  viajeId?: string;
  unidadId: string;
  dispositivoId?: string;
  posicionInicioId?: string;
  posicionFinId?: string;
  latInicio: number;
  lngInicio: number;
  latFin: number;
  lngFin: number;
  distanciaKm: number;
  distanciaCrudaKm?: number;
  tiempoInicio: Date;
  tiempoFin: Date;
  duracionMinutos?: number;
  tipoSegmento?: TipoSegmento;
  esValido?: boolean;
  notasValidacion?: string;
  polylineEncoded?: string;
  metadata?: Record<string, any>;
}

export interface SegmentoRutaFilters {
  viajeId?: string;
  unidadId?: string;
  dispositivoId?: string;
  tipoSegmento?: TipoSegmento;
  esValido?: boolean;
  fechaInicio?: Date;
  fechaFin?: Date;
}

export interface RutaCalculada {
  segmentos: SegmentoRuta[];
  distanciaTotalKm: number;
  duracionTotalMinutos: number;
  resumen: {
    haciaDestinoKm: number;
    enDestinoKm: number;
    retornoKm: number;
  };
}

export class SegmentoRutaService {
  private segmentoRepository: Repository<SegmentoRuta>;
  private posicionRepository: Repository<PosicionGps>;

  constructor(dataSource: DataSource) {
    this.segmentoRepository = dataSource.getRepository(SegmentoRuta);
    this.posicionRepository = dataSource.getRepository(PosicionGps);
  }

  /**
   * Create a route segment
   */
  async create(tenantId: string, dto: CreateSegmentoRutaDto): Promise<SegmentoRuta> {
    const duracionMinutos = dto.duracionMinutos ??
      (dto.tiempoFin.getTime() - dto.tiempoInicio.getTime()) / (1000 * 60);

    const segmento = this.segmentoRepository.create({
      tenantId,
      viajeId: dto.viajeId,
      unidadId: dto.unidadId,
      dispositivoId: dto.dispositivoId,
      posicionInicioId: dto.posicionInicioId,
      posicionFinId: dto.posicionFinId,
      latInicio: dto.latInicio,
      lngInicio: dto.lngInicio,
      latFin: dto.latFin,
      lngFin: dto.lngFin,
      distanciaKm: dto.distanciaKm,
      distanciaCrudaKm: dto.distanciaCrudaKm,
      tiempoInicio: dto.tiempoInicio,
      tiempoFin: dto.tiempoFin,
      duracionMinutos,
      tipoSegmento: dto.tipoSegmento || TipoSegmento.OTRO,
      esValido: dto.esValido !== false,
      notasValidacion: dto.notasValidacion,
      polylineEncoded: dto.polylineEncoded,
      metadata: dto.metadata || {},
    });

    return this.segmentoRepository.save(segmento);
  }

  /**
   * Find segment by ID
   */
  async findById(tenantId: string, id: string): Promise<SegmentoRuta | null> {
    return this.segmentoRepository.findOne({
      where: { id, tenantId },
    });
  }

  /**
   * List segments with filters
   */
  async findAll(
    tenantId: string,
    filters: SegmentoRutaFilters = {},
    pagination = { page: 1, limit: 20 }
  ) {
    const queryBuilder = this.segmentoRepository.createQueryBuilder('segmento')
      .where('segmento.tenant_id = :tenantId', { tenantId });

    if (filters.viajeId) {
      queryBuilder.andWhere('segmento.viaje_id = :viajeId', { viajeId: filters.viajeId });
    }
    if (filters.unidadId) {
      queryBuilder.andWhere('segmento.unidad_id = :unidadId', { unidadId: filters.unidadId });
    }
    if (filters.dispositivoId) {
      queryBuilder.andWhere('segmento.dispositivo_id = :dispositivoId', { dispositivoId: filters.dispositivoId });
    }
    if (filters.tipoSegmento) {
      queryBuilder.andWhere('segmento.tipo_segmento = :tipoSegmento', { tipoSegmento: filters.tipoSegmento });
    }
    if (filters.esValido !== undefined) {
      queryBuilder.andWhere('segmento.es_valido = :esValido', { esValido: filters.esValido });
    }
    if (filters.fechaInicio) {
      queryBuilder.andWhere('segmento.tiempo_inicio >= :fechaInicio', { fechaInicio: filters.fechaInicio });
    }
    if (filters.fechaFin) {
      queryBuilder.andWhere('segmento.tiempo_fin <= :fechaFin', { fechaFin: filters.fechaFin });
    }

    const skip = (pagination.page - 1) * pagination.limit;

    const [data, total] = await queryBuilder
      .orderBy('segmento.tiempo_inicio', 'DESC')
      .skip(skip)
      .take(pagination.limit)
      .getManyAndCount();

    return {
      data,
      total,
      page: pagination.page,
      limit: pagination.limit,
      totalPages: Math.ceil(total / pagination.limit),
    };
  }

  /**
   * Get segments for a viaje
   */
  async findByViaje(tenantId: string, viajeId: string): Promise<SegmentoRuta[]> {
    return this.segmentoRepository.find({
      where: { tenantId, viajeId },
      order: { tiempoInicio: 'ASC' },
    });
  }

  /**
   * Calculate route from positions
   */
  async calcularRutaDesdePosiciones(
    tenantId: string,
    dispositivoId: string,
    tiempoInicio: Date,
    tiempoFin: Date,
    viajeId?: string,
    tipoSegmento?: TipoSegmento
  ): Promise<SegmentoRuta | null> {
    // Get positions in time range
    const posiciones = await this.posicionRepository.find({
      where: {
        tenantId,
        dispositivoId,
        esValido: true,
      },
      order: { tiempoDispositivo: 'ASC' },
    });

    // Filter by time range
    const posicionesFiltradas = posiciones.filter(
      p => p.tiempoDispositivo >= tiempoInicio && p.tiempoDispositivo <= tiempoFin
    );

    if (posicionesFiltradas.length < 2) {
      return null;
    }

    const primeraPosicion = posicionesFiltradas[0];
    const ultimaPosicion = posicionesFiltradas[posicionesFiltradas.length - 1];

    // Calculate total distance
    let distanciaTotal = 0;
    for (let i = 1; i < posicionesFiltradas.length; i++) {
      distanciaTotal += this.calcularDistancia(
        Number(posicionesFiltradas[i - 1].latitud),
        Number(posicionesFiltradas[i - 1].longitud),
        Number(posicionesFiltradas[i].latitud),
        Number(posicionesFiltradas[i].longitud)
      );
    }

    // Create encoded polyline
    const polylineEncoded = this.encodePolyline(
      posicionesFiltradas.map(p => ({
        lat: Number(p.latitud),
        lng: Number(p.longitud),
      }))
    );

    return this.create(tenantId, {
      viajeId,
      unidadId: primeraPosicion.unidadId,
      dispositivoId,
      posicionInicioId: primeraPosicion.id,
      posicionFinId: ultimaPosicion.id,
      latInicio: Number(primeraPosicion.latitud),
      lngInicio: Number(primeraPosicion.longitud),
      latFin: Number(ultimaPosicion.latitud),
      lngFin: Number(ultimaPosicion.longitud),
      distanciaKm: Math.round(distanciaTotal * 1000) / 1000,
      distanciaCrudaKm: distanciaTotal,
      tiempoInicio: primeraPosicion.tiempoDispositivo,
      tiempoFin: ultimaPosicion.tiempoDispositivo,
      tipoSegmento: tipoSegmento || TipoSegmento.OTRO,
      polylineEncoded,
      metadata: {
        cantidadPosiciones: posicionesFiltradas.length,
      },
    });
  }

  /**
   * Get calculated route summary for a viaje
   */
  async getResumenRutaViaje(tenantId: string, viajeId: string): Promise<RutaCalculada | null> {
    const segmentos = await this.findByViaje(tenantId, viajeId);

    if (segmentos.length === 0) {
      return null;
    }

    let distanciaTotalKm = 0;
    let duracionTotalMinutos = 0;
    let haciaDestinoKm = 0;
    let enDestinoKm = 0;
    let retornoKm = 0;

    for (const segmento of segmentos) {
      if (segmento.esValido) {
        distanciaTotalKm += Number(segmento.distanciaKm);
        duracionTotalMinutos += Number(segmento.duracionMinutos || 0);

        switch (segmento.tipoSegmento) {
          case TipoSegmento.HACIA_DESTINO:
            haciaDestinoKm += Number(segmento.distanciaKm);
            break;
          case TipoSegmento.EN_DESTINO:
            enDestinoKm += Number(segmento.distanciaKm);
            break;
          case TipoSegmento.RETORNO:
            retornoKm += Number(segmento.distanciaKm);
            break;
        }
      }
    }

    return {
      segmentos,
      distanciaTotalKm: Math.round(distanciaTotalKm * 1000) / 1000,
      duracionTotalMinutos: Math.round(duracionTotalMinutos * 100) / 100,
      resumen: {
        haciaDestinoKm: Math.round(haciaDestinoKm * 1000) / 1000,
        enDestinoKm: Math.round(enDestinoKm * 1000) / 1000,
        retornoKm: Math.round(retornoKm * 1000) / 1000,
      },
    };
  }

  /**
   * Update segment validity
   */
  async updateValidez(
    tenantId: string,
    id: string,
    esValido: boolean,
    notas?: string
  ): Promise<SegmentoRuta | null> {
    const segmento = await this.findById(tenantId, id);
    if (!segmento) return null;

    segmento.esValido = esValido;
    if (notas) {
      segmento.notasValidacion = notas;
    }

    return this.segmentoRepository.save(segmento);
  }

  /**
   * Delete segment
   */
  async delete(tenantId: string, id: string): Promise<boolean> {
    const result = await this.segmentoRepository.delete({ id, tenantId });
    return (result.affected || 0) > 0;
  }

  /**
   * Get distance statistics for a unit
   */
  async getEstadisticasUnidad(
    tenantId: string,
    unidadId: string,
    fechaInicio?: Date,
    fechaFin?: Date
  ): Promise<{
    distanciaTotalKm: number;
    totalSegmentos: number;
    distanciaPromedioPorSegmento: number;
    porTipoSegmento: Record<TipoSegmento, number>;
  }> {
    const queryBuilder = this.segmentoRepository.createQueryBuilder('segmento')
      .where('segmento.tenant_id = :tenantId', { tenantId })
      .andWhere('segmento.unidad_id = :unidadId', { unidadId })
      .andWhere('segmento.es_valido = :esValido', { esValido: true });

    if (fechaInicio) {
      queryBuilder.andWhere('segmento.tiempo_inicio >= :fechaInicio', { fechaInicio });
    }
    if (fechaFin) {
      queryBuilder.andWhere('segmento.tiempo_fin <= :fechaFin', { fechaFin });
    }

    const segmentos = await queryBuilder.getMany();

    const porTipoSegmento: Record<TipoSegmento, number> = {
      [TipoSegmento.HACIA_DESTINO]: 0,
      [TipoSegmento.EN_DESTINO]: 0,
      [TipoSegmento.RETORNO]: 0,
      [TipoSegmento.ENTRE_PARADAS]: 0,
      [TipoSegmento.OTRO]: 0,
    };

    let distanciaTotalKm = 0;
    for (const segmento of segmentos) {
      const distancia = Number(segmento.distanciaKm);
      distanciaTotalKm += distancia;
      porTipoSegmento[segmento.tipoSegmento] += distancia;
    }

    return {
      distanciaTotalKm: Math.round(distanciaTotalKm * 1000) / 1000,
      totalSegmentos: segmentos.length,
      distanciaPromedioPorSegmento: segmentos.length > 0
        ? Math.round((distanciaTotalKm / segmentos.length) * 1000) / 1000
        : 0,
      porTipoSegmento,
    };
  }

  /**
   * Calculate distance using Haversine formula
   */
  private calcularDistancia(lat1: number, lng1: number, lat2: number, lng2: number): number {
    const R = 6371; // Earth radius in km
    const dLat = this.toRad(lat2 - lat1);
    const dLng = this.toRad(lng2 - lng1);
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) *
      Math.sin(dLng / 2) * Math.sin(dLng / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c;
  }

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

  /**
   * Encode polyline using Google's algorithm
   */
  private encodePolyline(points: { lat: number; lng: number }[]): string {
    if (points.length === 0) return '';

    let encoded = '';
    let prevLat = 0;
    let prevLng = 0;

    for (const point of points) {
      const lat = Math.round(point.lat * 1e5);
      const lng = Math.round(point.lng * 1e5);

      encoded += this.encodeNumber(lat - prevLat);
      encoded += this.encodeNumber(lng - prevLng);

      prevLat = lat;
      prevLng = lng;
    }

    return encoded;
  }

  private encodeNumber(num: number): string {
    let sgnNum = num << 1;
    if (num < 0) {
      sgnNum = ~sgnNum;
    }

    let encoded = '';
    while (sgnNum >= 0x20) {
      encoded += String.fromCharCode((0x20 | (sgnNum & 0x1f)) + 63);
      sgnNum >>= 5;
    }
    encoded += String.fromCharCode(sgnNum + 63);

    return encoded;
  }
}
