import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';

/**
 * Deduplicación de marcaciones (M22).
 *
 * Reglas del plan:
 *  - Agrupa marcaciones_raw por (empleado_id, DATE(timestamp_marcacion)).
 *  - Entrada = min(timestamp). Salida = max(timestamp).
 *  - Si hay más de una marcación distinta en el grupo, las no seleccionadas
 *    pasan a estado_importacion = DUPLICADO.
 *  - Cada grupo con duplicados genera UNA fila en rrhh_marcaciones_log_correcciones.
 *  - Idempotente: re-ejecutar la deduplicación sobre el mismo conjunto de raw
 *    no crea duplicados en marcaciones_limpias ni en el log (se upsertea por
 *    (empleado_id, fecha) y se evita reinsertar log si ya existe para el batch).
 *
 * No procesa marcaciones con estado_importacion = ERROR ni las que tengan
 * empleado_id NULL (documento no matcheado).
 */

type GrupoDia = {
  empleado_id: string;
  fecha: Date; // UTC date 00:00
  raw: Array<{ id: string; timestamp: Date; estado: string }>;
  batch_ids: Set<string>;
};

export type DedupResultado = {
  batch_id: string;
  grupos_procesados: number;
  marcaciones_limpias_creadas: number;
  marcaciones_limpias_actualizadas: number;
  marcaciones_duplicadas: number;
  log_correcciones_creados: number;
};

const utcDateOnly = (d: Date): Date =>
  new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));

const onlyTime = (d: Date): Date => {
  // Convierte un timestamp a Date sólo-hora sobre la época 1970-01-01 UTC,
  // que es la representación que usa Postgres para columnas TIME.
  return new Date(Date.UTC(1970, 0, 1, d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()));
};

@Injectable()
export class MarcacionesDedupService {
  private readonly logger = new Logger(MarcacionesDedupService.name);

  constructor(private readonly prisma: PrismaService) {}

