import {
  BadRequestException,
  ConflictException,
  Injectable,
  Logger,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { ParametrosService } from '../services/parametros.service';
import { TurnosResolverService } from '../services/turnos-resolver.service';

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

const ymd = (d: Date): string => d.toISOString().slice(0, 10);

const rangeDays = (desde: Date, hasta: Date): Date[] => {
  const out: Date[] = [];
  const cur = utcDateOnly(desde);
  const end = utcDateOnly(hasta);
  while (cur.getTime() <= end.getTime()) {
    out.push(new Date(cur));
    cur.setUTCDate(cur.getUTCDate() + 1);
  }
  return out;
};

const timeToMin = (value?: Date | null): number | null => {
  if (!value) return null;
  return value.getUTCHours() * 60 + value.getUTCMinutes();
};

const minToTime = (minutes: number): Date => {
  const hh = Math.floor(minutes / 60);
  const mm = minutes % 60;
  const d = new Date('1970-01-01T00:00:00Z');
  d.setUTCHours(hh, mm, 0, 0);
  return d;
};

type Permiso = {
  id: string;
  empleado_id: string;
  fecha: Date;
  tipo_permiso: string;
  turno_bloque_orden: number | null;
  hora_desde: Date | null;
  hora_hasta: Date | null;
  minutos_tolerados: number | null;
};

type EmpleadoLite = {
  id: string;
  sucursal_id: string;
  departamento_id: string;
  fecha_ingreso: Date;
  fecha_egreso: Date | null;
};

type ToleranciaDia = {
  entrada: number;
  salida: number;
};

type NovedadRow = Prisma.rrhh_asistencia_novedadesCreateManyInput;

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly parametros: ParametrosService,
    private readonly turnosResolver: TurnosResolverService,
  ) {}

  async procesarPeriodo(
    empresaId: string,
    opts: { fechaDesde: string; fechaHasta: string; sucursalId?: string; userId?: string },
  ) {
    const fechaDesde = utcDateOnly(new Date(opts.fechaDesde));
    const fechaHasta = utcDateOnly(new Date(opts.fechaHasta));
    if (Number.isNaN(fechaDesde.getTime()) || Number.isNaN(fechaHasta.getTime())) {
      throw new BadRequestException('Rango de fechas inválido');
    }
    if (fechaDesde > fechaHasta) {
      throw new BadRequestException('fecha_desde debe ser menor o igual a fecha_hasta');
    }

    await this.assertSinLiquidacionCerrada(empresaId, fechaDesde, fechaHasta, opts.sucursalId);
    const anuladas = await this.anularNovedadesPrevias(empresaId, fechaDesde, fechaHasta);

    const toleranciaEntradaGlobal = await this.leerParametroMinutos(
      empresaId,
      'PRESENTISMO_TOLERANCIA_ENTRADA_MIN',
      5,
    );
    const toleranciaSalidaGlobal = await this.leerParametroMinutos(
      empresaId,
      'PRESENTISMO_TOLERANCIA_SALIDA_MIN',
      5,
    );

    const empleados = await this.prisma.rrhh_empleados.findMany({
      where: {
        empresa_id: empresaId,
        estado: 'ACTIVO',
        fecha_ingreso: { lte: fechaHasta },
        OR: [{ fecha_egreso: null }, { fecha_egreso: { gte: fechaDesde } }],
        ...(opts.sucursalId ? { sucursal_id: opts.sucursalId } : {}),
      },
      select: {
        id: true,
        sucursal_id: true,
        departamento_id: true,
        fecha_ingreso: true,
        fecha_egreso: true,
      },
    });

    if (empleados.length === 0) {
      return {
        fecha_desde: ymd(fechaDesde),
        fecha_hasta: ymd(fechaHasta),
        empleados_evaluados: 0,
        dias_evaluados: 0,
        novedades_generadas: 0,
        novedades_previas_anuladas: anuladas,
        marcaciones_marcadas_procesadas: 0,
      };
    }

    const empleadosById = new Map(empleados.map((e) => [e.id, e]));
    const empleadoIds = empleados.map((e) => e.id);

    const [permisos, marcaciones, toleranciasEspeciales] = await Promise.all([
      this.prisma.rrhh_permisos_presentismo.findMany({
        where: {
          empresa_id: empresaId,
          empleado_id: { in: empleadoIds },
          estado: 'APROBADO',
          fecha: { gte: fechaDesde, lte: fechaHasta },
        },
        select: {
          id: true,
          empleado_id: true,
          fecha: true,
          tipo_permiso: true,
          turno_bloque_orden: true,
          hora_desde: true,
          hora_hasta: true,
          minutos_tolerados: true,
        },
      }),
      this.prisma.rrhh_marcaciones_limpias.findMany({
        where: {
          empresa_id: empresaId,
          empleado_id: { in: empleadoIds },
          fecha: { gte: fechaDesde, lte: fechaHasta },
        },
        select: {
          id: true,
          empleado_id: true,
          fecha: true,
          hora_entrada: true,
          hora_salida: true,
        },
      }),
      this.prisma.rrhh_presentismo_tolerancias_especiales.findMany({
        where: {
          empresa_id: empresaId,
          fecha: { gte: fechaDesde, lte: fechaHasta },
        },
        select: {
          fecha: true,
          tolerancia_entrada_min: true,
          tolerancia_salida_min: true,
          aplica_a: true,
          aplica_a_id: true,
        },
      }),
    ]);

    const permisosByDia = new Map<string, Permiso[]>();
    for (const p of permisos) {
      const key = `${p.empleado_id}|${ymd(p.fecha)}`;
      const list = permisosByDia.get(key) ?? [];
      list.push(p as Permiso);
      permisosByDia.set(key, list);
    }

    const marcacionesByDia = new Map<string, (typeof marcaciones)[number]>();
    for (const m of marcaciones) {
      marcacionesByDia.set(`${m.empleado_id}|${ymd(m.fecha)}`, m);
    }

    const toleranciasByDia = new Map<string, (typeof toleranciasEspeciales)>();
    for (const t of toleranciasEspeciales) {
      const key = ymd(t.fecha);
      const list = toleranciasByDia.get(key) ?? [];
      list.push(t);
      toleranciasByDia.set(key, list);
    }

    const dias = rangeDays(fechaDesde, fechaHasta);
    const rows: NovedadRow[] = [];
    const marcacionesProcesadas = new Set<string>();

    for (const dia of dias) {
      for (const emp of empleados) {
        if (!this.empleadoActivoEnDia(emp, dia)) continue;

        const turnoVigente = await this.turnosResolver.resolverTurnoVigente(empresaId, emp.id, dia);
        if (!turnoVigente?.turno?.bloques?.length) continue;

        const permisosDia = permisosByDia.get(`${emp.id}|${ymd(dia)}`) ?? [];

        const permisoJornada = permisosDia.find((p) => p.tipo_permiso === 'JORNADA_COMPLETA');
        if (permisoJornada) {
          rows.push(
            this.buildNovedadRow({
              empresaId,
              empleadoId: emp.id,
              fecha: dia,
              tipo: 'AUSENCIA_JUSTIFICADA',
              turnoId: turnoVigente.turno_id,
              permisoId: permisoJornada.id,
              justificado: true,
              impactaLiquidacion: false,
              tipoJustificacion: 'PERMISO_JORNADA_COMPLETA',
              observacion: 'Permiso JORNADA_COMPLETA aprobado',
              userId: opts.userId,
            }),
          );
          const marc = marcacionesByDia.get(`${emp.id}|${ymd(dia)}`);
          if (marc) marcacionesProcesadas.add(marc.id);
          continue;
        }

        const marc = marcacionesByDia.get(`${emp.id}|${ymd(dia)}`);
        const entradaMin = timeToMin(marc?.hora_entrada);
        const salidaMin = timeToMin(marc?.hora_salida);
        const tolerancia = this.resolverToleranciaDelDia(
          emp,
          toleranciasByDia.get(ymd(dia)) ?? [],
          toleranciaEntradaGlobal,
          toleranciaSalidaGlobal,
        );

        for (const bloque of turnoVigente.turno.bloques) {
          const bloqueEntrada = timeToMin(bloque.hora_entrada)!;
          const bloqueSalida = timeToMin(bloque.hora_salida)!;

          const permisoAusenciaTurno = permisosDia.find(
            (p) => p.tipo_permiso === 'AUSENCIA_TURNO' && p.turno_bloque_orden === bloque.orden,
          );
          if (permisoAusenciaTurno) {
            rows.push(
              this.buildNovedadRow({
                empresaId,
                empleadoId: emp.id,
                fecha: dia,
                tipo: 'AUSENCIA_JUSTIFICADA',
                turnoId: turnoVigente.turno_id,
                turnoBloqueOrden: bloque.orden,
                permisoId: permisoAusenciaTurno.id,
                justificado: true,
                impactaLiquidacion: false,
                tipoJustificacion: 'PERMISO_AUSENCIA_TURNO',
                horaEsperadaMin: bloqueEntrada,
                userId: opts.userId,
                observacion: `Permiso AUSENCIA_TURNO bloque ${bloque.orden}`,
              }),
            );
            continue;
          }

          // Bloque inicial: evaluamos entrada/tardanza con hora_entrada.
          if (bloque.orden === 1) {
            if (entradaMin == null) {
              rows.push(
                this.buildNovedadRow({
                  empresaId,
                  empleadoId: emp.id,
                  fecha: dia,
                  tipo: 'AUSENCIA_JORNADA',
                  turnoId: turnoVigente.turno_id,
                  turnoBloqueOrden: bloque.orden,
                  horaEsperadaMin: bloqueEntrada,
                  impactaLiquidacion: true,
                  userId: opts.userId,
                  observacion: 'Sin marcación de entrada',
                }),
              );
              break;
            }

            const tardanzaMin = entradaMin - bloqueEntrada;
            if (tardanzaMin > tolerancia.entrada) {
              const permisoLT = permisosDia.find(
                (p) =>
                  p.tipo_permiso === 'LLEGADA_TARDIA' &&
                  this.cubreHora(p, entradaMin) &&
                  (p.minutos_tolerados == null || tardanzaMin <= p.minutos_tolerados),
              );

              if (permisoLT) {
                rows.push(
                  this.buildNovedadRow({
                    empresaId,
                    empleadoId: emp.id,
                    fecha: dia,
                    tipo: 'TARDANZA_JUSTIFICADA',
                    turnoId: turnoVigente.turno_id,
                    turnoBloqueOrden: bloque.orden,
                    permisoId: permisoLT.id,
                    justificado: true,
                    impactaLiquidacion: false,
                    tipoJustificacion: 'PERMISO_LLEGADA_TARDIA',
                    horaEsperadaMin: bloqueEntrada,
                    horaRealMin: entradaMin,
                    minutos: tardanzaMin,
                    userId: opts.userId,
                  }),
                );
              } else {
                rows.push(
                  this.buildNovedadRow({
                    empresaId,
                    empleadoId: emp.id,
                    fecha: dia,
                    tipo: 'TARDANZA',
                    turnoId: turnoVigente.turno_id,
                    turnoBloqueOrden: bloque.orden,
                    impactaLiquidacion: true,
                    horaEsperadaMin: bloqueEntrada,
                    horaRealMin: entradaMin,
                    minutos: tardanzaMin,
                    toleranciaMin: tolerancia.entrada,
                    userId: opts.userId,
                  }),
                );
              }
            }
          }

          // Bloques posteriores: si la salida real no alcanza la entrada del bloque,
          // asumimos ausencia del bloque (caso típico de turno cortado).
          if (bloque.orden > 1) {
            if (salidaMin == null || salidaMin < bloqueEntrada) {
              rows.push(
                this.buildNovedadRow({
                  empresaId,
                  empleadoId: emp.id,
                  fecha: dia,
                  tipo: 'AUSENCIA_BLOQUE',
                  turnoId: turnoVigente.turno_id,
                  turnoBloqueOrden: bloque.orden,
                  impactaLiquidacion: true,
                  horaEsperadaMin: bloqueEntrada,
                  minutos: Math.max(0, bloqueSalida - bloqueEntrada),
                  userId: opts.userId,
                  observacion: 'Marcación incompleta para turno cortado',
                }),
              );
              continue;
            }
          }

          if (salidaMin != null) {
            const anticipacionMin = bloqueSalida - salidaMin;
            if (anticipacionMin > tolerancia.salida) {
              const permisoSA = permisosDia.find(
                (p) => p.tipo_permiso === 'SALIDA_ANTICIPADA' && this.cubreHora(p, salidaMin),
              );
              if (!permisoSA) {
                rows.push(
                  this.buildNovedadRow({
                    empresaId,
                    empleadoId: emp.id,
                    fecha: dia,
                    tipo: 'SALIDA_ANTICIPADA',
                    turnoId: turnoVigente.turno_id,
                    turnoBloqueOrden: bloque.orden,
                    impactaLiquidacion: true,
                    horaEsperadaMin: bloqueSalida,
                    horaRealMin: salidaMin,
                    minutos: anticipacionMin,
                    toleranciaMin: tolerancia.salida,
                    userId: opts.userId,
                  }),
                );
              }
            }
          }
        }

        if (marc) {
          marcacionesProcesadas.add(marc.id);
        }
      }
    }

    if (rows.length > 0) {
      await this.prisma.rrhh_asistencia_novedades.createMany({ data: rows });
    }

    if (marcacionesProcesadas.size > 0) {
      await this.prisma.rrhh_marcaciones_limpias.updateMany({
        where: {
          id: { in: [...marcacionesProcesadas] },
          empresa_id: empresaId,
        },
        data: {
          procesado_novedades: true,
          updated_at: new Date(),
        },
      });
    }

    this.logger.log(
      `M19 procesado empresa=${empresaId} rango=${ymd(fechaDesde)}..${ymd(fechaHasta)} ` +
        `anuladas=${anuladas} nuevas=${rows.length} marcaciones=${marcacionesProcesadas.size}`,
    );

    return {
      fecha_desde: ymd(fechaDesde),
      fecha_hasta: ymd(fechaHasta),
      empleados_evaluados: empleados.length,
      dias_evaluados: dias.length,
      novedades_generadas: rows.length,
      novedades_previas_anuladas: anuladas,
      marcaciones_marcadas_procesadas: marcacionesProcesadas.size,
    };
  }

  private async assertSinLiquidacionCerrada(
    empresaId: string,
    fechaDesde: Date,
    fechaHasta: Date,
    sucursalId?: string,
  ) {
    const periodos = this.periodosEntre(fechaDesde, fechaHasta);
    const conflicto = await this.prisma.rrhh_liquidaciones_cabecera.findFirst({
      where: {
        empresa_id: empresaId,
        estado: 'CERRADA',
        tipo: 'MENSUAL',
        ...(sucursalId ? { sucursal_id: sucursalId } : {}),
        OR: periodos.map((p) => ({ periodo_anio: p.anio, periodo_mes: p.mes })),
      },
      select: { id: true, periodo_anio: true, periodo_mes: true },
    });

    if (conflicto) {
      throw new ConflictException(
        `No se puede procesar novedades: existe liquidación CERRADA para ${conflicto.periodo_mes}/${conflicto.periodo_anio}`,
      );
    }
  }

  private periodosEntre(fechaDesde: Date, fechaHasta: Date): Array<{ anio: number; mes: number }> {
    const items: Array<{ anio: number; mes: number }> = [];
    let y = fechaDesde.getUTCFullYear();
    let m = fechaDesde.getUTCMonth() + 1;
    const yEnd = fechaHasta.getUTCFullYear();
    const mEnd = fechaHasta.getUTCMonth() + 1;

    while (y < yEnd || (y === yEnd && m <= mEnd)) {
      items.push({ anio: y, mes: m });
      m += 1;
      if (m > 12) {
        m = 1;
        y += 1;
      }
    }
    return items;
  }

  private async anularNovedadesPrevias(empresaId: string, fechaDesde: Date, fechaHasta: Date) {
    const conLiquidacion = await this.prisma.rrhh_asistencia_novedades.findFirst({
      where: {
        empresa_id: empresaId,
        fecha: { gte: fechaDesde, lte: fechaHasta },
        generado_automatico: true,
        estado: 'CONFIRMADA',
        liquidacion_id: { not: null },
      },
      select: { id: true },
    });
    if (conLiquidacion) {
      throw new ConflictException('Existen novedades del período ya aplicadas en liquidación. Anule la liquidación primero.');
    }

    const r = await this.prisma.rrhh_asistencia_novedades.updateMany({
      where: {
        empresa_id: empresaId,
        fecha: { gte: fechaDesde, lte: fechaHasta },
        generado_automatico: true,
        estado: 'CONFIRMADA',
      },
      data: {
        estado: 'ANULADA',
        updated_at: new Date(),
      },
    });

    return r.count;
  }

  private async leerParametroMinutos(empresaId: string, clave: string, fallback: number): Promise<number> {
    try {
      const p = await this.parametros.obtenerVigente(empresaId, clave, new Date());
      const n = Number((p as any)?.valor);
      if (!Number.isFinite(n)) return fallback;
      return Math.max(0, Math.floor(n));
    } catch {
      return fallback;
    }
  }

  private resolverToleranciaDelDia(
    empleado: EmpleadoLite,
    especiales: Array<{
      aplica_a: string;
      aplica_a_id: string | null;
      tolerancia_entrada_min: number;
      tolerancia_salida_min: number;
    }>,
    globalEntrada: number,
    globalSalida: number,
  ): ToleranciaDia {
    const candidates = especiales.filter((e) => {
      if (e.aplica_a === 'TODOS') return true;
      if (e.aplica_a === 'SUCURSAL') return e.aplica_a_id === empleado.sucursal_id;
      if (e.aplica_a === 'DEPARTAMENTO') return e.aplica_a_id === empleado.departamento_id;
      return false;
    });

    if (!candidates.length) return { entrada: globalEntrada, salida: globalSalida };

    const score = (aplica: string) => {
      if (aplica === 'DEPARTAMENTO') return 3;
      if (aplica === 'SUCURSAL') return 2;
      return 1;
    };
    candidates.sort((a, b) => score(b.aplica_a) - score(a.aplica_a));
    const best = candidates[0];

    return {
      entrada: best.tolerancia_entrada_min,
      salida: best.tolerancia_salida_min,
    };
  }

  private cubreHora(permiso: Permiso, min: number): boolean {
    const desde = timeToMin(permiso.hora_desde);
    const hasta = timeToMin(permiso.hora_hasta);
    if (desde == null || hasta == null) return true;
    return min >= desde && min <= hasta;
  }

  private empleadoActivoEnDia(emp: EmpleadoLite, dia: Date): boolean {
    const d = utcDateOnly(dia).getTime();
    const desde = utcDateOnly(emp.fecha_ingreso).getTime();
    const hasta = emp.fecha_egreso ? utcDateOnly(emp.fecha_egreso).getTime() : null;
    return d >= desde && (hasta == null || d <= hasta);
  }

  private buildNovedadRow(args: {
    empresaId: string;
    empleadoId: string;
    fecha: Date;
    tipo: Prisma.rrhh_asistencia_novedadesCreateManyInput['tipo_novedad'];
    turnoId?: string;
    turnoBloqueOrden?: number;
    permisoId?: string;
    horaEsperadaMin?: number;
    horaRealMin?: number;
    toleranciaMin?: number;
    minutos?: number;
    justificado?: boolean;
    impactaLiquidacion?: boolean;
    tipoJustificacion?: string;
    observacion?: string;
    userId?: string;
  }): NovedadRow {
    const minutos = args.minutos != null ? Math.max(0, Math.floor(args.minutos)) : null;

    return {
      empresa_id: args.empresaId,
      empleado_id: args.empleadoId,
      fecha: utcDateOnly(args.fecha),
      tipo_novedad: args.tipo,
      hora_entrada_esperada: args.horaEsperadaMin != null ? minToTime(args.horaEsperadaMin) : null,
      hora_entrada_real: args.horaRealMin != null ? minToTime(args.horaRealMin) : null,
      minutos_tardanza: minutos,
      justificado: args.justificado ?? false,
      tipo_justificacion: args.tipoJustificacion ?? null,
      impacta_liquidacion: args.impactaLiquidacion ?? true,
      observacion: args.observacion ?? null,
      creado_por: args.userId ?? null,
      turno_id: args.turnoId ?? null,
      turno_bloque_orden: args.turnoBloqueOrden ?? null,
      hora_esperada: args.horaEsperadaMin != null ? minToTime(args.horaEsperadaMin) : null,
      hora_real: args.horaRealMin != null ? minToTime(args.horaRealMin) : null,
      tolerancia_aplicada_min: args.toleranciaMin ?? null,
      permiso_id: args.permisoId ?? null,
      generado_automatico: true,
      estado: 'CONFIRMADA',
      tipo_novedad_legacy: null,
      updated_at: new Date(),
    };
  }
}
