import { Repository, FindOptionsWhere, ILike, In, MoreThan, LessThanOrEqual, Between } from 'typeorm';
import { Unidad, TipoUnidad, EstadoUnidad } from '../entities/unidad.entity';
import { Asignacion } from '../entities/asignacion.entity';
import { Operador, EstadoOperador } from '../entities/operador.entity';

// ============================================================================
// INTERFACES
// ============================================================================

export interface UnidadFilters {
  search?: string;
  tipo?: TipoUnidad;
  estado?: EstadoUnidad;
  estados?: EstadoUnidad[];
  sucursalId?: string;
  esPropia?: boolean;
  tieneGps?: boolean;
  activo?: boolean;
  limit?: number;
  offset?: number;
}

export interface CreateUnidadDto {
  numeroEconomico: string;
  tipo: TipoUnidad;
  marca?: string;
  modelo?: string;
  anio?: number;
  color?: string;
  numeroSerie?: string;
  numeroMotor?: string;
  placa?: string;
  placaEstado?: string;
  permisoSct?: string;
  tipoPermisoSct?: string;
  configuracionVehicular?: string;
  capacidadPesoKg?: number;
  capacidadVolumenM3?: number;
  capacidadPallets?: number;
  tipoCombustible?: string;
  rendimientoKmLitro?: number;
  capacidadTanqueLitros?: number;
  odometroActual?: number;
  tieneGps?: boolean;
  gpsProveedor?: string;
  gpsImei?: string;
  esPropia?: boolean;
  propietarioId?: string;
  costoAdquisicion?: number;
  fechaAdquisicion?: Date;
  valorActual?: number;
  fechaVerificacionProxima?: Date;
  fechaPolizaVencimiento?: Date;
  fechaPermisoVencimiento?: Date;
  sucursalId?: string;
}

export interface UpdateUnidadDto extends Partial<CreateUnidadDto> {
  estado?: EstadoUnidad;
}

export interface AsignarOperadorDto {
  operadorId: string;
  remolqueId?: string;
  motivo?: string;
}

export interface UnidadUbicacion {
  lat: number;
  lng: number;
  timestamp: Date;
}

export interface FleetStatistics {
  total: number;
  disponibles: number;
  enViaje: number;
  enTaller: number;
  bloqueadas: number;
  porTipo: Record<TipoUnidad, number>;
  propias: number;
  terceros: number;
  conGps: number;
  sinGps: number;
  documentosVencidos: number;
  mantenimientoPendiente: number;
}

export interface MaintenanceStatus {
  unidadId: string;
  numeroEconomico: string;
  odometroActual: number;
  odometroUltimoServicio: number;
  kmDesdeUltimoServicio: number;
  fechaVerificacionProxima?: Date;
  diasParaVerificacion?: number;
  fechaPolizaVencimiento?: Date;
  diasParaVencimientoPoliza?: number;
  fechaPermisoVencimiento?: Date;
  diasParaVencimientoPermiso?: number;
  requiereAtencion: boolean;
  alertas: string[];
}

// ============================================================================
// SERVICE
// ============================================================================

export class UnidadService {
  constructor(
    private readonly unidadRepository: Repository<Unidad>,
    private readonly asignacionRepository: Repository<Asignacion>,
    private readonly operadorRepository: Repository<Operador>
  ) {}

  // --------------------------------------------------------------------------
  // CRUD OPERATIONS
  // --------------------------------------------------------------------------

  /**
   * Crear nueva unidad (tractora, remolque, etc.)
   */
  async create(tenantId: string, dto: CreateUnidadDto, createdById: string): Promise<Unidad> {
    // Validar que no exista numero economico duplicado
    const existingNumero = await this.unidadRepository.findOne({
      where: { tenantId, numeroEconomico: dto.numeroEconomico },
    });
    if (existingNumero) {
      throw new Error(`Ya existe una unidad con el numero economico ${dto.numeroEconomico}`);
    }

    // Validar que no exista placa duplicada
    if (dto.placa) {
      const existingPlaca = await this.unidadRepository.findOne({
        where: { tenantId, placa: dto.placa },
      });
      if (existingPlaca) {
        throw new Error(`Ya existe una unidad con la placa ${dto.placa}`);
      }
    }

    const unidad = this.unidadRepository.create({
      ...dto,
      tenantId,
      estado: EstadoUnidad.DISPONIBLE,
      activo: true,
      createdById,
    });

    return this.unidadRepository.save(unidad);
  }

