import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { AuditService } from 'src/audit/audit.service';
import { AsientosService } from 'src/contabilidad/services/asientos.service';
import { MapeoCuentasService } from 'src/contabilidad/services/mapeo-cuentas.service';
import { PeriodosService } from 'src/contabilidad/services/periodos.service';
import { PrismaService } from 'src/prisma/prisma.service';

/**
 * FASE 1 — Integración contable de la rendición de viáticos.
 * Genera asientos para adelantos (Fase 1).
 * Gastos, devoluciones y reintegros se manejan en Fase 2 y 3.
 *
 * Ver docs/plan-rendicion-viaticos.md — sección "Impacto contable".
 */
@Injectable()
export class RendicionViaticosContabilidadService {
  private readonly logger = new Logger(RendicionViaticosContabilidadService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly asientos: AsientosService,
    private readonly mapeo: MapeoCuentasService,
    private readonly periodos: PeriodosService,
    private readonly audit: AuditService,
  ) {}

  /**
   * Deja registro auditable cuando un asiento no se puede generar por falta
   * de configuración (mapeo faltante, período cerrado, cuenta OLD-, etc.).
   * Los operadores verán este evento en la vista de auditoría.
   */
  private async auditarFalloAsiento(
    empresaId: string,
    entityType: string,
    entityId: string,
    motivo: string,
    contexto?: Record<string, unknown>,
    userId?: string,
  ) {
    try {
      await this.audit.log({
        empresa_id: empresaId,
        user_id: userId,
        action: 'ERROR',
        entity_type: entityType,
        entity_id: entityId,
        descripcion: `Asiento contable no generado: ${motivo}`,
        new_value: { motivo, ...(contexto ?? {}) },
      });
    } catch (err) {
      this.logger.warn(
        `Fallo auditando error contable (${entityType}/${entityId}): ${err instanceof Error ? err.message : err}`,
      );
    }
  }

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

