import { Repository, FindOptionsWhere, Between, In, MoreThanOrEqual } from 'typeorm';
import { EventoTracking, TipoEventoTracking, Geocerca, TipoGeocerca } from '../entities';

export interface EventoSearchParams {
  tenantId: string;
  unidadId?: string;
  viajeId?: string;
  tipoEvento?: TipoEventoTracking;
  fechaDesde?: Date;
  fechaHasta?: Date;
  limit?: number;
  offset?: number;
}

export interface CreateEventoDto {
  unidadId: string;
  viajeId?: string;
  operadorId?: string;
  tipoEvento: TipoEventoTracking;
  latitud: number;
  longitud: number;
  velocidad?: number;
  rumbo?: number;
  altitud?: number;
  precision?: number;
  odometro?: number;
  nivelCombustible?: number;
  motorEncendido?: boolean;
  descripcion?: string;
  datosAdicionales?: Record<string, any>;
}

export interface GeocercaSearchParams {
  tenantId: string;
  tipo?: TipoGeocerca;
  activa?: boolean;
  clienteId?: string;
  limit?: number;
  offset?: number;
}

export interface CreateGeocercaDto {
  codigo: string;
  nombre: string;
  tipo: TipoGeocerca;
  geometria: any; // GeoJSON
  radio?: number;
  clienteId?: string;
  descripcion?: string;
  alertaEntrada?: boolean;
  alertaSalida?: boolean;
  alertaPermanencia?: boolean;
  tiempoMaximoPermanencia?: number;
  horarioActivo?: any;
}

export class TrackingService {
  constructor(
    private readonly eventoRepository: Repository<EventoTracking>,
    private readonly geocercaRepository: Repository<Geocerca>
  ) {}

  // ==================== Eventos ====================

  async registrarEvento(tenantId: string, dto: CreateEventoDto): Promise<EventoTracking> {
    const evento = this.eventoRepository.create({
      ...dto,
      tenantId,
      timestamp: new Date(),
    });

    const savedEvento = await this.eventoRepository.save(evento);

    // Check geocercas if it's a position event
    if (dto.tipoEvento === TipoEventoTracking.POSICION) {
      await this.verificarGeocercas(savedEvento);
    }

    return savedEvento;
  }

  async registrarPosicion(
    tenantId: string,
    unidadId: string,
    datos: {
      latitud: number;
      longitud: number;
      velocidad?: number;
      rumbo?: number;
      odometro?: number;
      viajeId?: string;
      operadorId?: string;
    }
  ): Promise<EventoTracking> {
    return this.registrarEvento(tenantId, {
      ...datos,
      unidadId,
      tipoEvento: TipoEventoTracking.POSICION,
    });
  }

  async findEventos(params: EventoSearchParams): Promise<{ data: EventoTracking[]; total: number }> {
    const {
      tenantId,
      unidadId,
      viajeId,
      tipoEvento,
      fechaDesde,
      fechaHasta,
      limit = 100,
      offset = 0,
    } = params;

    const where: FindOptionsWhere<EventoTracking> = { tenantId };

    if (unidadId) {
      where.unidadId = unidadId;
    }

    if (viajeId) {
      where.viajeId = viajeId;
    }

    if (tipoEvento) {
      where.tipoEvento = tipoEvento;
    }

    if (fechaDesde && fechaHasta) {
      where.timestamp = Between(fechaDesde, fechaHasta);
    }

    const [data, total] = await this.eventoRepository.findAndCount({
      where,
      take: limit,
      skip: offset,
      order: { timestamp: 'DESC' },
    });

    return { data, total };
  }

  async getUltimaPosicion(unidadId: string, tenantId: string): Promise<EventoTracking | null> {
    return this.eventoRepository.findOne({
      where: {
        unidadId,
        tenantId,
        tipoEvento: TipoEventoTracking.POSICION,
      },
      order: { timestamp: 'DESC' },
    });
  }

  async getHistorialPosiciones(
    unidadId: string,
    tenantId: string,
    fechaDesde: Date,
    fechaHasta: Date
  ): Promise<EventoTracking[]> {
    return this.eventoRepository.find({
      where: {
        unidadId,
        tenantId,
        tipoEvento: TipoEventoTracking.POSICION,
        timestamp: Between(fechaDesde, fechaHasta),
      },
      order: { timestamp: 'ASC' },
    });
  }

  async getRutaViaje(viajeId: string, tenantId: string): Promise<EventoTracking[]> {
    return this.eventoRepository.find({
      where: {
        viajeId,
        tenantId,
        tipoEvento: In([TipoEventoTracking.POSICION, TipoEventoTracking.PARADA]),
      },
      order: { timestamp: 'ASC' },
    });
  }