  /**
   * Buscar unidad por ID
   */
  async findById(tenantId: string, id: string): Promise<Unidad | null> {
    return this.unidadRepository.findOne({
      where: { tenantId, id, activo: true },
    });
  }

  /**
   * Buscar unidad por ID o lanzar error
   */
  async findByIdOrFail(tenantId: string, id: string): Promise<Unidad> {
    const unidad = await this.findById(tenantId, id);
    if (!unidad) {
      throw new Error(`Unidad con ID ${id} no encontrada`);
    }
    return unidad;
  }

  /**
   * Listar unidades con filtros
   */
  async findAll(
    tenantId: string,
    filters: UnidadFilters = {}
  ): Promise<{ data: Unidad[]; total: number }> {
    const {
      search,
      tipo,
      estado,
      estados,
      sucursalId,
      esPropia,
      tieneGps,
      activo = true,
      limit = 50,
      offset = 0,
    } = filters;

    const qb = this.unidadRepository
      .createQueryBuilder('u')
      .where('u.tenant_id = :tenantId', { tenantId });

    if (activo !== undefined) {
      qb.andWhere('u.activo = :activo', { activo });
    }

    if (tipo) {
      qb.andWhere('u.tipo = :tipo', { tipo });
    }

    if (estado) {
      qb.andWhere('u.estado = :estado', { estado });
    }

    if (estados && estados.length > 0) {
      qb.andWhere('u.estado IN (:...estados)', { estados });
    }

    if (sucursalId) {
      qb.andWhere('u.sucursal_id = :sucursalId', { sucursalId });
    }

    if (esPropia !== undefined) {
      qb.andWhere('u.es_propia = :esPropia', { esPropia });
    }

    if (tieneGps !== undefined) {
      qb.andWhere('u.tiene_gps = :tieneGps', { tieneGps });
    }

    if (search) {
      qb.andWhere(
        '(u.numero_economico ILIKE :search OR u.placa ILIKE :search OR u.marca ILIKE :search OR u.modelo ILIKE :search)',
        { search: `%${search}%` }
      );
    }

    const total = await qb.getCount();

    qb.orderBy('u.numero_economico', 'ASC')
      .offset(offset)
      .limit(limit);

    const data = await qb.getMany();

    return { data, total };
  }

  /**
   * Obtener unidades disponibles
   */
  async findAvailable(tenantId: string, tipo?: TipoUnidad): Promise<Unidad[]> {
    const where: FindOptionsWhere<Unidad> = {
      tenantId,
      estado: EstadoUnidad.DISPONIBLE,
      activo: true,
    };

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

    return this.unidadRepository.find({
      where,
      order: { numeroEconomico: 'ASC' },
    });
  }

  /**
   * Filtrar unidades por tipo
   */
  async findByType(tenantId: string, tipo: TipoUnidad): Promise<Unidad[]> {
    return this.unidadRepository.find({
      where: { tenantId, tipo, activo: true },
      order: { numeroEconomico: 'ASC' },
    });
  }

  /**
   * Actualizar unidad
   */
  async update(
    tenantId: string,
    id: string,
    dto: UpdateUnidadDto,
    updatedById: string
  ): Promise<Unidad> {
    const unidad = await this.findByIdOrFail(tenantId, id);

    // Validar numero economico duplicado si cambia
    if (dto.numeroEconomico && dto.numeroEconomico !== unidad.numeroEconomico) {
      const existing = await this.unidadRepository.findOne({
        where: { tenantId, numeroEconomico: dto.numeroEconomico },
      });
      if (existing) {
        throw new Error(`Ya existe una unidad con el numero economico ${dto.numeroEconomico}`);
      }
    }

    // Validar placa duplicada si cambia
    if (dto.placa && dto.placa !== unidad.placa) {
      const existing = await this.unidadRepository.findOne({
        where: { tenantId, placa: dto.placa },
      });
      if (existing && existing.id !== id) {
        throw new Error(`Ya existe una unidad con la placa ${dto.placa}`);
      }
    }

    Object.assign(unidad, {
      ...dto,
      updatedById,
    });

    return this.unidadRepository.save(unidad);
  }

  /**
   * Actualizar estado de la unidad
   */
  async updateStatus(
    tenantId: string,
    id: string,
    estado: EstadoUnidad,
    updatedById: string
  ): Promise<Unidad> {
    const unidad = await this.findByIdOrFail(tenantId, id);

    // Validaciones de transicion de estado
    if (unidad.estado === EstadoUnidad.BAJA) {
      throw new Error('No se puede cambiar el estado de una unidad dada de baja');
    }

    if (estado === EstadoUnidad.EN_VIAJE && unidad.estado === EstadoUnidad.EN_TALLER) {
      throw new Error('No se puede asignar a viaje una unidad en taller');
    }

    unidad.estado = estado;
    unidad.updatedById = updatedById;

    return this.unidadRepository.save(unidad);
  }