  /**
   * Al pagar un adelanto:
   *   DEBE  Anticipo para Viáticos (concepto ANTICIPO_VIATICO)   monto
   *   HABER cuenta contable del medio de pago (caja/banco)       monto
   */
  async registrarAdelanto(adelantoId: string, empresaId: string, usuarioId?: string) {
    if (!(await this.tieneModuloContabilidad(empresaId))) return null;

    const adelanto = await this.prisma.rendicion_adelanto.findFirst({
      where: { id: adelantoId, empresa_id: empresaId, anulado: false },
      include: {
        rendicion: {
          select: { numero_rendicion: true, empleado_id: true, empleado: { select: { nombres: true, apellidos: true } } },
        },
        tes_cuenta: { select: { id: true, cuenta_contable_id: true, nombre: true } },
      },
    });
    if (!adelanto) throw new BadRequestException('Adelanto no encontrado');
    if (adelanto.cont_documento_id) {
      this.logger.warn(`Adelanto ${adelantoId} ya tiene documento contable`);
      return null;
    }

    const numeroDoc = `AD-${adelanto.rendicion?.numero_rendicion ?? adelantoId.substring(0, 8)}`;
    const fecha = adelanto.fecha ?? new Date();
    const monto = Number(adelanto.monto);
    if (monto <= 0) return null;

    let periodoId: string;
    try {
      periodoId = await this.periodos.resolverPeriodoPorFecha(empresaId, fecha);
    } catch (err) {
      this.logger.warn(`No hay período abierto para adelanto ${adelantoId}: ${err instanceof Error ? err.message : err}`);
      return null;
    }

    // Cuenta DEBE: Anticipo para Viáticos (concepto ANTICIPO_VIATICO).
    let cuentaDebeId: string;
    try {
      cuentaDebeId = await this.mapeo.getCuentaPorConcepto(empresaId, 'ANTICIPO_VIATICO');
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      if (msg.includes('No hay cuenta mapeada')) {
        // Fallback al concepto legacy ANTICIPO_PERSONAL si existe.
        try {
          cuentaDebeId = await this.mapeo.getCuentaPorConcepto(empresaId, 'ANTICIPO_PERSONAL');
        } catch {
          this.logger.warn(
            `Falta mapear concepto ANTICIPO_VIATICO para empresa ${empresaId}. Asiento omitido.`,
          );
          return null;
        }
      } else {
        throw err;
      }
    }

    // Cuenta HABER: la cuenta contable de la cuenta de tesorería seleccionada,
    // o fallback a BANCO/CAJA_GENERAL del mapeo.
    // Si la cuenta apunta a una cuenta contable OLD- (plan reemplazado), forzamos
    // el fallback al mapeo para que el contador pueda controlarlo.
    let cuentaHaberId: string | null = adelanto.tes_cuenta?.cuenta_contable_id ?? null;
    if (cuentaHaberId) {
      const c = await this.prisma.cont_plan_cuentas.findUnique({
        where: { id: cuentaHaberId },
        select: { codigo: true, active: true },
      });
      if (!c || !c.active || (c.codigo && c.codigo.startsWith('OLD-'))) {
        this.logger.warn(
          `Cuenta contable de tes_cuenta ${adelanto.tes_cuenta?.id} está OLD-/inactiva, cayendo al mapeo BANCO/CAJA_GENERAL`,
        );
        cuentaHaberId = null;
      }
    }
    if (!cuentaHaberId) {
      try {
        cuentaHaberId =
          (await this.mapeo.getCuentaPorConcepto(empresaId, 'BANCO')) ??
          (await this.mapeo.getCuentaPorConcepto(empresaId, 'CAJA_GENERAL'));
      } catch {
        this.logger.warn('Sin cuenta contable HABER para adelanto, asiento omitido');
        return null;
      }
      if (!cuentaHaberId) {
        this.logger.warn('Sin cuenta contable HABER para adelanto, asiento omitido');
        return null;
      }
    }

    const empleadoNombre = adelanto.rendicion?.empleado
      ? `${adelanto.rendicion.empleado.nombres} ${adelanto.rendicion.empleado.apellidos}`
      : 'empleado';

    const doc = await this.prisma.cont_documentos.create({
      data: {
        empresa_id: empresaId,
        tipo: 'PAGO',
        origen_tipo: 'rendicion_adelanto',
        origen_id: adelantoId,
        numero_documento: numeroDoc,
        estado: 'CONFIRMADO',
      },
    });

    await this.asientos.crearConfirmado(empresaId, usuarioId ?? empresaId, doc.id, {
      periodo_id: periodoId,
      fecha: fecha.toISOString().substring(0, 10),
      glosa: `Anticipo viáticos ${adelanto.rendicion?.numero_rendicion ?? ''} - ${empleadoNombre}`,
      moneda_origen: 'PYG',
      lineas: [
        {
          cuenta_id: cuentaDebeId,
          debe_moneda: monto,
          haber_moneda: 0,
          descripcion: `Adelanto a ${empleadoNombre}`,
        },
        {
          cuenta_id: cuentaHaberId,
          debe_moneda: 0,
          haber_moneda: monto,
          descripcion: adelanto.tes_cuenta?.nombre ?? 'Caja/Banco',
        },
      ],
    });

    this.logger.log(`Adelanto ${adelantoId} contabilizado (doc ${doc.id})`);
    return doc;
  }

  /**
   * Reversa el asiento de un adelanto (al anular la rendición).
   */
  async revertirAdelanto(adelantoId: string, empresaId: string, usuarioId?: string) {
    if (!(await this.tieneModuloContabilidad(empresaId))) return;
    const doc = await this.prisma.cont_documentos.findFirst({
      where: { origen_tipo: 'rendicion_adelanto', origen_id: adelantoId, estado: 'CONFIRMADO' },
      include: { asiento: true },
    });
    if (!doc || !doc.asiento) return;
    await this.asientos.revertir(doc.asiento.id, empresaId, usuarioId ?? null);
    this.logger.log(`Reversión contable de adelanto ${adelantoId}`);
  }

