import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { MailService } from '../../mail/mail.service';
import { LegajoVencimientoEstado } from '../dto/legajos.dto';
import { ParametrosService } from './parametros.service';

export interface VencimientoJobResultado {
  empresas_procesadas: number;
  documentos_evaluados: number;
  documentos_actualizados: number;
  alertas_enviadas: number;
  alertas_omitidas: number;
  alertas_con_error: number;
  errores: Array<{ documento_id: string; mensaje: string }>;
}

interface AlertaCandidata {
  documento_id: string;
  empresa_id: string;
  empleado_id: string;
  dias_para_vencer: number;
  es_vencido: boolean;
  fecha_vencimiento: string;
  empleado_nombre: string;
  empleado_email: string | null;
  empresa_nombre: string;
  tipo_codigo: string;
  tipo_nombre: string;
  nombre_original: string;
}

/**
 * Job idempotente que recalcula los estados de vencimiento de todos los documentos
 * ACTIVOS con fecha_vencimiento no nula y dispara alertas escalonadas por email
 * según el parámetro LEGAJO_DIAS_ANTICIPACION_EMAIL.
 *
 * Reglas (PDF Novasis_ERP_RRHH_Legajos_v1 §5):
 *  - `estado_vencimiento` se calcula de la siguiente forma:
 *      SIN_VENCIMIENTO si fecha_vencimiento es NULL.
 *      VENCIDO         si fecha_vencimiento <= hoy.
 *      POR_VENCER      si fecha_vencimiento <= hoy + dias_alerta (custom del tipo o LEGAJO_DIAS_ALERTA_VENCIMIENTO).
 *      VIGENTE         caso contrario.
 *  - Alertas escalonadas: si los días restantes para el vencimiento (positivo o cero)
 *    están en la lista LEGAJO_DIAS_ANTICIPACION_EMAIL → enviar email.
 *    También se envía cuando ya está VENCIDO (dias_para_vencer = 0 cubre el día exacto;
 *    para mantener simple, sólo enviamos al cruzar exactamente cada umbral).
 *  - Idempotencia: la tabla `rrhh_legajo_alertas_vencimiento` tiene UNIQUE
 *    (documento_id, dias_para_vencer, canal). Si se intenta insertar de nuevo,
 *    se omite silenciosamente.
 */
@Injectable()
export class LegajosVencimientoJobService {
  private readonly logger = new Logger(LegajosVencimientoJobService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly parametros: ParametrosService,
    private readonly mail: MailService,
  ) {}

  /**
   * Ejecuta el job para todas las empresas o una específica.
   * @param empresaId opcional — si se pasa, sólo procesa esa empresa.
   */
  async ejecutar(empresaId?: string): Promise<VencimientoJobResultado> {
    const resultado: VencimientoJobResultado = {
      empresas_procesadas: 0,
      documentos_evaluados: 0,
      documentos_actualizados: 0,
      alertas_enviadas: 0,
      alertas_omitidas: 0,
      alertas_con_error: 0,
      errores: [],
    };

    const empresas = empresaId
      ? await this.prisma.empresas.findMany({
          where: { id: empresaId },
          select: { id: true, razon_social: true, nombre_fantasia: true },
        })
      : await this.empresasConDocsActivos();

    for (const empresa of empresas) {
      try {
        const nombre = empresa.nombre_fantasia || empresa.razon_social || 'Empresa';
        await this.procesarEmpresa(empresa.id, nombre, resultado);
        resultado.empresas_procesadas++;
      } catch (err) {
        this.logger.error(
          `Error procesando empresa ${empresa.id}: ${err instanceof Error ? err.message : err}`,
        );
      }
    }

    this.logger.log(
      `Vencimientos job — empresas=${resultado.empresas_procesadas}, docs=${resultado.documentos_evaluados}, ` +
        `actualizados=${resultado.documentos_actualizados}, alertas=${resultado.alertas_enviadas}, ` +
        `omitidas=${resultado.alertas_omitidas}, errores=${resultado.alertas_con_error}`,
    );

    return resultado;
  }

  // ──────────────────────────────────────────────────────────────────────

  private async empresasConDocsActivos() {
    return this.prisma.empresas.findMany({
      where: {
        rrhh_legajo_documentos: {
          some: { estado: 'ACTIVO', fecha_vencimiento: { not: null } },
        },
      },
      select: { id: true, razon_social: true, nombre_fantasia: true },
    });
  }

