import {
  BadRequestException,
  ConflictException,
  Injectable,
  Logger,
  NotFoundException,
  Optional,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import * as XLSX from 'xlsx';
import { PrismaService } from '../../prisma/prisma.service';
import { ContabilidadIntegracionService } from '../../contabilidad/services/integracion.service';
import { MapeoCuentasService } from '../../contabilidad/services/mapeo-cuentas.service';
import {
  ActualizarDetalleIpsDto,
  AnularReporteIpsDto,
  GenerarReporteIpsDto,
  MarcarDeclaradoDto,
  MarcarPagadoDto,
  QueryReportesIpsDto,
} from '../dto/reportes-ips.dto';

const ORIGEN_MOVIMIENTO_TESORERIA = 'rrhh_reporte_ips';

const r2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
const num = (v: unknown, fb = 0) => {
  const n = Number(v);
  return Number.isFinite(n) ? n : fb;
};

@Injectable()
export class ReportesIpsService {
  private readonly logger = new Logger(ReportesIpsService.name);
  constructor(
    private readonly prisma: PrismaService,
    @Optional() private readonly contabilidad?: ContabilidadIntegracionService,
    @Optional() private readonly mapeo?: MapeoCuentasService,
  ) {}

  private async tieneModuloTesoreria(empresaId: string): Promise<boolean> {
    const registro = await this.prisma.suscripcion_modulos.findFirst({
      where: {
        activo: true,
        modulos: { codigo: 'TESORERIA' },
        suscripciones: { empresa_id: empresaId },
      },
    });
    return !!registro;
  }

  /**
   * Registra el egreso de tesorería del pago de IPS (idempotente por origen). El asiento lo
   * genera la integración de tesorería → contabilidad: DEBE "IPS a depositar" (cancela el pasivo
   * que creó la liquidación) / HABER Banco.
   */
  private async registrarMovimientoTesoreria(
    rep: { id: string; empresa_id: string; cuenta_tesoreria_id: string | null; total_a_depositar: Prisma.Decimal },
    userId?: string,
    fechaPago?: Date,
  ): Promise<void> {
    const empresaId = rep.empresa_id;
    const cuentaId = rep.cuenta_tesoreria_id;
    if (!cuentaId) {
      throw new BadRequestException(
        'Seleccioná la cuenta de tesorería desde la que se paga el IPS para marcarlo como pagado.',
      );
    }
    const cuenta = await this.prisma.tes_cuentas.findFirst({
      where: { id: cuentaId, empresa_id: empresaId, activo: true },
      select: { id: true, moneda: true },
    });
    if (!cuenta) throw new BadRequestException('Cuenta de tesorería inválida o inactiva.');
    if (cuenta.moneda !== 'PYG') {
      throw new BadRequestException(
        `El IPS se paga en PYG. Seleccioná una cuenta de tesorería en PYG (la elegida está en ${cuenta.moneda}).`,
      );
    }

    const existente = await this.prisma.tes_movimientos.findFirst({
      where: { empresa_id: empresaId, origen_tipo: ORIGEN_MOVIMIENTO_TESORERIA, origen_id: rep.id },
    });
    if (existente) return;

    const categoria = await this.prisma.tes_categorias.findFirst({
      where: { codigo_interno: 'PAGO_IPS', is_sistema: true },
    });
    if (!categoria) {
      throw new BadRequestException(
        'Falta la categoría de tesorería "Pago de IPS". Ejecutá las migraciones de tesorería.',
      );
    }

    let cuentaContableId: string | null = null;
    try {
      cuentaContableId = (await this.mapeo?.getCuentaPorConcepto(empresaId, 'IPS_A_DEPOSITAR')) ?? null;
    } catch {
      cuentaContableId = categoria.cuenta_contable_id ?? null;
    }

    const monto = r2(num(rep.total_a_depositar));
    if (monto <= 0) return;

    const movimiento = await this.prisma.$transaction(async (tx) => {
      const mov = await tx.tes_movimientos.create({
        data: {
          empresa_id: empresaId,
          cuenta_id: cuentaId,
          tipo: 'EGRESO',
          estado: 'CONFIRMADO',
          // Fecha de valor = la del pago declarado, no la de carga: si no, un pago de
          // un período anterior cae en el mes equivocado del extracto de Tesorería.
          fecha: fechaPago ?? new Date(),
          monto,
          descripcion: 'Pago de IPS (aportes obrero + patronal + admin)',
          origen_tipo: ORIGEN_MOVIMIENTO_TESORERIA,
          origen_id: rep.id,
          confirmado_por: userId ?? null,
          confirmado_en: new Date(),
          created_by: userId ?? null,
          detalles: {
            create: [
              { categoria_id: categoria.id, monto, descripcion: 'Pago de IPS', cuenta_contable_id: cuentaContableId },
            ],
          },
        },
      });
      await tx.tes_cuentas.update({
        where: { id: cuentaId },
        data: { saldo_actual: { increment: -monto } },
      });
      return mov;
    });

    this.contabilidad?.integrarMovimientoTesoreria(movimiento.id).catch((e) =>
      this.logger.error(`Error al contabilizar pago de IPS ${movimiento.id}: ${(e as Error).message}`),
    );
  }

  // ── Listado / detalle ──────────────────────────────────
  async listar(empresaId: string, query: QueryReportesIpsDto) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;

    const where: Prisma.rrhh_reportes_ipsWhereInput = { empresa_id: empresaId };
    if (query.estado) where.estado = query.estado;
    if (query.periodo_anio) where.periodo_anio = query.periodo_anio;
    if (query.periodo_mes) where.periodo_mes = query.periodo_mes;

    const [data, total] = await this.prisma.$transaction([
      this.prisma.rrhh_reportes_ips.findMany({
        where,
        skip,
        take: limit,
        orderBy: [{ periodo_anio: 'desc' }, { periodo_mes: 'desc' }, { created_at: 'desc' }],
        include: { liquidacion: { select: { id: true, tipo: true, estado: true } } },
      }),
      this.prisma.rrhh_reportes_ips.count({ where }),
    ]);
    return { data, total, page, limit };
  }

  async obtener(id: string, empresaId: string) {
    const rep = await this.prisma.rrhh_reportes_ips.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        liquidacion: { select: { id: true, tipo: true, estado: true, fecha_cierre: true } },
        detalles: { orderBy: { apellidos: 'asc' } },
      },
    });
    if (!rep) throw new NotFoundException('Reporte IPS no encontrado');
    return rep;
  }

  // ── Generar desde liquidación CERRADA ──────────────────
  async generar(empresaId: string, dto: GenerarReporteIpsDto, userId?: string) {
    const liq = await this.prisma.rrhh_liquidaciones_cabecera.findFirst({
      where: { id: dto.liquidacion_id, empresa_id: empresaId },
    });
    if (!liq) throw new NotFoundException('Liquidación no encontrada');
    if (liq.estado !== 'CERRADA') {
      throw new ConflictException(
        `El reporte IPS solo puede generarse desde una liquidación CERRADA (actual: ${liq.estado}).`,
      );
    }
    if (liq.tipo !== 'MENSUAL') {
      throw new ConflictException(
        'El reporte IPS solo se genera desde la liquidación MENSUAL del período.',
      );
    }

    // Reporte previo no anulado: bloquea
    const previo = await this.prisma.rrhh_reportes_ips.findFirst({
      where: { liquidacion_id: liq.id, estado: { not: 'ANULADO' } },
    });
    if (previo) {
      throw new ConflictException(
        `Ya existe un reporte IPS para esta liquidación (estado ${previo.estado}). Anulalo primero si querés regenerar.`,
      );
    }

    // Tomamos los resúmenes con aporte_obrero > 0 (los demás no son aportantes)
    const resumenes = await this.prisma.rrhh_liq_empleado_resumen.findMany({
      where: { liquidacion_id: liq.id, ips_obrero: { gt: 0 } },
      include: {
        empleado: {
          select: {
            id: true,
            numero_ips: true,
            cedula_identidad: true,
            nombres: true,
            apellidos: true,
            regimen_ips: true,
          },
        },
      },
      orderBy: { empleado: { apellidos: 'asc' } },
    });

    if (resumenes.length === 0) {
      throw new BadRequestException(
        'La liquidación no tiene empleados aportantes a IPS. Verificá que aporta_ips = true en los empleados.',
      );
    }

    const totales = resumenes.reduce(
      (acc, r) => {
        const base = num(r.base_ips);
        const obrero = num(r.ips_obrero);
        const patronal = num(r.ips_patronal);
        const admin = num(r.ips_admin);
        acc.base_aportable = r2(acc.base_aportable + base);
        acc.aporte_obrero = r2(acc.aporte_obrero + obrero);
        acc.aporte_patronal = r2(acc.aporte_patronal + patronal);
        acc.aporte_admin = r2(acc.aporte_admin + admin);
        return acc;
      },
      { base_aportable: 0, aporte_obrero: 0, aporte_patronal: 0, aporte_admin: 0 },
    );
    const totalDepositar = r2(totales.aporte_obrero + totales.aporte_patronal + totales.aporte_admin);

    return this.prisma.$transaction(async (tx) => {
      const rep = await tx.rrhh_reportes_ips.create({
        data: {
          empresa_id: empresaId,
          liquidacion_id: liq.id,
          periodo_anio: liq.periodo_anio,
          periodo_mes: liq.periodo_mes,
          total_empleados: resumenes.length,
          total_base_aportable: totales.base_aportable,
          total_aporte_obrero: totales.aporte_obrero,
          total_aporte_patronal: totales.aporte_patronal,
          total_aporte_admin: totales.aporte_admin,
          total_a_depositar: totalDepositar,
          estado: 'GENERADO',
          observaciones: dto.observaciones,
          generado_por: userId,
        },
      });

      await tx.rrhh_reportes_ips_det.createMany({
        data: resumenes.map((r) => {
          const obrero = r2(num(r.ips_obrero));
          const patronal = r2(num(r.ips_patronal));
          const admin = r2(num(r.ips_admin));
          return {
            reporte_id: rep.id,
            empleado_id: r.empleado_id,
            numero_ips: r.empleado?.numero_ips ?? null,
            cedula: r.empleado?.cedula_identidad ?? '',
            nombres: r.empleado?.nombres ?? '',
            apellidos: r.empleado?.apellidos ?? '',
            base_aportable: r2(num(r.base_ips)),
            aporte_obrero: obrero,
            aporte_patronal: patronal,
            aporte_admin: admin,
            total_aporte: r2(obrero + patronal + admin),
            dias_trabajados: r.dias_trabajados,
            regimen_ips: r.empleado?.regimen_ips ?? null,
          };
        }),
      });

      return rep;
    });
  }

  // ── Transiciones de estado ─────────────────────────────
  async marcarDeclarado(id: string, empresaId: string, dto: MarcarDeclaradoDto) {
    const rep = await this.prisma.rrhh_reportes_ips.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!rep) throw new NotFoundException('Reporte IPS no encontrado');
    if (rep.estado !== 'GENERADO') {
      throw new ConflictException(
        `Solo se puede declarar un reporte GENERADO (actual: ${rep.estado}).`,
      );
    }
    return this.prisma.rrhh_reportes_ips.update({
      where: { id },
      data: {
        estado: 'DECLARADO',
        fecha_declaracion: new Date(dto.fecha_declaracion),
        numero_planilla_ips: dto.numero_planilla_ips ?? rep.numero_planilla_ips,
        observaciones: dto.observaciones ?? rep.observaciones,
        updated_at: new Date(),
      },
    });
  }

  async marcarPagado(id: string, empresaId: string, dto: MarcarPagadoDto, userId?: string) {
    const rep = await this.prisma.rrhh_reportes_ips.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!rep) throw new NotFoundException('Reporte IPS no encontrado');
    if (rep.estado !== 'DECLARADO' && rep.estado !== 'GENERADO') {
      throw new ConflictException(
        `Solo se puede marcar como pagado un reporte GENERADO o DECLARADO (actual: ${rep.estado}).`,
      );
    }

    const cuentaTesoreriaId = dto.cuenta_tesoreria_id ?? rep.cuenta_tesoreria_id;

    // Si la empresa usa Tesorería, registrar el egreso ANTES de marcar PAGADO (idempotente).
    if (await this.tieneModuloTesoreria(empresaId)) {
      await this.registrarMovimientoTesoreria(
        { ...rep, cuenta_tesoreria_id: cuentaTesoreriaId },
        userId,
        new Date(dto.fecha_pago),
      );
    }

    return this.prisma.rrhh_reportes_ips.update({
      where: { id },
      data: {
        estado: 'PAGADO',
        fecha_pago: new Date(dto.fecha_pago),
        cuenta_tesoreria_id: cuentaTesoreriaId,
        comprobante_url: dto.comprobante_url ?? rep.comprobante_url,
        observaciones: dto.observaciones ?? rep.observaciones,
        updated_at: new Date(),
      },
    });
  }

  async anular(id: string, empresaId: string, dto: AnularReporteIpsDto, userId?: string) {
    const rep = await this.prisma.rrhh_reportes_ips.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!rep) throw new NotFoundException('Reporte IPS no encontrado');
    if (rep.estado === 'ANULADO') throw new ConflictException('El reporte ya está anulado');
    if (rep.estado === 'PAGADO') {
      throw new ConflictException(
        'Un reporte PAGADO no puede anularse directamente. Registralo manualmente con observación.',
      );
    }
    const obs = dto.comentario
      ? rep.observaciones
        ? `${rep.observaciones}\n[ANULADO] ${dto.comentario}`
        : `[ANULADO] ${dto.comentario}`
      : rep.observaciones;
    return this.prisma.rrhh_reportes_ips.update({
      where: { id },
      data: {
        estado: 'ANULADO',
        anulado_por: userId,
        fecha_anulacion: new Date(),
        observaciones: obs,
        updated_at: new Date(),
      },
    });
  }

  // ── Edición de detalle ─────────────────────────────────
  async actualizarDetalle(
    reporteId: string,
    detalleId: string,
    dto: ActualizarDetalleIpsDto,
    empresaId: string,
  ) {
    const rep = await this.prisma.rrhh_reportes_ips.findFirst({
      where: { id: reporteId, empresa_id: empresaId },
    });
    if (!rep) throw new NotFoundException('Reporte IPS no encontrado');
    if (rep.estado !== 'GENERADO') {
      throw new ConflictException(
        'Solo se pueden editar los días de un reporte en estado GENERADO.',
      );
    }
    const det = await this.prisma.rrhh_reportes_ips_det.findFirst({
      where: { id: detalleId, reporte_id: reporteId },
    });
    if (!det) throw new NotFoundException('Fila de detalle no encontrada');

    return this.prisma.rrhh_reportes_ips_det.update({
      where: { id: detalleId },
      data: { dias_trabajados: dto.dias_trabajados },
    });
  }

  // ── Archivo REOP (Excel) ───────────────────────────────
  async generarArchivoReop(
    id: string,
    empresaId: string,
  ): Promise<{ buffer: Buffer; filename: string }> {
    const rep = await this.obtener(id, empresaId);
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { razon_social: true, ruc: true },
    });

    const wb = XLSX.utils.book_new();

    // Hoja 1: Cabecera (resumen del reporte)
    const cabRows: (string | number)[][] = [
      ['REPORTE IPS / REOP'],
      [],
      ['Empresa', empresa?.razon_social ?? '—'],
      ['RUC', empresa?.ruc ?? '—'],
      ['Período', `${String(rep.periodo_mes).padStart(2, '0')}/${rep.periodo_anio}`],
      ['Estado', rep.estado],
      ['Empleados aportantes', rep.total_empleados],
      [],
      ['Total base aportable', Number(rep.total_base_aportable)],
      ['Total aporte obrero (9%)', Number(rep.total_aporte_obrero)],
      ['Total aporte patronal (16.5%)', Number(rep.total_aporte_patronal)],
      ['Total aporte administrativo (1%)', Number(rep.total_aporte_admin)],
      ['TOTAL A DEPOSITAR', Number(rep.total_a_depositar)],
    ];
    const wsCab = XLSX.utils.aoa_to_sheet(cabRows);
    wsCab['!cols'] = [{ wch: 32 }, { wch: 28 }];
    XLSX.utils.book_append_sheet(wb, wsCab, 'Resumen');

    // Hoja 2: Detalle por empleado (formato REOP)
    const detHeader = [
      'N° IPS',
      'Cédula',
      'Apellidos',
      'Nombres',
      'Régimen IPS',
      'Días trabajados',
      'Base aportable',
      'Aporte obrero (9%)',
      'Aporte patronal (16.5%)',
      'Aporte administrativo (1%)',
      'Total aporte',
    ];
    const detRows = [
      detHeader,
      ...(rep.detalles ?? []).map((d) => [
        d.numero_ips || '',
        d.cedula,
        d.apellidos,
        d.nombres,
        d.regimen_ips || '',
        d.dias_trabajados ?? '',
        Number(d.base_aportable),
        Number(d.aporte_obrero),
        Number(d.aporte_patronal),
        Number(d.aporte_admin),
        Number(d.total_aporte),
      ]),
    ];
    const wsDet = XLSX.utils.aoa_to_sheet(detRows);
    wsDet['!cols'] = [
      { wch: 14 }, { wch: 14 }, { wch: 24 }, { wch: 24 }, { wch: 18 },
      { wch: 14 }, { wch: 18 }, { wch: 18 }, { wch: 20 }, { wch: 22 }, { wch: 18 },
    ];
    XLSX.utils.book_append_sheet(wb, wsDet, 'Detalle empleados');

    const buffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
    const filename = `reop_ips_${rep.periodo_anio}-${String(rep.periodo_mes).padStart(2, '0')}.xlsx`;
    return { buffer, filename };
  }
}