  /**
   * FASE 3 — Cierre de rendición.
   *
   * Caso DEVOLUCION (funcionario devuelve sobrante):
   *   DEBE   cuenta_tesoreria destino (Caja/Banco)         monto
   *   HABER  Anticipo para Viáticos                         monto
   *
   * Caso REINTEGRO INMEDIATO (empresa paga faltante):
   *   DEBE   Anticipo para Viáticos                         monto
   *   HABER  cuenta_tesoreria origen (Caja/Banco)           monto
   *
   * Caso REINTEGRO DIFERIDO (queda como CxP al empleado):
   *   DEBE   Anticipo para Viáticos                         monto
   *   HABER  Reintegros a Empleados Pendientes              monto
   */
  async registrarDevolucion(devolucionId: string, empresaId: string, usuarioId?: string) {
    if (!(await this.tieneModuloContabilidad(empresaId))) return null;

    const dev = await this.prisma.rendicion_devolucion.findFirst({
      where: { id: devolucionId, empresa_id: empresaId, anulado: false },
      include: {
        rendicion: {
          select: { numero_rendicion: true, empleado: { select: { nombres: true, apellidos: true } } },
        },
        tes_cuenta: { select: { id: true, cuenta_contable_id: true, nombre: true } },
      },
    });
    if (!dev) throw new BadRequestException('Devolución no encontrada');
    if (dev.cont_documento_id) {
      this.logger.warn(`Devolución ${devolucionId} ya tiene documento contable`);
      return null;
    }

    const numeroDoc = `CIE-${dev.rendicion?.numero_rendicion ?? devolucionId.substring(0, 8)}`;
    const fecha = dev.fecha ?? new Date();
    const monto = Number(dev.monto);
    if (monto <= 0) return null;

    let periodoId: string;
    try {
      periodoId = await this.periodos.resolverPeriodoPorFecha(empresaId, fecha);
    } catch (err) {
      this.logger.warn(
        `No hay período abierto para devolución ${devolucionId}: ${err instanceof Error ? err.message : err}`,
      );
      return null;
    }

    let cuentaAnticipo: string;
    try {
      cuentaAnticipo = await this.mapeo.getCuentaPorConcepto(empresaId, 'ANTICIPO_VIATICO');
    } catch {
      try {
        cuentaAnticipo = await this.mapeo.getCuentaPorConcepto(empresaId, 'ANTICIPO_PERSONAL');
      } catch {
        this.logger.warn(`Falta concepto ANTICIPO_VIATICO/ANTICIPO_PERSONAL para empresa ${empresaId}`);
        return null;
      }
    }

    const empleadoNombre = dev.rendicion?.empleado
      ? `${dev.rendicion.empleado.nombres} ${dev.rendicion.empleado.apellidos}`
      : 'empleado';

    let lineas: Array<{ cuenta_id: string; debe_moneda: number; haber_moneda: number; descripcion: string }> = [];

    // Helper: resuelve la cuenta contable de tesorería, evitando cuentas OLD-/inactivas.
    const resolverCuentaTes = async (): Promise<string | null> => {
      let cid: string | null = dev.tes_cuenta?.cuenta_contable_id ?? null;
      if (cid) {
        const c = await this.prisma.cont_plan_cuentas.findUnique({
          where: { id: cid },
          select: { codigo: true, active: true },
        });
        if (!c || !c.active || (c.codigo && c.codigo.startsWith('OLD-'))) {
          this.logger.warn(
            `Cuenta contable de tes_cuenta ${dev.tes_cuenta?.id} está OLD-/inactiva, cayendo al mapeo BANCO/CAJA_GENERAL`,
          );
          cid = null;
        }
      }
      if (!cid) {
        try {
          cid =
            (await this.mapeo.getCuentaPorConcepto(empresaId, 'BANCO')) ??
            (await this.mapeo.getCuentaPorConcepto(empresaId, 'CAJA_GENERAL'));
        } catch {
          return null;
        }
      }
      return cid;
    };

    if (dev.tipo === 'DEVOLUCION') {
      // Funcionario devuelve → DEBE cuenta tesorería destino / HABER Anticipo.
      const cuentaTes = await resolverCuentaTes();
      if (!cuentaTes) {
        this.logger.warn('Sin cuenta contable destino para devolución, asiento omitido');
        return null;
      }
      lineas = [
        {
          cuenta_id: cuentaTes,
          debe_moneda: monto,
          haber_moneda: 0,
          descripcion: `Devolución sobrante viáticos - ${empleadoNombre}`,
        },
        {
          cuenta_id: cuentaAnticipo,
          debe_moneda: 0,
          haber_moneda: monto,
          descripcion: `Cancela anticipo ${dev.rendicion?.numero_rendicion ?? ''}`,
        },
      ];
    } else if (dev.tipo === 'REINTEGRO' && !dev.diferido) {
      // Empresa paga reintegro ahora → DEBE Anticipo / HABER cuenta tesorería.
      const cuentaTes = await resolverCuentaTes();
      if (!cuentaTes) {
        this.logger.warn('Sin cuenta contable origen para reintegro, asiento omitido');
        return null;
      }
      lineas = [
        {
          cuenta_id: cuentaAnticipo,
          debe_moneda: monto,
          haber_moneda: 0,
          descripcion: `Reintegro viáticos - ${empleadoNombre}`,
        },
        {
          cuenta_id: cuentaTes,
          debe_moneda: 0,
          haber_moneda: monto,
          descripcion: `Cancela anticipo ${dev.rendicion?.numero_rendicion ?? ''} desde ${dev.tes_cuenta?.nombre ?? 'caja'}`,
        },
      ];
    } else if (dev.tipo === 'REINTEGRO' && dev.diferido) {
      // Reintegro diferido → DEBE Anticipo / HABER Reintegros Pendientes.
      let cuentaReintegro: string;
      try {
        cuentaReintegro = await this.mapeo.getCuentaPorConcepto(empresaId, 'REINTEGRO_EMPLEADO_PENDIENTE');
      } catch {
        this.logger.warn(`Falta concepto REINTEGRO_EMPLEADO_PENDIENTE para empresa ${empresaId}`);
        return null;
      }
      lineas = [
        {
          cuenta_id: cuentaAnticipo,
          debe_moneda: monto,
          haber_moneda: 0,
          descripcion: `Reintegro viáticos diferido - ${empleadoNombre}`,
        },
        {
          cuenta_id: cuentaReintegro,
          debe_moneda: 0,
          haber_moneda: monto,
          descripcion: `Queda como CxP a ${empleadoNombre}`,
        },
      ];
    } else {
      return null;
    }

    const doc = await this.prisma.cont_documentos.create({
      data: {
        empresa_id: empresaId,
        tipo: 'PAGO',
        origen_tipo: 'rendicion_devolucion',
        origen_id: devolucionId,
        numero_documento: numeroDoc,
        estado: 'CONFIRMADO',
      },
    });

    await this.asientos.crearConfirmado(empresaId, usuarioId ?? empresaId, doc.id, {
      periodo_id: periodoId,
      fecha: fecha.toISOString().substring(0, 10),
      glosa: `Cierre rendición ${dev.rendicion?.numero_rendicion ?? ''} — ${dev.tipo}${dev.diferido ? ' diferido' : ''}`,
      moneda_origen: 'PYG',
      lineas,
    });

    this.logger.log(`Cierre ${devolucionId} contabilizado (doc ${doc.id})`);
    return doc;
  }