  // --------------------------------------------------------------------------
  // ASSIGNMENT OPERATIONS
  // --------------------------------------------------------------------------

  /**
   * Asignar operador a unidad
   */
  async assignOperador(
    tenantId: string,
    unidadId: string,
    dto: AsignarOperadorDto,
    createdById: string
  ): Promise<Asignacion> {
    const unidad = await this.findByIdOrFail(tenantId, unidadId);

    if (unidad.estado !== EstadoUnidad.DISPONIBLE) {
      throw new Error('Solo se puede asignar operador a unidades disponibles');
    }

    // Verificar que el operador existe y esta disponible
    const operador = await this.operadorRepository.findOne({
      where: { tenantId, id: dto.operadorId, activo: true },
    });
    if (!operador) {
      throw new Error(`Operador con ID ${dto.operadorId} no encontrado`);
    }

    if (operador.estado !== EstadoOperador.DISPONIBLE && operador.estado !== EstadoOperador.ACTIVO) {
      throw new Error('El operador no esta disponible para asignacion');
    }

    // Verificar que no tenga asignacion activa
    const asignacionExistente = await this.asignacionRepository.findOne({
      where: { tenantId, unidadId, activa: true },
    });
    if (asignacionExistente) {
      throw new Error('La unidad ya tiene un operador asignado. Desasigne primero.');
    }

    // Crear asignacion
    const asignacion = this.asignacionRepository.create({
      tenantId,
      unidadId,
      operadorId: dto.operadorId,
      remolqueId: dto.remolqueId,
      fechaInicio: new Date(),
      activa: true,
      motivo: dto.motivo,
      createdById,
    });

    await this.asignacionRepository.save(asignacion);

    // Actualizar unidad asignada en operador
    operador.unidadAsignadaId = unidadId;
    await this.operadorRepository.save(operador);

    return asignacion;
  }

  /**
   * Desasignar operador de unidad
   */
  async unassignOperador(
    tenantId: string,
    unidadId: string,
    updatedById: string
  ): Promise<void> {
    const unidad = await this.findByIdOrFail(tenantId, unidadId);

    if (unidad.estado === EstadoUnidad.EN_VIAJE || unidad.estado === EstadoUnidad.EN_RUTA) {
      throw new Error('No se puede desasignar operador de una unidad en viaje');
    }

    const asignacion = await this.asignacionRepository.findOne({
      where: { tenantId, unidadId, activa: true },
    });

    if (!asignacion) {
      throw new Error('La unidad no tiene operador asignado');
    }

    // Finalizar asignacion
    asignacion.activa = false;
    asignacion.fechaFin = new Date();
    await this.asignacionRepository.save(asignacion);

    // Remover unidad asignada del operador
    const operador = await this.operadorRepository.findOne({
      where: { tenantId, id: asignacion.operadorId },
    });
    if (operador) {
      operador.unidadAsignadaId = undefined as unknown as string;
      await this.operadorRepository.save(operador);
    }
  }

  /**
   * Obtener asignacion activa de una unidad
   */
  async getActiveAssignment(tenantId: string, unidadId: string): Promise<Asignacion | null> {
    return this.asignacionRepository.findOne({
      where: { tenantId, unidadId, activa: true },
      relations: ['operador'],
    });
  }

  // --------------------------------------------------------------------------
  // MAINTENANCE & LOCATION
  // --------------------------------------------------------------------------

