import {
  BadRequestException,
  ConflictException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { ContabilidadIntegracionService } from '../../contabilidad/services/integracion.service';
import { MapeoCuentasService } from '../../contabilidad/services/mapeo-cuentas.service';
import {
  AnularAcreditacionDto,
  GenerarAcreditacionDto,
  MarcarConfirmadoDto,
  MarcarEnviadoDto,
  QueryAcreditacionesDto,
} from '../dto/acreditaciones-bancarias.dto';
import { renderTemplate, unescapeSeparator } from '../engine/template-renderer';

const ORIGEN_MOVIMIENTO_TESORERIA = 'rrhh_acreditacion';

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 AcreditacionesBancariasService {
  private readonly logger = new Logger(AcreditacionesBancariasService.name);
  constructor(
    private readonly prisma: PrismaService,
    private readonly contabilidad: ContabilidadIntegracionService,
    private readonly mapeo: MapeoCuentasService,
  ) {}

  private async validarCuentaPagoPyg(empresaId: string, cuentaId: string) {
    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(
        `Las acreditaciones de sueldos se generan en PYG. Seleccioná una cuenta de tesorería en PYG para confirmar el pago (la cuenta elegida está en ${cuenta.moneda}).`,
      );
    }
    return cuenta;
  }

  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 sueldos (idempotente por origen).
   * El asiento contable lo genera la propia integración de tesorería al confirmar el
   * movimiento (DEBE Sueldos a pagar / HABER Banco), si la empresa tiene Contabilidad.
   */
  private async registrarMovimientoTesoreria(
    acreditacion: { id: string; empresa_id: string; monto_total: Prisma.Decimal; cuenta_tesoreria_id: string | null },
    userId?: string,
  ): Promise<void> {
    const empresaId = acreditacion.empresa_id;
    const cuentaId = acreditacion.cuenta_tesoreria_id;
    if (!cuentaId) {
      throw new BadRequestException(
        'Seleccioná la cuenta de tesorería desde la que se paga para confirmar la acreditación.',
      );
    }

    await this.validarCuentaPagoPyg(empresaId, cuentaId);

    // Idempotencia: no duplicar el movimiento si ya existe para esta acreditación.
    const existente = await this.prisma.tes_movimientos.findFirst({
      where: { empresa_id: empresaId, origen_tipo: ORIGEN_MOVIMIENTO_TESORERIA, origen_id: acreditacion.id },
    });
    if (existente) return;

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

    // Cuenta contable para debitar el pasivo (opcional: si no hay mapeo, el asiento se omite/loguea).
    let cuentaContableId: string | null = null;
    try {
      cuentaContableId = await this.mapeo.getCuentaPorConcepto(empresaId, 'SUELDOS_A_PAGAR');
    } catch {
      cuentaContableId = categoria.cuenta_contable_id ?? null;
    }

    const monto = Number(acreditacion.monto_total);
    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: new Date(),
          monto,
          descripcion: 'Pago de sueldos (acreditación bancaria)',
          origen_tipo: ORIGEN_MOVIMIENTO_TESORERIA,
          origen_id: acreditacion.id,
          confirmado_por: userId ?? null,
          confirmado_en: new Date(),
          created_by: userId ?? null,
          detalles: {
            create: [
              {
                categoria_id: categoria.id,
                monto,
                descripcion: 'Pago de sueldos',
                cuenta_contable_id: cuentaContableId,
              },
            ],
          },
        },
      });
      // EGRESO → baja el saldo de la cuenta.
      await tx.tes_cuentas.update({
        where: { id: cuentaId },
        data: { saldo_actual: { increment: -monto } },
      });
      return mov;
    });

    // Integración contable (self-gated por módulo CONTABILIDAD; fire-and-forget).
    this.contabilidad.integrarMovimientoTesoreria(movimiento.id).catch((e) =>
      this.logger.error(`Error al contabilizar movimiento de pago de sueldos ${movimiento.id}: ${(e as Error).message}`),
    );
  }

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

    const where: Prisma.rrhh_acreditaciones_bancariasWhereInput = { empresa_id: empresaId };
    if (query.estado) where.estado = query.estado;
    if (query.liquidacion_id) where.liquidacion_id = query.liquidacion_id;
    if (query.banco_id) where.banco_id = query.banco_id;

    const [data, total] = await this.prisma.$transaction([
      this.prisma.rrhh_acreditaciones_bancarias.findMany({
        where,
        skip,
        take: limit,
        orderBy: [{ fecha_generacion: 'desc' }],
        include: {
          liquidacion: {
            select: { id: true, periodo_anio: true, periodo_mes: true, tipo: true, quincena: true },
          },
          banco: { select: { id: true, codigo: true, descripcion: true } },
          cuenta_tesoreria: { select: { id: true, nombre: true, moneda: true } },
          formato: { select: { id: true, nombre: true, tipo_archivo: true } },
        },
      }),
      this.prisma.rrhh_acreditaciones_bancarias.count({ where }),
    ]);
    return { data, total, page, limit };
  }

  async obtener(id: string, empresaId: string) {
    const a = await this.prisma.rrhh_acreditaciones_bancarias.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        liquidacion: {
          select: { id: true, periodo_anio: true, periodo_mes: true, tipo: true, quincena: true, estado: true },
        },
        banco: { select: { id: true, codigo: true, descripcion: true } },
        cuenta_tesoreria: { select: { id: true, nombre: true, moneda: true } },
        formato: { select: { id: true, nombre: true, tipo_archivo: true, encoding: true } },
      },
    });
    if (!a) throw new NotFoundException('Acreditación bancaria no encontrada');
    return a;
  }

  // ── Generación ─────────────────────────────────────────
  async generar(empresaId: string, dto: GenerarAcreditacionDto, 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(
        `La acreditación bancaria sólo puede generarse desde una liquidación CERRADA (actual: ${liq.estado}).`,
      );
    }

    const banco = await this.prisma.bancos.findFirst({
      where: { id: dto.banco_id, OR: [{ empresa_id: empresaId }, { empresa_id: null }] },
    });
    if (!banco) throw new BadRequestException('Banco inválido para esta empresa');

    const confirmadaExistente = await this.prisma.rrhh_acreditaciones_bancarias.findFirst({
      where: {
        empresa_id: empresaId,
        liquidacion_id: liq.id,
        banco_id: banco.id,
        estado: 'CONFIRMADO',
      },
      select: { id: true, nombre_archivo: true },
    });
    if (confirmadaExistente) {
      throw new ConflictException(
        `Esta liquidación ya tiene una acreditación CONFIRMADA para ${banco.descripcion}. No se puede generar otro archivo para el mismo banco.`,
      );
    }

    if (dto.cuenta_tesoreria_id) {
      await this.validarCuentaPagoPyg(empresaId, dto.cuenta_tesoreria_id);
    }

    // Resolver formato: el especificado, o el primero activo del banco para la empresa.
    const formato = dto.formato_id
      ? await this.prisma.rrhh_formatos_acreditacion_bancaria.findFirst({
          where: { id: dto.formato_id, empresa_id: empresaId, banco_id: dto.banco_id },
        })
      : await this.prisma.rrhh_formatos_acreditacion_bancaria.findFirst({
          where: { empresa_id: empresaId, banco_id: dto.banco_id, activo: true },
          orderBy: { updated_at: 'desc' },
        });
    if (!formato) {
      throw new BadRequestException(
        'No hay formato bancario activo para este banco. Configurá uno en RRHH › Configuración › Formatos bancarios.',
      );
    }

    // Empleados con cuenta en este banco + neto > 0 en la liquidación.
    const resumenes = await this.prisma.rrhh_liq_empleado_resumen.findMany({
      where: {
        liquidacion_id: liq.id,
        neto_a_pagar: { gt: 0 },
        empleado: { banco_id: dto.banco_id, cuenta_bancaria: { not: null } },
      },
      include: {
        empleado: {
          select: {
            id: true,
            numero_empleado: true,
            cedula_identidad: true,
            ruc: true,
            nombres: true,
            apellidos: true,
            cuenta_bancaria: true,
            tipo_cuenta: true,
            numero_ips: true,
          },
        },
      },
      orderBy: { empleado: { apellidos: 'asc' } },
    });

    if (resumenes.length === 0) {
      throw new BadRequestException(
        `Ningún empleado con cuenta en ${banco.descripcion} y neto a pagar > 0 en esta liquidación.`,
      );
    }

    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { razon_social: true, ruc: true },
    });

    const totalMonto = r2(resumenes.reduce((acc, r) => acc + num(r.neto_a_pagar), 0));
    const cantidadRegistros = resumenes.length;
    const fechaGen = new Date();
    const fechaPago = dto.fecha_pago_programada
      ? new Date(dto.fecha_pago_programada)
      : (liq.fecha_pago_programada ?? fechaGen);

    const ctxHeaderFooter = {
      empresa_razon_social: empresa?.razon_social ?? '',
      empresa_ruc: empresa?.ruc ?? '',
      banco_codigo: banco.codigo ?? '',
      banco_descripcion: banco.descripcion ?? '',
      fecha_generacion: fechaGen.toISOString().slice(0, 10),
      fecha_pago: fechaPago.toISOString().slice(0, 10),
      total_registros: cantidadRegistros,
      monto_total: totalMonto.toFixed(2),
      monto_total_centavos: Math.round(totalMonto * 100),
      periodo_anio: liq.periodo_anio,
      periodo_mes: String(liq.periodo_mes).padStart(2, '0'),
    };

    const lineas: string[] = [];
    if (formato.tiene_cabecera && formato.template_cabecera) {
      lineas.push(renderTemplate(formato.template_cabecera, ctxHeaderFooter));
    }
    resumenes.forEach((r, idx) => {
      const emp = r.empleado;
      const monto = num(r.neto_a_pagar);
      const ctx = {
        ...ctxHeaderFooter,
        indice: idx + 1,
        cedula: emp?.cedula_identidad ?? '',
        ruc: emp?.ruc ?? '',
        numero_empleado: emp?.numero_empleado ?? '',
        numero_ips: emp?.numero_ips ?? '',
        nombres: emp?.nombres ?? '',
        apellidos: emp?.apellidos ?? '',
        nombre_completo: `${emp?.apellidos ?? ''} ${emp?.nombres ?? ''}`.trim(),
        cuenta_bancaria: emp?.cuenta_bancaria ?? '',
        tipo_cuenta: emp?.tipo_cuenta ?? '',
        monto: monto.toFixed(2),
        monto_centavos: Math.round(monto * 100),
      };
      lineas.push(renderTemplate(formato.template_detalle, ctx));
    });
    if (formato.tiene_pie && formato.template_pie) {
      lineas.push(renderTemplate(formato.template_pie, ctxHeaderFooter));
    }

    const sep = unescapeSeparator(formato.separador_lineas);
    const contenido = lineas.join(sep);

    const bancoSlug = (banco.codigo || banco.descripcion || 'banco')
      .toString()
      .replace(/[^a-z0-9]+/gi, '')
      .toLowerCase()
      .slice(0, 12);
    const nombreArchivo = `acred_${bancoSlug}_${liq.periodo_anio}-${String(liq.periodo_mes).padStart(2, '0')}${liq.tipo === 'QUINCENAL' ? `_Q${liq.quincena}` : ''}.${formato.extension_archivo || 'txt'}`;

    return this.prisma.rrhh_acreditaciones_bancarias.create({
      data: {
        empresa_id: empresaId,
        liquidacion_id: liq.id,
        banco_id: banco.id,
        cuenta_tesoreria_id: dto.cuenta_tesoreria_id ?? null,
        formato_id: formato.id,
        fecha_generacion: fechaGen,
        fecha_pago_programada: fechaPago,
        cantidad_registros: cantidadRegistros,
        monto_total: totalMonto,
        nombre_archivo: nombreArchivo,
        contenido_archivo: contenido,
        encoding: formato.encoding,
        estado: 'GENERADO',
        observaciones: dto.observaciones,
        generado_por: userId,
      },
    });
  }

  async descargarArchivo(id: string, empresaId: string) {
    const a = await this.prisma.rrhh_acreditaciones_bancarias.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!a) throw new NotFoundException('Acreditación no encontrada');
    return {
      nombre: a.nombre_archivo,
      contenido: a.contenido_archivo,
      encoding: a.encoding,
    };
  }

  // ── Workflow ───────────────────────────────────────────
  async marcarEnviado(id: string, empresaId: string, dto: MarcarEnviadoDto) {
    const a = await this.prisma.rrhh_acreditaciones_bancarias.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!a) throw new NotFoundException('Acreditación no encontrada');
    if (a.estado !== 'GENERADO') {
      throw new ConflictException(`Sólo se puede marcar como ENVIADO una acreditación GENERADA (actual: ${a.estado}).`);
    }
    return this.prisma.rrhh_acreditaciones_bancarias.update({
      where: { id },
      data: {
        estado: 'ENVIADO',
        fecha_envio: new Date(),
        observaciones: dto.observaciones ?? a.observaciones,
        updated_at: new Date(),
      },
    });
  }

  async marcarConfirmado(id: string, empresaId: string, dto: MarcarConfirmadoDto, userId?: string) {
    const a = await this.prisma.rrhh_acreditaciones_bancarias.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!a) throw new NotFoundException('Acreditación no encontrada');
    if (a.estado !== 'ENVIADO' && a.estado !== 'GENERADO') {
      throw new ConflictException(
        `Sólo se puede confirmar una acreditación GENERADA o ENVIADA (actual: ${a.estado}).`,
      );
    }

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

    // Si la empresa usa Tesorería, registrar el egreso ANTES de marcar confirmado, para que un
    // fallo (cuenta faltante, etc.) no deje la acreditación confirmada sin movimiento. El registro
    // es idempotente, así que un reintento no duplica.
    if (await this.tieneModuloTesoreria(empresaId)) {
      await this.registrarMovimientoTesoreria(
        { id: a.id, empresa_id: empresaId, monto_total: a.monto_total, cuenta_tesoreria_id: cuentaTesoreriaId },
        userId,
      );
    }

    return this.prisma.rrhh_acreditaciones_bancarias.update({
      where: { id },
      data: {
        estado: 'CONFIRMADO',
        cuenta_tesoreria_id: cuentaTesoreriaId,
        fecha_confirmacion: new Date(),
        observaciones: dto.observaciones ?? a.observaciones,
        updated_at: new Date(),
      },
    });
  }

  async anular(id: string, empresaId: string, dto: AnularAcreditacionDto, userId?: string) {
    const a = await this.prisma.rrhh_acreditaciones_bancarias.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!a) throw new NotFoundException('Acreditación no encontrada');
    if (a.estado === 'ANULADO') throw new ConflictException('La acreditación ya está anulada');
    if (a.estado === 'CONFIRMADO') {
      throw new ConflictException(
        'Una acreditación CONFIRMADA no se puede anular automáticamente. Registralo manualmente.',
      );
    }
    return this.prisma.rrhh_acreditaciones_bancarias.update({
      where: { id },
      data: {
        estado: 'ANULADO',
        anulado_por: userId,
        fecha_anulacion: new Date(),
        observaciones: dto.comentario
          ? a.observaciones
            ? `${a.observaciones}\n[ANULADO] ${dto.comentario}`
            : `[ANULADO] ${dto.comentario}`
          : a.observaciones,
        updated_at: new Date(),
      },
    });
  }
}
