/**
 * Sync Engine - Handles offline event queue and auto-synchronization
 */

import { readAuthSession } from './auth-storage';
import { cacheCatalogList } from './catalogos-offline';
import {
  db,
  getLastSyncTimestamp,
  setLastSyncTimestamp,
  type DbSyncEvent,
} from './db';
import { mapRemoteClienteToDb } from './offline-clientes';
import { cacheRemoteOtList } from './offline-ots';
import { cacheRemoteVehiculos } from './offline-vehiculos';
import { getApiUrl } from './runtime-api';

export interface SyncResult {
  success: boolean;
  processed: number;
  failed: number;
  errors: Array<{ eventId: string; error: string }>;
  mappings: Record<string, any>;
}

export interface SyncStatusSnapshot {
  isOnline: boolean;
  isSyncing: boolean;
  pendingCount: number;
  lastSyncAt: number | null;
  lastError: string | null;
}

export class SyncEngine {
  private isOnline = true;
  private isSyncing = false;
  private pendingCount = 0;
  private lastSyncAt: number | null = null;
  private lastError: string | null = null;
  private syncInterval: NodeJS.Timeout | null = null;
  private listeners = new Set<(snapshot: SyncStatusSnapshot) => void>();

  constructor() {
    this.setupOnlineListeners();
    void this.refreshStatus();
  }

  private setupOnlineListeners() {
    if (typeof window !== 'undefined') {
      this.isOnline = navigator.onLine;
      this.emitStatusChange();

      window.addEventListener('online', () => {
        this.isOnline = true;
        this.emitStatusChange();
        void this.syncNow();
      });

      window.addEventListener('offline', () => {
        this.isOnline = false;
        this.emitStatusChange();
      });

      this.syncInterval = setInterval(() => {
        if (this.isOnline && !this.isSyncing) {
          void this.syncNow();
        }
      }, 30000);
    }
  }

  subscribe(listener: (snapshot: SyncStatusSnapshot) => void): () => void {
    this.listeners.add(listener);
    listener(this.getStatus());
    return () => {
      this.listeners.delete(listener);
    };
  }

  private emitStatusChange() {
    const snapshot = this.getStatus();
    for (const listener of this.listeners) {
      try {
        listener(snapshot);
      } catch (error) {
        console.error('[sync] Listener error:', error);
      }
    }
  }

  private async refreshPendingCount(): Promise<number> {
    this.pendingCount = await db.syncEvents.where('status').anyOf(['pending', 'failed']).count();
    this.emitStatusChange();
    return this.pendingCount;
  }

  async refreshStatus(): Promise<SyncStatusSnapshot> {
    await this.refreshPendingCount();
    return this.getStatus();
  }

  async queueEvent(event: Omit<DbSyncEvent, 'id' | 'status' | 'retries' | 'error' | 'timestamp'>): Promise<void> {
    await db.syncEvents.add({
      ...event,
      timestamp: Date.now(),
      status: 'pending',
      retries: 0,
    });

    await this.refreshPendingCount();

    if (this.isOnline) {
      void this.syncNow();
    }
  }