  /**
   * Obtener estado de mantenimiento de una unidad
   */
  async getMaintenanceStatus(tenantId: string, id: string): Promise<MaintenanceStatus> {
    const unidad = await this.findByIdOrFail(tenantId, id);
    const hoy = new Date();
    const alertas: string[] = [];

    const kmDesdeUltimoServicio = unidad.odometroActual - (unidad.odometroUltimoServicio || 0);

    // Calcular dias para vencimientos
    let diasParaVerificacion: number | undefined;
    let diasParaVencimientoPoliza: number | undefined;
    let diasParaVencimientoPermiso: number | undefined;

    if (unidad.fechaVerificacionProxima) {
      diasParaVerificacion = Math.ceil(
        (unidad.fechaVerificacionProxima.getTime() - hoy.getTime()) / (1000 * 60 * 60 * 24)
      );
      if (diasParaVerificacion <= 0) {
        alertas.push('Verificacion vencida');
      } else if (diasParaVerificacion <= 30) {
        alertas.push(`Verificacion vence en ${diasParaVerificacion} dias`);
      }
    }

    if (unidad.fechaPolizaVencimiento) {
      diasParaVencimientoPoliza = Math.ceil(
        (unidad.fechaPolizaVencimiento.getTime() - hoy.getTime()) / (1000 * 60 * 60 * 24)
      );
      if (diasParaVencimientoPoliza <= 0) {
        alertas.push('Poliza de seguro vencida');
      } else if (diasParaVencimientoPoliza <= 30) {
        alertas.push(`Poliza de seguro vence en ${diasParaVencimientoPoliza} dias`);
      }
    }

    if (unidad.fechaPermisoVencimiento) {
      diasParaVencimientoPermiso = Math.ceil(
        (unidad.fechaPermisoVencimiento.getTime() - hoy.getTime()) / (1000 * 60 * 60 * 24)
      );
      if (diasParaVencimientoPermiso <= 0) {
        alertas.push('Permiso SCT vencido');
      } else if (diasParaVencimientoPermiso <= 30) {
        alertas.push(`Permiso SCT vence en ${diasParaVencimientoPermiso} dias`);
      }
    }

    // Alerta por kilometraje
    const KM_SERVICIO_RECOMENDADO = 15000;
    if (kmDesdeUltimoServicio >= KM_SERVICIO_RECOMENDADO) {
      alertas.push(`Mantenimiento requerido (${kmDesdeUltimoServicio.toLocaleString()} km desde ultimo servicio)`);
    }

    return {
      unidadId: unidad.id,
      numeroEconomico: unidad.numeroEconomico,
      odometroActual: unidad.odometroActual,
      odometroUltimoServicio: unidad.odometroUltimoServicio || 0,
      kmDesdeUltimoServicio,
      fechaVerificacionProxima: unidad.fechaVerificacionProxima,
      diasParaVerificacion,
      fechaPolizaVencimiento: unidad.fechaPolizaVencimiento,
      diasParaVencimientoPoliza,
      fechaPermisoVencimiento: unidad.fechaPermisoVencimiento,
      diasParaVencimientoPermiso,
      requiereAtencion: alertas.length > 0,
      alertas,
    };
  }

  /**
   * Obtener ubicacion actual de la unidad (GPS)
   */
  async trackLocation(tenantId: string, id: string): Promise<UnidadUbicacion | null> {
    const unidad = await this.findByIdOrFail(tenantId, id);

    if (!unidad.tieneGps) {
      throw new Error('La unidad no tiene GPS habilitado');
    }

    if (!unidad.ubicacionActualLat || !unidad.ubicacionActualLng) {
      return null;
    }

    return {
      lat: Number(unidad.ubicacionActualLat),
      lng: Number(unidad.ubicacionActualLng),
      timestamp: unidad.ultimaActualizacionUbicacion || new Date(),
    };
  }

  /**
   * Actualizar ubicacion de la unidad
   */
  async updateLocation(
    tenantId: string,
    id: string,
    ubicacion: UnidadUbicacion
  ): Promise<Unidad> {
    const unidad = await this.findByIdOrFail(tenantId, id);

    unidad.ubicacionActualLat = ubicacion.lat;
    unidad.ubicacionActualLng = ubicacion.lng;
    unidad.ultimaActualizacionUbicacion = ubicacion.timestamp || new Date();

    return this.unidadRepository.save(unidad);
  }

  /**
   * Actualizar odometro de la unidad
   */
  async updateOdometro(
    tenantId: string,
    id: string,
    odometro: number,
    updatedById: string
  ): Promise<Unidad> {
    const unidad = await this.findByIdOrFail(tenantId, id);

    if (odometro < unidad.odometroActual) {
      throw new Error('El odometro no puede ser menor al valor actual');
    }

    unidad.odometroActual = odometro;
    unidad.updatedById = updatedById;

    return this.unidadRepository.save(unidad);
  }

  // --------------------------------------------------------------------------
  // STATISTICS
  // --------------------------------------------------------------------------