  private async procesarEmpresa(
    empresaId: string,
    empresaNombre: string,
    resultado: VencimientoJobResultado,
  ) {
    const [paramDiasAlerta, paramAlertasHabilitado, paramDiasAnticipacion] = await Promise.all([
      this.parametros.obtenerVigente(empresaId, 'LEGAJO_DIAS_ALERTA_VENCIMIENTO'),
      this.parametros.obtenerVigente(empresaId, 'LEGAJO_ALERTAS_EMAIL_HABILITADO'),
      this.parametros.obtenerVigente(empresaId, 'LEGAJO_DIAS_ANTICIPACION_EMAIL'),
    ]);

    const diasAlertaDefault = paramDiasAlerta?.valor ? Number(paramDiasAlerta.valor) : 30;
    const alertasHabilitadas = (paramAlertasHabilitado?.valor ?? 'true').toLowerCase() === 'true';
    const diasAnticipacion = this.parseDiasAnticipacion(paramDiasAnticipacion?.valor ?? '30,15,7');

    const docs = await this.prisma.rrhh_legajo_documentos.findMany({
      where: {
        empresa_id: empresaId,
        estado: 'ACTIVO',
        fecha_vencimiento: { not: null },
      },
      include: {
        tipo_documento: { select: { codigo: true, nombre: true, dias_alerta_vencimiento: true } },
        empleado: { select: { id: true, nombres: true, apellidos: true, email: true } },
      },
    });

    const inicioHoy = this.inicioDeHoy();
    const candidatas: AlertaCandidata[] = [];

    for (const doc of docs) {
      resultado.documentos_evaluados++;
      const diasAlerta = doc.tipo_documento?.dias_alerta_vencimiento ?? diasAlertaDefault;
      const fechaVenc = doc.fecha_vencimiento as Date;
      const nuevoEstado = this.calcularEstado(fechaVenc, inicioHoy, diasAlerta);

      if (nuevoEstado !== doc.estado_vencimiento) {
        await this.prisma.rrhh_legajo_documentos.update({
          where: { id: doc.id },
          data: { estado_vencimiento: nuevoEstado as any },
        });
        resultado.documentos_actualizados++;
      }

      if (!alertasHabilitadas) continue;

      const diasParaVencer = this.diferenciaDias(inicioHoy, fechaVenc);
      // Solo enviar alertas si el día de hoy coincide con uno de los umbrales configurados.
      // Esto hace que se mande exactamente una vez por umbral (la tabla tiene UNIQUE como red de seguridad).
      if (!diasAnticipacion.includes(diasParaVencer)) continue;

      const empleadoNombre = `${doc.empleado.apellidos || ''} ${doc.empleado.nombres || ''}`.trim() ||
        `Funcionario ${doc.empleado.id.slice(0, 8)}`;

      candidatas.push({
        documento_id: doc.id,
        empresa_id: empresaId,
        empleado_id: doc.empleado_id,
        dias_para_vencer: diasParaVencer,
        es_vencido: diasParaVencer <= 0,
        fecha_vencimiento: fechaVenc.toISOString().slice(0, 10),
        empleado_nombre: empleadoNombre,
        empleado_email: doc.empleado.email,
        empresa_nombre: empresaNombre,
        tipo_codigo: doc.tipo_documento?.codigo || 'OTRO',
        tipo_nombre: doc.tipo_documento?.nombre || 'Documento',
        nombre_original: doc.nombre_original,
      });
    }

    if (candidatas.length === 0) return;

    // Agrupamos alertas por empleado para mandar UN email con todos los documentos por umbral.
    const grupos = new Map<string, AlertaCandidata[]>();
    for (const c of candidatas) {
      const key = `${c.empleado_id}:${c.dias_para_vencer}`;
      if (!grupos.has(key)) grupos.set(key, []);
      grupos.get(key)!.push(c);
    }

    for (const [, items] of grupos) {
      await this.enviarYRegistrar(items, empresaId, resultado);
    }
  }

