/**
 * PosicionGps Service
 * ERP Transportistas
 *
 * Business logic for GPS position tracking and history.
 * Adapted from erp-mecanicas-diesel MMD-014 GPS Integration
 * Module: MAI-006 Tracking
 */

import { Repository, DataSource } from 'typeorm';
import { PosicionGps } from '../entities/posicion-gps.entity';
import { DispositivoGps } from '../entities/dispositivo-gps.entity';

// DTOs
export interface CreatePosicionDto {
  dispositivoId: string;
  unidadId: string;
  latitud: number;
  longitud: number;
  altitud?: number;
  velocidad?: number;
  rumbo?: number;
  precision?: number;
  hdop?: number;
  atributos?: Record<string, any>;
  tiempoDispositivo: Date;
  tiempoFix?: Date;
  esValido?: boolean;
}

export interface PosicionFilters {
  dispositivoId?: string;
  unidadId?: string;
  tiempoInicio?: Date;
  tiempoFin?: Date;
  velocidadMinima?: number;
  velocidadMaxima?: number;
  esValido?: boolean;
}

export interface PuntoPosicion {
  latitud: number;
  longitud: number;
  timestamp: Date;
  velocidad?: number;
}

export interface ResumenTrack {
  totalPuntos: number;
  distanciaTotalKm: number;
  velocidadPromedio: number;
  velocidadMaxima: number;
  duracionMinutos: number;
  tiempoInicio: Date;
  tiempoFin: Date;
}

export class PosicionGpsService {
  private posicionRepository: Repository<PosicionGps>;
  private dispositivoRepository: Repository<DispositivoGps>;

  constructor(dataSource: DataSource) {
    this.posicionRepository = dataSource.getRepository(PosicionGps);
    this.dispositivoRepository = dataSource.getRepository(DispositivoGps);
  }

  /**
   * Record a new GPS position
   */
  async create(tenantId: string, dto: CreatePosicionDto): Promise<PosicionGps> {
    // Validate device exists and belongs to tenant
    const dispositivo = await this.dispositivoRepository.findOne({
      where: { id: dto.dispositivoId, tenantId },
    });

    if (!dispositivo) {
      throw new Error(`Dispositivo ${dto.dispositivoId} no encontrado`);
    }

    const posicion = this.posicionRepository.create({
      tenantId,
      dispositivoId: dto.dispositivoId,
      unidadId: dto.unidadId,
      latitud: dto.latitud,
      longitud: dto.longitud,
      altitud: dto.altitud,
      velocidad: dto.velocidad,
      rumbo: dto.rumbo,
      precision: dto.precision,
      hdop: dto.hdop,
      atributos: dto.atributos || {},
      tiempoDispositivo: dto.tiempoDispositivo,
      tiempoFix: dto.tiempoFix,
      esValido: dto.esValido !== false,
    });

    const savedPosicion = await this.posicionRepository.save(posicion);

    // Update device last position (fire and forget)
    this.dispositivoRepository.update(
      { id: dto.dispositivoId },
      {
        ultimaPosicionLat: dto.latitud,
        ultimaPosicionLng: dto.longitud,
        ultimaPosicionAt: dto.tiempoDispositivo,
      }
    );

    return savedPosicion;
  }

  /**
   * Record multiple positions in batch
   */
  async createBatch(tenantId: string, posiciones: CreatePosicionDto[]): Promise<number> {
    if (posiciones.length === 0) return 0;

    const entities = posiciones.map(dto => this.posicionRepository.create({
      tenantId,
      dispositivoId: dto.dispositivoId,
      unidadId: dto.unidadId,
      latitud: dto.latitud,
      longitud: dto.longitud,
      altitud: dto.altitud,
      velocidad: dto.velocidad,
      rumbo: dto.rumbo,
      precision: dto.precision,
      hdop: dto.hdop,
      atributos: dto.atributos || {},
      tiempoDispositivo: dto.tiempoDispositivo,
      tiempoFix: dto.tiempoFix,
      esValido: dto.esValido !== false,
    }));

    const result = await this.posicionRepository.insert(entities);
    return result.identifiers.length;
  }