  /**
   * Obtener estadisticas de la flota
   */
  async getStatistics(tenantId: string): Promise<FleetStatistics> {
    const hoy = new Date();

    // Conteo total
    const total = await this.unidadRepository.count({
      where: { tenantId, activo: true },
    });

    // Conteo por estado
    const disponibles = await this.unidadRepository.count({
      where: { tenantId, estado: EstadoUnidad.DISPONIBLE, activo: true },
    });

    const enViaje = await this.unidadRepository.count({
      where: { tenantId, estado: In([EstadoUnidad.EN_VIAJE, EstadoUnidad.EN_RUTA]), activo: true },
    });

    const enTaller = await this.unidadRepository.count({
      where: { tenantId, estado: EstadoUnidad.EN_TALLER, activo: true },
    });

    const bloqueadas = await this.unidadRepository.count({
      where: { tenantId, estado: EstadoUnidad.BLOQUEADA, activo: true },
    });

    // Conteo por tipo
    const porTipo: Record<TipoUnidad, number> = {} as Record<TipoUnidad, number>;
    for (const tipo of Object.values(TipoUnidad)) {
      porTipo[tipo] = await this.unidadRepository.count({
        where: { tenantId, tipo, activo: true },
      });
    }

    // Propias vs terceros
    const propias = await this.unidadRepository.count({
      where: { tenantId, esPropia: true, activo: true },
    });

    const terceros = total - propias;

    // GPS
    const conGps = await this.unidadRepository.count({
      where: { tenantId, tieneGps: true, activo: true },
    });

    const sinGps = total - conGps;

    // Documentos vencidos
    const documentosVencidos = await this.unidadRepository
      .createQueryBuilder('u')
      .where('u.tenant_id = :tenantId', { tenantId })
      .andWhere('u.activo = true')
      .andWhere(
        '(u.fecha_verificacion_proxima < :hoy OR u.fecha_poliza_vencimiento < :hoy OR u.fecha_permiso_vencimiento < :hoy)',
        { hoy }
      )
      .getCount();

    // Mantenimiento pendiente (mas de 15000 km desde ultimo servicio)
    const KM_SERVICIO = 15000;
    const mantenimientoPendiente = await this.unidadRepository
      .createQueryBuilder('u')
      .where('u.tenant_id = :tenantId', { tenantId })
      .andWhere('u.activo = true')
      .andWhere('(u.odometro_actual - COALESCE(u.odometro_ultimo_servicio, 0)) >= :km', { km: KM_SERVICIO })
      .getCount();

    return {
      total,
      disponibles,
      enViaje,
      enTaller,
      bloqueadas,
      porTipo,
      propias,
      terceros,
      conGps,
      sinGps,
      documentosVencidos,
      mantenimientoPendiente,
    };
  }

  /**
   * Obtener unidades con documentos por vencer
   */
  async getUnidadesDocumentosPorVencer(
    tenantId: string,
    dias: number = 30
  ): Promise<Unidad[]> {
    const hoy = new Date();
    const fechaLimite = new Date();
    fechaLimite.setDate(fechaLimite.getDate() + dias);

    return this.unidadRepository
      .createQueryBuilder('u')
      .where('u.tenant_id = :tenantId', { tenantId })
      .andWhere('u.activo = true')
      .andWhere(
        '((u.fecha_verificacion_proxima BETWEEN :hoy AND :fechaLimite) OR ' +
        '(u.fecha_poliza_vencimiento BETWEEN :hoy AND :fechaLimite) OR ' +
        '(u.fecha_permiso_vencimiento BETWEEN :hoy AND :fechaLimite))',
        { hoy, fechaLimite }
      )
      .orderBy('u.numero_economico', 'ASC')
      .getMany();
  }

  /**
   * Dar de baja unidad
   */
  async darDeBaja(
    tenantId: string,
    id: string,
    motivoBaja: string,
    updatedById: string
  ): Promise<Unidad> {
    const unidad = await this.findByIdOrFail(tenantId, id);

    if (unidad.estado === EstadoUnidad.EN_VIAJE || unidad.estado === EstadoUnidad.EN_RUTA) {
      throw new Error('No se puede dar de baja una unidad en viaje');
    }

    // Desasignar operador si tiene
    const asignacion = await this.asignacionRepository.findOne({
      where: { tenantId, unidadId: id, activa: true },
    });
    if (asignacion) {
      await this.unassignOperador(tenantId, id, updatedById);
    }

    unidad.estado = EstadoUnidad.BAJA;
    unidad.activo = false;
    unidad.fechaBaja = new Date();
    unidad.motivoBaja = motivoBaja;
    unidad.updatedById = updatedById;

    return this.unidadRepository.save(unidad);
  }
}
