/**
 * Sync Service
 * ERP Transportistas
 *
 * Handles offline queue synchronization with priority and conflict resolution.
 * Sprint: S4 - TASK-007
 * Module: Offline Sync
 */

import { Repository, DataSource, LessThan, In } from 'typeorm';
import {
  OfflineQueue,
  TipoOperacionOffline,
  EstadoSincronizacion,
  PrioridadSync,
} from '../entities/offline-queue.entity';

// DTOs
export interface CreateOfflineOperationDto {
  dispositivoId?: string;
  usuarioId?: string;
  unidadId?: string;
  viajeId?: string;
  tipoOperacion: TipoOperacionOffline;
  prioridad?: PrioridadSync;
  payload: Record<string, any>;
  endpointDestino: string;
  metodoHttp?: string;
  creadoOfflineEn: Date;
  clienteId?: string;
}

export interface SyncResultDto {
  id: string;
  clienteId?: string;
  estado: EstadoSincronizacion;
  error?: string;
  sincronizadoEn?: Date;
}

export interface SyncBatchResult {
  procesados: number;
  exitosos: number;
  errores: number;
  conflictos: number;
  resultados: SyncResultDto[];
}

export interface FiltrosOfflineQueue {
  dispositivoId?: string;
  usuarioId?: string;
  unidadId?: string;
  viajeId?: string;
  tipoOperacion?: TipoOperacionOffline;
  estado?: EstadoSincronizacion;
  prioridad?: PrioridadSync;
}

/**
 * Sync Service - Priority Queue Manager
 */
export class SyncService {
  private queueRepository: Repository<OfflineQueue>;

  constructor(private dataSource: DataSource) {
    this.queueRepository = dataSource.getRepository(OfflineQueue);
  }

  /**
   * Enqueue an offline operation
   */
  async encolar(tenantId: string, dto: CreateOfflineOperationDto): Promise<OfflineQueue> {
    // Check for duplicate by clienteId
    if (dto.clienteId) {
      const existente = await this.queueRepository.findOne({
        where: { tenantId, clienteId: dto.clienteId },
      });
      if (existente) {
        return existente; // Idempotent - return existing
      }
    }

    const operacion = this.queueRepository.create({
      tenantId,
      dispositivoId: dto.dispositivoId,
      usuarioId: dto.usuarioId,
      unidadId: dto.unidadId,
      viajeId: dto.viajeId,
      tipoOperacion: dto.tipoOperacion,
      prioridad: dto.prioridad || this.determinarPrioridad(dto.tipoOperacion),
      payload: dto.payload,
      endpointDestino: dto.endpointDestino,
      metodoHttp: dto.metodoHttp || 'POST',
      creadoOfflineEn: dto.creadoOfflineEn,
      clienteId: dto.clienteId,
      estado: EstadoSincronizacion.PENDIENTE,
      tamanoBytes: JSON.stringify(dto.payload).length,
    });

    return this.queueRepository.save(operacion);
  }

  /**
   * Enqueue multiple operations (batch)
   */
  async encolarLote(
    tenantId: string,
    operaciones: CreateOfflineOperationDto[]
  ): Promise<OfflineQueue[]> {
    const resultados: OfflineQueue[] = [];

    for (const dto of operaciones) {
      const operacion = await this.encolar(tenantId, dto);
      resultados.push(operacion);
    }

    return resultados;
  }

  /**
   * Get pending operations ordered by priority
   */
  async obtenerPendientes(
    tenantId: string,
    limite: number = 50
  ): Promise<OfflineQueue[]> {
    return this.queueRepository.find({
      where: {
        tenantId,
        estado: In([EstadoSincronizacion.PENDIENTE, EstadoSincronizacion.ERROR]),
      },
      order: {
        prioridad: 'ASC',
        creadoOfflineEn: 'ASC',
      },
      take: limite,
    });
  }

  /**
   * Get operations for a specific device
   */
  async obtenerPorDispositivo(
    tenantId: string,
    dispositivoId: string,
    filtros: FiltrosOfflineQueue = {}
  ): Promise<OfflineQueue[]> {
    const where: any = { tenantId, dispositivoId };

    if (filtros.estado) where.estado = filtros.estado;
    if (filtros.tipoOperacion) where.tipoOperacion = filtros.tipoOperacion;
    if (filtros.viajeId) where.viajeId = filtros.viajeId;

    return this.queueRepository.find({
      where,
      order: { prioridad: 'ASC', creadoOfflineEn: 'ASC' },
    });
  }