  /**
   * Deduplica todas las marcaciones de un batch específico.
   * Útil tras una importación o sincronización.
   */
  async deduplicarBatch(empresaId: string, batchId: string): Promise<DedupResultado> {
    // 1. Recoger raw del batch (que pertenezcan a empleados identificados y NO estén en ERROR).
    const raws = await this.prisma.rrhh_marcaciones_raw.findMany({
      where: {
        empresa_id: empresaId,
        batch_importacion_id: batchId,
        empleado_id: { not: null },
        estado_importacion: { not: 'ERROR' },
      },
      select: {
        id: true,
        empleado_id: true,
        timestamp_marcacion: true,
        estado_importacion: true,
      },
      orderBy: { timestamp_marcacion: 'asc' },
    });

    if (raws.length === 0) {
      return {
        batch_id: batchId,
        grupos_procesados: 0,
        marcaciones_limpias_creadas: 0,
        marcaciones_limpias_actualizadas: 0,
        marcaciones_duplicadas: 0,
        log_correcciones_creados: 0,
      };
    }

    // Para idempotencia: si el batch trajo marcaciones de un mismo (empleado, día),
    // PERO ya hay marcaciones_raw previas del MISMO día anteriores al batch, hay que
    // considerar también esas para elegir min/max definitivos.
    const empleadoIds = Array.from(new Set(raws.map((r) => r.empleado_id!)));
    const fechas = Array.from(
      new Set(raws.map((r) => utcDateOnly(r.timestamp_marcacion).toISOString())),
    ).map((iso) => new Date(iso));

    const fechaMin = new Date(Math.min(...fechas.map((f) => f.getTime())));
    const fechaMax = new Date(Math.max(...fechas.map((f) => f.getTime())));
    // Rango [fechaMin 00:00, fechaMax + 1 día 00:00)
    const tsDesde = fechaMin;
    const tsHastaExcl = new Date(fechaMax.getTime() + 86_400_000);

    const allRawsRelevantes = await this.prisma.rrhh_marcaciones_raw.findMany({
      where: {
        empresa_id: empresaId,
        empleado_id: { in: empleadoIds },
        timestamp_marcacion: { gte: tsDesde, lt: tsHastaExcl },
        estado_importacion: { not: 'ERROR' },
      },
      select: {
        id: true,
        empleado_id: true,
        timestamp_marcacion: true,
        estado_importacion: true,
        batch_importacion_id: true,
      },
      orderBy: { timestamp_marcacion: 'asc' },
    });

    // 2. Agrupar por (empleado_id, fecha)
    const grupos = new Map<string, GrupoDia>();
    for (const r of allRawsRelevantes) {
      const fecha = utcDateOnly(r.timestamp_marcacion);
      const key = `${r.empleado_id}|${fecha.toISOString()}`;
      let g = grupos.get(key);
      if (!g) {
        g = { empleado_id: r.empleado_id!, fecha, raw: [], batch_ids: new Set() };
        grupos.set(key, g);
      }
      g.raw.push({ id: r.id, timestamp: r.timestamp_marcacion, estado: r.estado_importacion });
      if (r.batch_importacion_id) g.batch_ids.add(r.batch_importacion_id);
    }

    let creadas = 0;
    let actualizadas = 0;
    let duplicadas = 0;
    let logs = 0;

    // 3. Procesar cada grupo en transacción
    for (const grupo of grupos.values()) {
      if (!grupo.batch_ids.has(batchId)) {
        // El grupo no fue tocado por este batch (sólo data histórica); lo salteamos.
        continue;
      }

      const ordenadas = [...grupo.raw].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
      const seleccionEntrada = ordenadas[0];
      const seleccionSalida = ordenadas[ordenadas.length - 1];
      const esMismaMarcacion = seleccionEntrada.id === seleccionSalida.id;

      const idsSeleccionados = new Set<string>([seleccionEntrada.id, seleccionSalida.id]);
      const descartados = ordenadas.filter((r) => !idsSeleccionados.has(r.id));

      const out = await this.prisma.$transaction(async (tx) => {
        // 3.a. Estado de las raw del grupo
        if (idsSeleccionados.size > 0) {
          await tx.rrhh_marcaciones_raw.updateMany({
            where: { id: { in: Array.from(idsSeleccionados) } },
            data: { estado_importacion: 'PROCESADO' },
          });
        }
        if (descartados.length > 0) {
          await tx.rrhh_marcaciones_raw.updateMany({
            where: { id: { in: descartados.map((d) => d.id) } },
            data: { estado_importacion: 'DUPLICADO' },
          });
        }

        // 3.b. Upsert marcaciones_limpias (UNIQUE empleado_id+fecha)
        const dataLimpias: Prisma.rrhh_marcaciones_limpiasUncheckedCreateInput = {
          empresa_id: empresaId,
          empleado_id: grupo.empleado_id,
          fecha: grupo.fecha,
          hora_entrada: onlyTime(seleccionEntrada.timestamp),
          hora_salida: esMismaMarcacion ? null : onlyTime(seleccionSalida.timestamp),
          turno_id: null,
          procesado_novedades: false,
          marcacion_raw_entrada_id: seleccionEntrada.id,
          marcacion_raw_salida_id: esMismaMarcacion ? null : seleccionSalida.id,
        };

        const existente = await tx.rrhh_marcaciones_limpias.findUnique({
          where: {
            empleado_id_fecha: { empleado_id: grupo.empleado_id, fecha: grupo.fecha },
          },
          select: { id: true, procesado_novedades: true },
        });

        if (existente) {
          // Si ya fue procesada por el motor de novedades, no la cambiamos en silencio:
          // pasa a procesado_novedades = false para que se reprocese, y se actualiza
          // sólo si hay diferencias.
          await tx.rrhh_marcaciones_limpias.update({
            where: { id: existente.id },
            data: {
              hora_entrada: dataLimpias.hora_entrada,
              hora_salida: dataLimpias.hora_salida,
              marcacion_raw_entrada_id: dataLimpias.marcacion_raw_entrada_id,
              marcacion_raw_salida_id: dataLimpias.marcacion_raw_salida_id,
              procesado_novedades: false,
              updated_at: new Date(),
            },
          });
          return { creada: false, actualizada: true } as const;
        } else {
          await tx.rrhh_marcaciones_limpias.create({ data: dataLimpias });
          return { creada: true, actualizada: false } as const;
        }
      });

      if (out.creada) creadas++;
      if (out.actualizada) actualizadas++;
      duplicadas += descartados.length;

      // 3.c. Log de correcciones (sólo si hubo duplicados en este grupo)
      if (descartados.length > 0) {
        const motivo: 'DUPLICADO_ENTRADA' | 'DUPLICADO_SALIDA' | 'DUPLICADO_AMBOS' =
          this.calcularMotivo(ordenadas, seleccionEntrada.id, seleccionSalida.id);
        // Idempotencia: si ya existe un log para (empleado, fecha, batch), no duplicarlo.
        const yaHay = await this.prisma.rrhh_marcaciones_log_correcciones.findFirst({
          where: {
            empleado_id: grupo.empleado_id,
            fecha: grupo.fecha,
            batch_importacion_id: batchId,
          },
          select: { id: true },
        });
        if (!yaHay) {
          await this.prisma.rrhh_marcaciones_log_correcciones.create({
            data: {
              empresa_id: empresaId,
              empleado_id: grupo.empleado_id,
              fecha: grupo.fecha,
              batch_importacion_id: batchId,
              cantidad_marcaciones_raw: ordenadas.length,
              cantidad_duplicados_descartados: descartados.length,
              timestamp_entrada_seleccionado: seleccionEntrada.timestamp,
              timestamp_salida_seleccionado: esMismaMarcacion ? null : seleccionSalida.timestamp,
              timestamps_descartados: descartados.map((d) => d.timestamp.toISOString()),
              motivo_correccion: motivo,
            },
          });
          logs++;
        }
      }
    }

    this.logger.log(
      `Dedup batch=${batchId} empresa=${empresaId} → ${creadas} nuevas, ${actualizadas} actualizadas, ` +
        `${duplicadas} marcadas duplicadas, ${logs} entradas en log de correcciones.`,
    );

    return {
      batch_id: batchId,
      grupos_procesados: grupos.size,
      marcaciones_limpias_creadas: creadas,
      marcaciones_limpias_actualizadas: actualizadas,
      marcaciones_duplicadas: duplicadas,
      log_correcciones_creados: logs,
    };
  }