  /**
   * Get position by ID
   */
  async findById(tenantId: string, id: string): Promise<PosicionGps | null> {
    return this.posicionRepository.findOne({
      where: { id, tenantId },
    });
  }

  /**
   * Get position history with filters
   */
  async findAll(
    tenantId: string,
    filters: PosicionFilters = {},
    pagination = { page: 1, limit: 100 }
  ) {
    const queryBuilder = this.posicionRepository.createQueryBuilder('pos')
      .where('pos.tenant_id = :tenantId', { tenantId });

    if (filters.dispositivoId) {
      queryBuilder.andWhere('pos.dispositivo_id = :dispositivoId', { dispositivoId: filters.dispositivoId });
    }
    if (filters.unidadId) {
      queryBuilder.andWhere('pos.unidad_id = :unidadId', { unidadId: filters.unidadId });
    }
    if (filters.tiempoInicio) {
      queryBuilder.andWhere('pos.tiempo_dispositivo >= :tiempoInicio', { tiempoInicio: filters.tiempoInicio });
    }
    if (filters.tiempoFin) {
      queryBuilder.andWhere('pos.tiempo_dispositivo <= :tiempoFin', { tiempoFin: filters.tiempoFin });
    }
    if (filters.velocidadMinima !== undefined) {
      queryBuilder.andWhere('pos.velocidad >= :velocidadMinima', { velocidadMinima: filters.velocidadMinima });
    }
    if (filters.velocidadMaxima !== undefined) {
      queryBuilder.andWhere('pos.velocidad <= :velocidadMaxima', { velocidadMaxima: filters.velocidadMaxima });
    }
    if (filters.esValido !== undefined) {
      queryBuilder.andWhere('pos.es_valido = :esValido', { esValido: filters.esValido });
    }

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

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

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

  /**
   * Get last position for a device
   */
  async getUltimaPosicion(tenantId: string, dispositivoId: string): Promise<PosicionGps | null> {
    return this.posicionRepository.findOne({
      where: { tenantId, dispositivoId },
      order: { tiempoDispositivo: 'DESC' },
    });
  }

  /**
   * Get last positions for multiple devices
   */
  async getUltimasPosiciones(tenantId: string, dispositivoIds: string[]): Promise<PosicionGps[]> {
    if (dispositivoIds.length === 0) return [];

    const subQuery = this.posicionRepository
      .createQueryBuilder('sub')
      .select('sub.dispositivo_id', 'dispositivo_id')
      .addSelect('MAX(sub.tiempo_dispositivo)', 'max_time')
      .where('sub.tenant_id = :tenantId', { tenantId })
      .andWhere('sub.dispositivo_id IN (:...dispositivoIds)', { dispositivoIds })
      .groupBy('sub.dispositivo_id');

    return this.posicionRepository
      .createQueryBuilder('pos')
      .innerJoin(
        `(${subQuery.getQuery()})`,
        'latest',
        'pos.dispositivo_id = latest.dispositivo_id AND pos.tiempo_dispositivo = latest.max_time'
      )
      .setParameters(subQuery.getParameters())
      .where('pos.tenant_id = :tenantId', { tenantId })
      .getMany();
  }

  /**
   * Get track for a device in a time range
   */
  async getTrack(
    tenantId: string,
    dispositivoId: string,
    tiempoInicio: Date,
    tiempoFin: Date,
    simplificar: boolean = false
  ): Promise<PuntoPosicion[]> {
    const queryBuilder = this.posicionRepository
      .createQueryBuilder('pos')
      .select(['pos.latitud', 'pos.longitud', 'pos.tiempoDispositivo', 'pos.velocidad'])
      .where('pos.tenant_id = :tenantId', { tenantId })
      .andWhere('pos.dispositivo_id = :dispositivoId', { dispositivoId })
      .andWhere('pos.tiempo_dispositivo BETWEEN :tiempoInicio AND :tiempoFin', { tiempoInicio, tiempoFin })
      .andWhere('pos.es_valido = :esValido', { esValido: true })
      .orderBy('pos.tiempo_dispositivo', 'ASC');

    const posiciones = await queryBuilder.getMany();

    const puntos: PuntoPosicion[] = posiciones.map(p => ({
      latitud: Number(p.latitud),
      longitud: Number(p.longitud),
      timestamp: p.tiempoDispositivo,
      velocidad: p.velocidad ? Number(p.velocidad) : undefined,
    }));

    if (simplificar && puntos.length > 500) {
      return this.simplificarTrack(puntos, 500);
    }

    return puntos;
  }

  /**
   * Get track summary statistics
   */
  async getResumenTrack(
    tenantId: string,
    dispositivoId: string,
    tiempoInicio: Date,
    tiempoFin: Date
  ): Promise<ResumenTrack | null> {
    const result = await this.posicionRepository
      .createQueryBuilder('pos')
      .select('COUNT(*)', 'totalPuntos')
      .addSelect('AVG(pos.velocidad)', 'velocidadPromedio')
      .addSelect('MAX(pos.velocidad)', 'velocidadMaxima')
      .addSelect('MIN(pos.tiempo_dispositivo)', 'tiempoInicio')
      .addSelect('MAX(pos.tiempo_dispositivo)', 'tiempoFin')
      .where('pos.tenant_id = :tenantId', { tenantId })
      .andWhere('pos.dispositivo_id = :dispositivoId', { dispositivoId })
      .andWhere('pos.tiempo_dispositivo BETWEEN :tiempoInicio AND :tiempoFin', { tiempoInicio, tiempoFin })
      .andWhere('pos.es_valido = :esValido', { esValido: true })
      .getRawOne();

    if (!result || result.totalPuntos === '0') return null;

    // Calculate distance using positions
    const track = await this.getTrack(tenantId, dispositivoId, tiempoInicio, tiempoFin, false);
    const distanciaTotalKm = this.calcularDistanciaTrack(track);

    const actualTiempoInicio = new Date(result.tiempoInicio);
    const actualTiempoFin = new Date(result.tiempoFin);
    const duracionMinutos = (actualTiempoFin.getTime() - actualTiempoInicio.getTime()) / (1000 * 60);

    return {
      totalPuntos: parseInt(result.totalPuntos, 10),
      distanciaTotalKm,
      velocidadPromedio: parseFloat(result.velocidadPromedio) || 0,
      velocidadMaxima: parseFloat(result.velocidadMaxima) || 0,
      duracionMinutos,
      tiempoInicio: actualTiempoInicio,
      tiempoFin: actualTiempoFin,
    };
  }

  /**
   * Calculate distance between two coordinates using Haversine formula
   */
  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;
  }