  /**
   * Mark operation as in progress
   */
  async marcarEnProceso(id: string): Promise<OfflineQueue | null> {
    const operacion = await this.queueRepository.findOne({ where: { id } });
    if (!operacion) return null;

    operacion.estado = EstadoSincronizacion.EN_PROCESO;
    operacion.ultimoIntentoEn = new Date();
    operacion.intentosSync++;

    return this.queueRepository.save(operacion);
  }

  /**
   * Mark operation as completed
   */
  async marcarCompletado(id: string): Promise<OfflineQueue | null> {
    const operacion = await this.queueRepository.findOne({ where: { id } });
    if (!operacion) return null;

    operacion.estado = EstadoSincronizacion.COMPLETADO;
    operacion.sincronizadoEn = new Date();

    return this.queueRepository.save(operacion);
  }

  /**
   * Mark operation as error
   */
  async marcarError(id: string, error: string): Promise<OfflineQueue | null> {
    const operacion = await this.queueRepository.findOne({ where: { id } });
    if (!operacion) return null;

    operacion.estado = operacion.intentosSync >= operacion.maxIntentos
      ? EstadoSincronizacion.DESCARTADO
      : EstadoSincronizacion.ERROR;
    operacion.ultimoError = error;
    operacion.historialErrores.push({
      timestamp: new Date().toISOString(),
      error,
    });

    return this.queueRepository.save(operacion);
  }

  /**
   * Mark operation as conflict
   */
  async marcarConflicto(
    id: string,
    versionServidor: number,
    resolucion?: 'CLIENT_WINS' | 'SERVER_WINS' | 'MERGE' | 'MANUAL'
  ): Promise<OfflineQueue | null> {
    const operacion = await this.queueRepository.findOne({ where: { id } });
    if (!operacion) return null;

    operacion.estado = EstadoSincronizacion.CONFLICTO;
    operacion.versionServidor = versionServidor;
    operacion.resolucionConflicto = resolucion;

    return this.queueRepository.save(operacion);
  }

  /**
   * Process sync batch - simulates processing pending operations
   * In real implementation, this would call actual endpoints
   */
  async procesarLote(tenantId: string, limite: number = 20): Promise<SyncBatchResult> {
    const pendientes = await this.obtenerPendientes(tenantId, limite);
    const resultados: SyncResultDto[] = [];

    let exitosos = 0;
    let errores = 0;
    let conflictos = 0;

    for (const operacion of pendientes) {
      await this.marcarEnProceso(operacion.id);

      try {
        // Simulate processing based on operation type
        const resultado = await this.procesarOperacion(operacion);

        if (resultado.success) {
          await this.marcarCompletado(operacion.id);
          exitosos++;
          resultados.push({
            id: operacion.id,
            clienteId: operacion.clienteId,
            estado: EstadoSincronizacion.COMPLETADO,
            sincronizadoEn: new Date(),
          });
        } else if (resultado.conflict) {
          await this.marcarConflicto(operacion.id, resultado.serverVersion || 0, 'SERVER_WINS');
          conflictos++;
          resultados.push({
            id: operacion.id,
            clienteId: operacion.clienteId,
            estado: EstadoSincronizacion.CONFLICTO,
            error: 'Conflicto de versión',
          });
        } else {
          await this.marcarError(operacion.id, resultado.error || 'Error desconocido');
          errores++;
          resultados.push({
            id: operacion.id,
            clienteId: operacion.clienteId,
            estado: EstadoSincronizacion.ERROR,
            error: resultado.error,
          });
        }
      } catch (error) {
        await this.marcarError(operacion.id, (error as Error).message);
        errores++;
        resultados.push({
          id: operacion.id,
          clienteId: operacion.clienteId,
          estado: EstadoSincronizacion.ERROR,
          error: (error as Error).message,
        });
      }
    }

    return {
      procesados: pendientes.length,
      exitosos,
      errores,
      conflictos,
      resultados,
    };
  }