  async getEventosViaje(viajeId: string, tenantId: string): Promise<EventoTracking[]> {
    return this.eventoRepository.find({
      where: { viajeId, tenantId },
      order: { timestamp: 'ASC' },
    });
  }

  // Posiciones actuales de todas las unidades
  async getPosicionesActuales(tenantId: string, unidadIds?: string[]): Promise<EventoTracking[]> {
    const subquery = this.eventoRepository
      .createQueryBuilder('e')
      .select('DISTINCT ON (e.unidad_id) e.*')
      .where('e.tenant_id = :tenantId', { tenantId })
      .andWhere('e.tipo_evento = :tipo', { tipo: TipoEventoTracking.POSICION });

    if (unidadIds && unidadIds.length > 0) {
      subquery.andWhere('e.unidad_id IN (:...unidadIds)', { unidadIds });
    }

    subquery.orderBy('e.unidad_id').addOrderBy('e.timestamp', 'DESC');

    return subquery.getRawMany();
  }

  // ==================== Geocercas ====================

  async findAllGeocercas(params: GeocercaSearchParams): Promise<{ data: Geocerca[]; total: number }> {
    const {
      tenantId,
      tipo,
      activa,
      clienteId,
      limit = 50,
      offset = 0,
    } = params;

    const where: FindOptionsWhere<Geocerca> = { tenantId };

    if (tipo) {
      where.tipo = tipo;
    }

    if (activa !== undefined) {
      where.activa = activa;
    }

    if (clienteId) {
      where.clienteId = clienteId;
    }

    const [data, total] = await this.geocercaRepository.findAndCount({
      where,
      take: limit,
      skip: offset,
      order: { nombre: 'ASC' },
    });

    return { data, total };
  }

  async findGeocerca(id: string, tenantId: string): Promise<Geocerca | null> {
    return this.geocercaRepository.findOne({
      where: { id, tenantId },
    });
  }

  async createGeocerca(tenantId: string, dto: CreateGeocercaDto, createdBy: string): Promise<Geocerca> {
    const geocerca = this.geocercaRepository.create({
      ...dto,
      tenantId,
      activa: true,
      createdById: createdBy,
    });

    return this.geocercaRepository.save(geocerca);
  }

  async updateGeocerca(
    id: string,
    tenantId: string,
    dto: Partial<CreateGeocercaDto>,
    updatedBy: string
  ): Promise<Geocerca | null> {
    const geocerca = await this.findGeocerca(id, tenantId);
    if (!geocerca) return null;

    Object.assign(geocerca, {
      ...dto,
      updatedById: updatedBy,
    });

    return this.geocercaRepository.save(geocerca);
  }

  async deleteGeocerca(id: string, tenantId: string): Promise<boolean> {
    const geocerca = await this.findGeocerca(id, tenantId);
    if (!geocerca) return false;

    const result = await this.geocercaRepository.softDelete(id);
    return (result.affected ?? 0) > 0;
  }

  // Verificar geocercas para un evento
  private async verificarGeocercas(evento: EventoTracking): Promise<void> {
    // Get active geocercas for this tenant
    const geocercas = await this.geocercaRepository.find({
      where: { tenantId: evento.tenantId, activa: true },
    });

    for (const geocerca of geocercas) {
      const dentroGeocerca = this.puntoEnGeocerca(
        evento.latitud,
        evento.longitud,
        geocerca
      );

      if (dentroGeocerca) {
        // Registrar evento de entrada si es necesario
        if (geocerca.alertaEntrada) {
          await this.registrarEvento(evento.tenantId, {
            unidadId: evento.unidadId,
            viajeId: evento.viajeId,
            operadorId: evento.operadorId,
            tipoEvento: TipoEventoTracking.GEOCERCA_ENTRADA,
            latitud: evento.latitud,
            longitud: evento.longitud,
            descripcion: `Entrada a geocerca: ${geocerca.nombre}`,
            datosAdicionales: { geocercaId: geocerca.id },
          });
        }
      }
    }
  }

  private puntoEnGeocerca(lat: number, lon: number, geocerca: Geocerca): boolean {
    // Simplified point-in-polygon check
    // In production, use PostGIS ST_Contains or similar
    if (geocerca.tipo === TipoGeocerca.CIRCULAR && geocerca.radio) {
      const centro = geocerca.geometria?.coordinates;
      if (centro) {
        const distancia = this.calcularDistancia(lat, lon, centro[1], centro[0]);
        return distancia <= geocerca.radio;
      }
    }
    // For polygons, would need proper geometric check
    return false;
  }

  private calcularDistancia(lat1: number, lon1: number, lat2: number, lon2: number): number {
    // Haversine formula
    const R = 6371000; // Earth radius in meters
    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));
    return R * c;
  }

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