import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { PrismaService } from '../prisma/prisma.service';
import { RelojesMarcadoresService } from './services/relojes-marcadores.service';

const TZ = 'America/Asuncion';

/**
 * Reconciliación diaria de relojes Hikvision (Fase 3 — respaldo del polling).
 *
 * Cada día a las 00:05 (America/Asuncion) re-consulta el día anterior completo vía
 * AcsEvent para recuperar marcaciones perdidas ante caídas prolongadas del polling.
 * La deduplicación es idempotente, por lo que re-procesar un día ya sincronizado
 * no genera duplicados.
 */
@Injectable()
export class PresentismoReconciliacionCron {
  private readonly logger = new Logger(PresentismoReconciliacionCron.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly relojes: RelojesMarcadoresService,
  ) {}

  @Cron('5 0 * * *', { name: 'rrhh-presentismo-reconciliacion', timeZone: TZ })
  async reconciliarDiaAnterior() {
    const { desde, hasta } = this.rangoDiaAnterior();

    const relojes = await this.prisma.rrhh_relojes_marcadores.findMany({
      where: { tipo_conexion: 'API', protocolo_api: 'HIKVISION_ISAPI', activo: true },
      select: { id: true, empresa_id: true, nombre: true },
    });
    if (relojes.length === 0) return;

    this.logger.log(
      `Reconciliación Hikvision ${desde.toISOString()}..${hasta.toISOString()} — ${relojes.length} reloj(es)`,
    );

    for (const r of relojes) {
      try {
        const res = await this.relojes.sincronizar(r.id, r.empresa_id, { desde, hasta });
        this.logger.log(
          `Reloj ${r.nombre}: ${res.insertadas_raw} raw / ${res.dedup.marcaciones_limpias_creadas} limpias nuevas`,
        );
      } catch (err: any) {
        // Una falla por reloj no detiene la reconciliación de los demás.
        this.logger.error(`Reloj ${r.nombre} (${r.id}): ${err?.message ?? err}`);
      }
    }
  }

  /** Rango [00:00:00, 23:59:59.999] del día anterior en {@link TZ}, como instantes UTC. */
  private rangoDiaAnterior(): { desde: Date; hasta: Date } {
    const hoy = new Intl.DateTimeFormat('en-CA', {
      timeZone: TZ,
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
    }).format(new Date());
    const [y, m, d] = hoy.split('-').map(Number);
    return {
      desde: this.wallTimeToUtc(y, m, d - 1, 0, 0, 0, 0),
      hasta: this.wallTimeToUtc(y, m, d - 1, 23, 59, 59, 0),
    };
  }

  /** Convierte una hora de pared local (en {@link TZ}) al instante UTC equivalente. */
  private wallTimeToUtc(
    y: number,
    mo: number,
    d: number,
    h: number,
    mi: number,
    s: number,
    ms: number,
  ): Date {
    // Date.UTC normaliza desbordes (d-1, fin de mes, etc.).
    const guess = Date.UTC(y, mo - 1, d, h, mi, s, ms);
    const offsetMin = this.tzOffsetMinutes(new Date(guess));
    return new Date(guess - offsetMin * 60_000);
  }

  /** Offset de la zona en minutos (local = UTC + offset; negativo al oeste de UTC). */
  private tzOffsetMinutes(date: Date): number {
    const parts = Object.fromEntries(
      new Intl.DateTimeFormat('en-US', {
        timeZone: TZ,
        hour12: false,
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit',
      })
        .formatToParts(date)
        .map((p) => [p.type, p.value]),
    );
    const asUtc = Date.UTC(
      Number(parts.year),
      Number(parts.month) - 1,
      Number(parts.day),
      Number(parts.hour === '24' ? '0' : parts.hour),
      Number(parts.minute),
      Number(parts.second),
    );
    return (asUtc - date.getTime()) / 60_000;
  }
}