  private async enviarYRegistrar(
    items: AlertaCandidata[],
    empresaId: string,
    resultado: VencimientoJobResultado,
  ) {
    const head = items[0];
    const destinatarios = await this.resolverDestinatarios(empresaId, head.empleado_email);

    if (destinatarios.length === 0) {
      resultado.alertas_omitidas += items.length;
      return;
    }

    const documentos = items.map((c) => ({
      tipo_nombre: c.tipo_nombre,
      codigo: c.tipo_codigo,
      nombre_original: c.nombre_original,
      fecha_vencimiento: c.fecha_vencimiento,
      dias_para_vencer: c.dias_para_vencer,
      es_vencido: c.es_vencido,
    }));

    try {
      await this.mail.sendLegajoVencimientoAlert(destinatarios, {
        empresaNombre: head.empresa_nombre,
        empleadoNombre: head.empleado_nombre,
        empleadoEmail: head.empleado_email,
        documentos,
      });

      for (const c of items) {
        await this.registrarAlertaEnBD(c, destinatarios, 'ENVIADO', null);
        resultado.alertas_enviadas++;
      }
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      this.logger.error(`Fallo enviando alerta legajo: ${msg}`);
      for (const c of items) {
        await this.registrarAlertaEnBD(c, destinatarios, 'ERROR', msg);
        resultado.alertas_con_error++;
        resultado.errores.push({ documento_id: c.documento_id, mensaje: msg });
      }
    }
  }

  private async registrarAlertaEnBD(
    candidata: AlertaCandidata,
    destinatarios: string[],
    estado: 'ENVIADO' | 'ERROR',
    errorDetalle: string | null,
  ) {
    try {
      await this.prisma.rrhh_legajo_alertas_vencimiento.create({
        data: {
          empresa_id: candidata.empresa_id,
          documento_id: candidata.documento_id,
          empleado_id: candidata.empleado_id,
          dias_para_vencer: candidata.dias_para_vencer,
          canal: 'EMAIL' as any,
          destinatarios: destinatarios as unknown as Prisma.InputJsonValue,
          estado_envio: estado as any,
          error_detalle: errorDetalle,
        },
      });
    } catch (err) {
      // UNIQUE constraint (documento_id, dias_para_vencer, canal) — ya se registró antes.
      // Idempotencia esperada; lo dejamos pasar silenciosamente.
      const code = (err as { code?: string })?.code;
      if (code !== 'P2002') {
        this.logger.warn(`Error registrando alerta: ${err instanceof Error ? err.message : err}`);
      }
    }
  }

  /**
   * Resuelve los destinatarios del email:
   *  1) email del empleado, si existe.
   *  2) emails de usuarios is_superadmin=true activos de la misma empresa
   *     (failback razonable hasta tener una configuración explícita de "responsable de RRHH").
   * Filtra duplicados y emails vacíos.
   */
  private async resolverDestinatarios(
    empresaId: string,
    empleadoEmail: string | null,
  ): Promise<string[]> {
    const set = new Set<string>();
    if (empleadoEmail) set.add(empleadoEmail);

    const admins = await this.prisma.usuario.findMany({
      where: {
        empresa_id: empresaId,
        active: true,
        is_superadmin: true,
        email: { not: null },
      },
      select: { email: true },
    });
    for (const u of admins) {
      if (u.email) set.add(u.email);
    }

    return Array.from(set).filter(Boolean);
  }

  // ──────────────────────────────────────────────────────────────────────

  private calcularEstado(
    fechaVenc: Date,
    inicioHoy: Date,
    diasAlerta: number,
  ): LegajoVencimientoEstado {
    if (!fechaVenc) return LegajoVencimientoEstado.SIN_VENCIMIENTO;
    if (fechaVenc.getTime() <= inicioHoy.getTime()) return LegajoVencimientoEstado.VENCIDO;
    const umbral = new Date(inicioHoy);
    umbral.setDate(umbral.getDate() + diasAlerta);
    return fechaVenc.getTime() <= umbral.getTime()
      ? LegajoVencimientoEstado.POR_VENCER
      : LegajoVencimientoEstado.VIGENTE;
  }

  private diferenciaDias(desde: Date, hasta: Date): number {
    const ms = hasta.getTime() - desde.getTime();
    return Math.floor(ms / (1000 * 60 * 60 * 24));
  }

  private inicioDeHoy(): Date {
    const n = new Date();
    return new Date(n.getFullYear(), n.getMonth(), n.getDate());
  }

  private parseDiasAnticipacion(raw: string): number[] {
    return raw
      .split(',')
      .map((s) => s.trim())
      .filter(Boolean)
      .map((s) => Number(s))
      .filter((n) => Number.isFinite(n) && n >= 0);
  }
}