  async syncNow(): Promise<SyncResult> {
    if (this.isSyncing) {
      return { success: true, processed: 0, failed: 0, errors: [], mappings: {} };
    }

    if (!this.isOnline) {
      this.lastError = 'Sin conexion';
      this.emitStatusChange();
      return {
        success: false,
        processed: 0,
        failed: 0,
        errors: [{ eventId: 'offline', error: 'No connection' }],
        mappings: {},
      };
    }

    this.isSyncing = true;
    this.lastError = null;
    this.emitStatusChange();

    try {
      const pendingEvents = await db.syncEvents
        .where('status')
        .equals('pending')
        .or('status')
        .equals('failed')
        .toArray();

      this.pendingCount = pendingEvents.length;
      this.emitStatusChange();

      const token = readAuthSession().accessToken;
      if (!token) {
        this.lastError = 'Sesion no disponible';
        return {
          success: false,
          processed: 0,
          failed: pendingEvents.length,
          errors: [{ eventId: 'auth', error: 'No token' }],
          mappings: {},
        };
      }

      if (pendingEvents.length === 0) {
        await this.pullChanges(token);
        this.lastSyncAt = Date.now();
        this.lastError = null;
        return { success: true, processed: 0, failed: 0, errors: [], mappings: {} };
      }

      const eventsForPush = await Promise.all(
        pendingEvents.map(async (event) => {
          const payload = { ...(event.payload || {}) };

          if (payload.otLocalId && !payload.otId) {
            const ot = await db.ordenesTrabajo.where('localId').equals(payload.otLocalId).first();
            if (ot?.serverId) {
              payload.otId = ot.serverId;
            }
          }

          return {
            eventId: event.eventId,
            eventType: event.eventType,
            entityType: event.entityType,
            localId: event.localId,
            payload,
            timestamp: event.timestamp,
          };
        }),
      );

      const response = await fetch(`${getApiUrl()}/sync/push`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({ events: eventsForPush }),
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${await response.text()}`);
      }

      const result: SyncResult = await response.json();
      await this.applyMappings(result.mappings);
      await this.applyPushResults(pendingEvents, result);
      await this.pullChanges(token);

      this.lastSyncAt = Date.now();
      this.lastError = result.errors.length > 0 ? `${result.errors.length} evento(s) con error` : null;
      return result;
    } catch (error: any) {
      const pendingEvents = await db.syncEvents
        .where('status')
        .anyOf(['pending', 'failed'])
        .toArray();

      for (const event of pendingEvents) {
        await db.syncEvents.update(event.id!, {
          status: 'failed',
          retries: event.retries + 1,
          error: error.message,
        });
      }

      this.lastError = error.message || 'Error de sincronizacion';
      return {
        success: false,
        processed: 0,
        failed: pendingEvents.length,
        errors: [{ eventId: 'sync', error: error.message }],
        mappings: {},
      };
    } finally {
      this.isSyncing = false;
      await this.refreshPendingCount();
      this.emitStatusChange();
    }
  }

  private async applyPushResults(pendingEvents: DbSyncEvent[], result: SyncResult) {
    for (const event of pendingEvents) {
      const wasProcessed = result.errors.findIndex((entry) => entry.eventId === event.eventId) === -1;

      if (wasProcessed) {
        await db.syncEvents.delete(event.id!);
        continue;
      }

      await db.syncEvents.update(event.id!, {
        status: 'failed',
        retries: event.retries + 1,
        error: result.errors.find((entry) => entry.eventId === event.eventId)?.error || 'Unknown error',
      });
    }
  }

  private async pullChanges(token: string): Promise<void> {
    const since = await getLastSyncTimestamp();
    const response = await fetch(`${getApiUrl()}/sync/pull?since=${since}`, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });

    if (!response.ok) {
      throw new Error(`Pull sync failed: HTTP ${response.status}`);
    }

    const payload = await response.json();
    const changes = payload?.changes || {};

    if (Array.isArray(changes.ots) && changes.ots.length > 0) {
      await cacheRemoteOtList(changes.ots);
    }

    if (Array.isArray(changes.vehiculos) && changes.vehiculos.length > 0) {
      await cacheRemoteVehiculos(changes.vehiculos);
    }

    if (Array.isArray(changes.clientes) && changes.clientes.length > 0) {
      await db.clientes.bulkPut(changes.clientes.map(mapRemoteClienteToDb));
    }

    if (changes.catalogos?.tiposVehiculo) {
      await cacheCatalogList('tipo_vehiculo', changes.catalogos.tiposVehiculo);
    }

    if (changes.catalogos?.tiposDano) {
      await cacheCatalogList('tipo_dano', changes.catalogos.tiposDano);
    }

    if (changes.catalogos?.cosasTrae) {
      await cacheCatalogList('cosa_trae', changes.catalogos.cosasTrae);
    }

    await setLastSyncTimestamp(Number(payload?.timestamp || Date.now()));
  }

  private async applyMappings(mappings: Record<string, any>): Promise<void> {
    const entries = Object.entries(mappings || {});
    if (entries.length === 0) {
      return;
    }

    for (const [localId, mapping] of entries) {
      const serverId = Number(mapping?.serverId);
      if (Number.isFinite(serverId) && serverId > 0) {
        await db.ordenesTrabajo
          .where('localId')
          .equals(localId)
          .modify((ot: any) => {
            ot.serverId = serverId;
            if (mapping?.numeroOt) {
              ot.numeroOt = mapping.numeroOt;
            }
            ot.syncStatus = 'synced';
            ot.lastModified = Date.now();
          });

        await db.vehiculos
          .where('localId')
          .equals(localId)
          .modify((vehiculo: any) => {
            vehiculo.serverId = serverId;
            if (mapping?.placa) {
              vehiculo.placa = String(mapping.placa).toUpperCase();
            }
            vehiculo.syncStatus = 'synced';
            vehiculo.lastModified = Date.now();
          });

        if (String(localId).startsWith('catalog:')) {
          const parts = String(localId).split(':');
          const catalogType = parts[1] as any;
          const localCatalogId = Number(parts[2]);

          if (catalogType && Number.isFinite(localCatalogId)) {
            const cached = await db.catalogos.get([catalogType, localCatalogId]);
            if (cached) {
              await db.catalogos.delete([catalogType, localCatalogId]);
              await db.catalogos.put({
                ...cached,
                id: serverId,
                cachedAt: Date.now(),
              });
            }
          }
        }
      }

      if (mapping?.tarjeta) {
        const realTarjeta = String(mapping.tarjeta);
        const pendingClient = await db.clientes.get(localId);

        if (pendingClient) {
          await db.clientes.put({
            ...pendingClient,
            tarjeta: realTarjeta,
            syncStatus: 'synced',
            lastModified: Date.now(),
          });

          if (localId !== realTarjeta) {
            await db.clientes.delete(localId);
          }

          await db.ordenesTrabajo
            .where('clienteTarjeta')
            .equals(localId)
            .modify((ot: any) => {
              ot.clienteTarjeta = realTarjeta;
              ot.lastModified = Date.now();
            });
        } else {
          await db.clientes
            .where('tarjeta')
            .equals(realTarjeta)
            .modify((cliente: any) => {
              cliente.syncStatus = 'synced';
              cliente.lastModified = Date.now();
            });
        }
      }
    }
  }

  async getPendingCount(): Promise<number> {
    return this.refreshPendingCount();
  }

  getStatus(): SyncStatusSnapshot {
    return {
      isOnline: this.isOnline,
      isSyncing: this.isSyncing,
      pendingCount: this.pendingCount,
      lastSyncAt: this.lastSyncAt,
      lastError: this.lastError,
    };
  }

  destroy() {
    if (this.syncInterval) {
      clearInterval(this.syncInterval);
    }
  }
}

export const syncEngine = new SyncEngine();