  /**
   * Calculate total distance for a track
   */
  calcularDistanciaTrack(puntos: PuntoPosicion[]): number {
    if (puntos.length < 2) return 0;

    let distanciaTotal = 0;
    for (let i = 1; i < puntos.length; i++) {
      distanciaTotal += this.calcularDistancia(
        puntos[i - 1].latitud,
        puntos[i - 1].longitud,
        puntos[i].latitud,
        puntos[i].longitud
      );
    }
    return Math.round(distanciaTotal * 1000) / 1000; // 3 decimal places
  }

  /**
   * Simplify track using nth-point sampling
   */
  private simplificarTrack(puntos: PuntoPosicion[], maxPuntos: number): PuntoPosicion[] {
    if (puntos.length <= maxPuntos) return puntos;

    const step = Math.ceil(puntos.length / maxPuntos);
    const simplified: PuntoPosicion[] = [puntos[0]];

    for (let i = step; i < puntos.length - 1; i += step) {
      simplified.push(puntos[i]);
    }

    // Always include last point
    simplified.push(puntos[puntos.length - 1]);
    return simplified;
  }

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

  /**
   * Delete old positions (for data retention)
   */
  async eliminarPosicionesAntiguas(tenantId: string, antesDe: Date): Promise<number> {
    const result = await this.posicionRepository
      .createQueryBuilder()
      .delete()
      .where('tenant_id = :tenantId', { tenantId })
      .andWhere('tiempo_dispositivo < :antesDe', { antesDe })
      .execute();

    return result.affected || 0;
  }

  /**
   * Get position count for a device
   */
  async getContadorPosiciones(tenantId: string, dispositivoId: string): Promise<number> {
    return this.posicionRepository.count({
      where: { tenantId, dispositivoId },
    });
  }
}