  private calcularMotivo(
    ordenadas: Array<{ id: string; timestamp: Date }>,
    idEntrada: string,
    idSalida: string,
  ): 'DUPLICADO_ENTRADA' | 'DUPLICADO_SALIDA' | 'DUPLICADO_AMBOS' {
    // Posición de la entrada en el sort ascendente
    const idxEntrada = ordenadas.findIndex((r) => r.id === idEntrada); // siempre 0
    const idxSalida = ordenadas.findIndex((r) => r.id === idSalida); // siempre length-1
    // Identificamos qué "lado" tuvo duplicados:
    const hayPrevios = idxEntrada > 0;
    const hayPosterioresAEntrada = ordenadas.length > 1;
    // Si la entrada está en posición 0 y la salida en N-1, los descartados son los del medio.
    // Si idEntrada === idSalida (mismo timestamp único pero hay copias), agrupamos como AMBOS.
    if (idEntrada === idSalida && ordenadas.length > 1) return 'DUPLICADO_AMBOS';

    const cantEntreEntrada = idxSalida - idxEntrada - 1;
    const huboCercaDeEntrada = hayPrevios; // si min no era único
    const huboCercaDeSalida = idxSalida < ordenadas.length - 1;
    const hayMedios = cantEntreEntrada > 0;

    if (huboCercaDeEntrada && huboCercaDeSalida) return 'DUPLICADO_AMBOS';
    if (huboCercaDeEntrada) return 'DUPLICADO_ENTRADA';
    if (huboCercaDeSalida) return 'DUPLICADO_SALIDA';
    if (hayMedios) return 'DUPLICADO_AMBOS';
    return 'DUPLICADO_AMBOS';
  }
}