  /**
   * Get queue statistics
   */
  async getEstadisticas(tenantId: string): Promise<{
    total: number;
    pendientes: number;
    enProceso: number;
    completados: number;
    errores: number;
    conflictos: number;
    descartados: number;
    porPrioridad: Record<PrioridadSync, number>;
    porTipo: Record<TipoOperacionOffline, number>;
  }> {
    const operaciones = await this.queueRepository.find({ where: { tenantId } });

    const stats = {
      total: operaciones.length,
      pendientes: 0,
      enProceso: 0,
      completados: 0,
      errores: 0,
      conflictos: 0,
      descartados: 0,
      porPrioridad: {
        [PrioridadSync.CRITICA]: 0,
        [PrioridadSync.ALTA]: 0,
        [PrioridadSync.NORMAL]: 0,
        [PrioridadSync.BAJA]: 0,
      },
      porTipo: {} as Record<TipoOperacionOffline, number>,
    };

    for (const op of operaciones) {
      // Count by status
      switch (op.estado) {
        case EstadoSincronizacion.PENDIENTE:
          stats.pendientes++;
          break;
        case EstadoSincronizacion.EN_PROCESO:
          stats.enProceso++;
          break;
        case EstadoSincronizacion.COMPLETADO:
          stats.completados++;
          break;
        case EstadoSincronizacion.ERROR:
          stats.errores++;
          break;
        case EstadoSincronizacion.CONFLICTO:
          stats.conflictos++;
          break;
        case EstadoSincronizacion.DESCARTADO:
          stats.descartados++;
          break;
      }

      // Count by priority
      stats.porPrioridad[op.prioridad]++;

      // Count by type
      stats.porTipo[op.tipoOperacion] = (stats.porTipo[op.tipoOperacion] || 0) + 1;
    }

    return stats;
  }

  /**
   * Clean up old completed operations
   */
  async limpiarCompletados(tenantId: string, diasAntiguedad: number = 7): Promise<number> {
    const threshold = new Date();
    threshold.setDate(threshold.getDate() - diasAntiguedad);

    const result = await this.queueRepository.delete({
      tenantId,
      estado: EstadoSincronizacion.COMPLETADO,
      sincronizadoEn: LessThan(threshold),
    });

    return result.affected || 0;
  }

  /**
   * Retry failed operations
   */
  async reintentarFallidos(tenantId: string): Promise<number> {
    const result = await this.queueRepository.update(
      {
        tenantId,
        estado: EstadoSincronizacion.ERROR,
      },
      {
        estado: EstadoSincronizacion.PENDIENTE,
      }
    );

    return result.affected || 0;
  }

  // ==========================================
  // Private Helpers
  // ==========================================

  /**
   * Determine priority based on operation type
   */
  private determinarPrioridad(tipo: TipoOperacionOffline): PrioridadSync {
    switch (tipo) {
      case TipoOperacionOffline.GPS_POSICION:
      case TipoOperacionOffline.GPS_EVENTO:
        return PrioridadSync.CRITICA;

      case TipoOperacionOffline.VIAJE_ESTADO:
      case TipoOperacionOffline.POD_FIRMA:
      case TipoOperacionOffline.CHECKIN:
      case TipoOperacionOffline.CHECKOUT:
        return PrioridadSync.ALTA;

      case TipoOperacionOffline.CHECKLIST_ITEM:
      case TipoOperacionOffline.CHECKLIST_COMPLETADO:
      case TipoOperacionOffline.VIAJE_EVENTO:
        return PrioridadSync.NORMAL;

      case TipoOperacionOffline.POD_FOTO:
      case TipoOperacionOffline.POD_DOCUMENTO:
        return PrioridadSync.BAJA;

      default:
        return PrioridadSync.NORMAL;
    }
  }

  /**
   * Process a single operation (mock implementation)
   * In real implementation, this would call the actual service
   */
  private async procesarOperacion(operacion: OfflineQueue): Promise<{
    success: boolean;
    conflict?: boolean;
    serverVersion?: number;
    error?: string;
  }> {
    // Simulate processing delay
    await new Promise(resolve => setTimeout(resolve, 10));

    // Mock success rate (95%)
    const random = Math.random();
    if (random < 0.95) {
      return { success: true };
    } else if (random < 0.98) {
      return { success: false, conflict: true, serverVersion: 2 };
    } else {
      return { success: false, error: 'Simulated error' };
    }
  }
}