  /**
   * Al pagar una CxP diferida a empleado:
   *   DEBE  Reintegros Empleado Pendientes (REINTEGRO_EMPLEADO_PENDIENTE)  monto
   *   HABER cuenta tesorería origen (o fallback BANCO/CAJA_GENERAL)         monto
   */
  async registrarPagoCxpEmpleado(
    cxpId: string,
    empresaId: string,
    monto: number,
    fecha: Date,
    tesCuentaId: string,
    usuarioId?: string,
  ) {
    if (!(await this.tieneModuloContabilidad(empresaId))) return null;
    if (monto <= 0) return null;

    let periodoId: string;
    try {
      periodoId = await this.periodos.resolverPeriodoPorFecha(empresaId, fecha);
    } catch (err) {
      const motivo = `No hay período contable abierto para la fecha ${fecha.toISOString().substring(0, 10)}`;
      this.logger.warn(`${motivo} — pago CxP ${cxpId}`);
      await this.auditarFalloAsiento(
        empresaId,
        'cuentas_pagar_empleado',
        cxpId,
        motivo,
        { fecha, error: err instanceof Error ? err.message : String(err) },
        usuarioId,
      );
      return null;
    }

    let cuentaReintegro: string;
    try {
      cuentaReintegro = await this.mapeo.getCuentaPorConcepto(
        empresaId,
        'REINTEGRO_EMPLEADO_PENDIENTE',
      );
    } catch {
      const motivo = 'Falta mapeo del concepto REINTEGRO_EMPLEADO_PENDIENTE';
      this.logger.warn(`${motivo} — pago CxP ${cxpId}`);
      await this.auditarFalloAsiento(
        empresaId,
        'cuentas_pagar_empleado',
        cxpId,
        motivo,
        { concepto_faltante: 'REINTEGRO_EMPLEADO_PENDIENTE' },
        usuarioId,
      );
      return null;
    }

    // Cuenta HABER: tes_cuenta.cuenta_contable_id, evitando OLD- o inactiva.
    const tesCuenta = await this.prisma.tes_cuentas.findUnique({
      where: { id: tesCuentaId },
      select: { cuenta_contable_id: true },
    });
    let cuentaTes: string | null = tesCuenta?.cuenta_contable_id ?? null;
    if (cuentaTes) {
      const c = await this.prisma.cont_plan_cuentas.findUnique({
        where: { id: cuentaTes },
        select: { codigo: true, active: true },
      });
      if (!c || !c.active || (c.codigo && c.codigo.startsWith('OLD-'))) {
        this.logger.warn(
          `Cuenta contable de tes_cuenta ${tesCuentaId} OLD/inactiva, fallback a mapeo`,
        );
        cuentaTes = null;
      }
    }
    if (!cuentaTes) {
      try {
        cuentaTes =
          (await this.mapeo.getCuentaPorConcepto(empresaId, 'BANCO')) ??
          (await this.mapeo.getCuentaPorConcepto(empresaId, 'CAJA_GENERAL'));
      } catch {
        const motivo = 'Sin cuenta contable HABER — falta mapeo BANCO/CAJA_GENERAL';
        this.logger.warn(`${motivo} — pago CxP ${cxpId}`);
        await this.auditarFalloAsiento(
          empresaId,
          'cuentas_pagar_empleado',
          cxpId,
          motivo,
          { tes_cuenta_id: tesCuentaId },
          usuarioId,
        );
        return null;
      }
      if (!cuentaTes) {
        const motivo = 'Sin cuenta contable HABER — falta mapeo BANCO/CAJA_GENERAL';
        this.logger.warn(`${motivo} — pago CxP ${cxpId}`);
        await this.auditarFalloAsiento(
          empresaId,
          'cuentas_pagar_empleado',
          cxpId,
          motivo,
          { tes_cuenta_id: tesCuentaId },
          usuarioId,
        );
        return null;
      }
    }

    const cxp = await this.prisma.cuentas_pagar_empleado.findFirst({
      where: { id: cxpId, empresa_id: empresaId },
      include: { empleado: { select: { nombres: true, apellidos: true } } },
    });
    const empleadoNombre = cxp?.empleado
      ? `${cxp.empleado.nombres} ${cxp.empleado.apellidos}`
      : 'empleado';
    const numeroDoc = `PAG-CXP-EMP-${cxpId.substring(0, 8)}`;

    const doc = await this.prisma.cont_documentos.create({
      data: {
        empresa_id: empresaId,
        tipo: 'PAGO',
        origen_tipo: 'cxp_empleado_pago',
        origen_id: cxpId,
        numero_documento: numeroDoc,
        estado: 'CONFIRMADO',
      },
    });

    await this.asientos.crearConfirmado(empresaId, usuarioId ?? empresaId, doc.id, {
      periodo_id: periodoId,
      fecha: fecha.toISOString().substring(0, 10),
      glosa: `Pago CxP viáticos a ${empleadoNombre}`,
      moneda_origen: 'PYG',
      lineas: [
        {
          cuenta_id: cuentaReintegro,
          debe_moneda: monto,
          haber_moneda: 0,
          descripcion: `Cancela CxP a ${empleadoNombre}`,
        },
        {
          cuenta_id: cuentaTes,
          debe_moneda: 0,
          haber_moneda: monto,
          descripcion: `Egreso de caja/banco por pago a ${empleadoNombre}`,
        },
      ],
    });

    this.logger.log(`Pago CxP ${cxpId} contabilizado (doc ${doc.id})`);
    return doc;
  }

  /**
   * Reversa el asiento de cierre (al anular la rendición).
   */
  async revertirDevolucion(devolucionId: string, empresaId: string, usuarioId?: string) {
    if (!(await this.tieneModuloContabilidad(empresaId))) return;
    const doc = await this.prisma.cont_documentos.findFirst({
      where: { origen_tipo: 'rendicion_devolucion', origen_id: devolucionId, estado: 'CONFIRMADO' },
      include: { asiento: true },
    });
    if (!doc || !doc.asiento) return;
    await this.asientos.revertir(doc.asiento.id, empresaId, usuarioId ?? null);
    this.logger.log(`Reversión contable de devolución ${devolucionId}`);
  }
}
