import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';

type Tx = Prisma.TransactionClient;

type TesoreriaSyncResult = {
  warning?: string;
};

const TES_REGLA_PAGO_GASTO = 'PAGO_GASTO';
const TES_ORIGEN_GASTO = 'gasto_cab';
const TES_CATEGORIA_CODIGO_EGRESO_OTRO = 'EGRESO_OTRO';

// Puente Gastos → Tesorería: genera el egreso real (o el cheque emitido) cuando
// un gasto paga de contado/transferencia (no diferido a Orden de Pago). Replica
// el patrón ya probado en PagosProveedorService.syncTesoreriaOrdenPago, pero con
// la regla PAGO_GASTO (separada de PAGO_PROVEEDOR) y un solo pago por gasto.
@Injectable()
export class GastosTesoreriaService {
  private readonly logger = new Logger(GastosTesoreriaService.name);

  constructor(private readonly prisma: PrismaService) {}

  async integrarGasto(gastoId: string): Promise<TesoreriaSyncResult> {
    if (!this.hasTesoreriaBridgeSupport(this.prisma as any)) {
      return {
        warning:
          'El gasto quedó pagado, pero no se registró en Tesorería porque el puente Gasto->Tesorería no está disponible en este entorno.',
      };
    }

    try {
      let result: TesoreriaSyncResult = {};
      await this.prisma.$transaction(async (tx) => {
        const gasto = await tx.gasto_cab.findFirst({
          where: { id: gastoId, anulado: false },
          select: {
            id: true,
            empresa_id: true,
            estado: true,
            forma_pago: true,
            fecha_emision: true,
            total: true,
            numero_factura: true,
            medio_pago_id: true,
            cuenta_tesoreria_id: true,
            medio_pago: { select: { codigo: true, descripcion: true } },
          },
        });
        if (!gasto) return;

        const forma = String(gasto.forma_pago ?? 'CONTADO').toUpperCase();
        // Sólo gastos que quedaron 'pagada' de una vez generan egreso ahora. Los
        // ANTICIPO (rendición de viáticos) ya salieron del anticipo del empleado
        // — esa salida de caja la registra el módulo de rendición de viáticos.
        if (gasto.estado !== 'pagada' || forma === 'ANTICIPO') return;

        const monto = this.round(this.toNumber(gasto.total), 2);
        if (monto <= 0) return;

        // Idempotencia: no reprocesar si ya existe un movimiento o cheque activo.
        const movimientoExistente = await tx.tes_movimientos.findFirst({
          where: {
            empresa_id: gasto.empresa_id,
            origen_tipo: TES_ORIGEN_GASTO,
            origen_id: gasto.id,
            estado: { not: 'ANULADO' },
          },
          select: { id: true },
        });
        const chequeExistente = await tx.tes_cheques.findFirst({
          where: {
            empresa_id: gasto.empresa_id,
            origen_tipo: TES_ORIGEN_GASTO,
            origen_id: gasto.id,
            estado: { not: 'ANULADO' },
          },
          select: { id: true },
        });
        if (movimientoExistente || chequeExistente) return;

        // Resolver cuenta: prioridad a la elegida en el gasto, luego regla PAGO_GASTO.
        let cuentaId: string | null = null;
        if (gasto.cuenta_tesoreria_id) {
          const cuentaDirecta = await tx.tes_cuentas.findFirst({
            where: { id: gasto.cuenta_tesoreria_id, empresa_id: gasto.empresa_id, activo: true },
            select: { id: true },
          });
          cuentaId = cuentaDirecta?.id ?? null;
        }
        if (!cuentaId) {
          const cuentaInfo = await this.getCuentaTesoreriaPagoGasto(tx, gasto.empresa_id, gasto.medio_pago_id);
          if (!cuentaInfo.id) {
            result = { warning: cuentaInfo.warning };
            this.logger.warn(`Gasto ${gastoId}: ${cuentaInfo.warning}`);
            return;
          }
          cuentaId = cuentaInfo.id;
        }

        const fechaPago = gasto.fecha_emision ? new Date(gasto.fecha_emision) : new Date();
        const numeroDoc = gasto.numero_factura ?? `GASTO-${gastoId.substring(0, 8).toUpperCase()}`;
        const descripcion = `Pago gasto ${numeroDoc}${gasto.medio_pago?.descripcion ? ` — ${gasto.medio_pago.descripcion}` : ''}`;

        const esCheque = this.isMedioCheque(gasto.medio_pago);
        if (esCheque) {
          await tx.tes_cheques.create({
            data: {
              empresa_id: gasto.empresa_id,
              tipo: 'EMITIDO',
              estado: 'EN_CARTERA',
              numero_cheque: `GASTO-${gastoId.substring(0, 8).toUpperCase()}`,
              monto,
              moneda: 'PYG',
              fecha_emision: fechaPago,
              cuenta_id: cuentaId,
              origen_tipo: TES_ORIGEN_GASTO,
              origen_id: gasto.id,
              referencia: descripcion,
            },
          });
          return;
        }

        const categoriaId = await this.getCategoriaTesoreriaPagoGasto(tx, gasto.empresa_id);
        const movimiento = await tx.tes_movimientos.create({
          data: {
            empresa_id: gasto.empresa_id,
            cuenta_id: cuentaId,
            tipo: 'EGRESO',
            estado: 'CONFIRMADO',
            fecha: fechaPago,
            monto,
            descripcion,
            referencia: numeroDoc,
            origen_tipo: TES_ORIGEN_GASTO,
            origen_id: gasto.id,
            confirmado_en: new Date(),
          },
        });

        await tx.tes_movimiento_det.create({
          data: { movimiento_id: movimiento.id, categoria_id: categoriaId, monto, descripcion },
        });

        await tx.tes_cuentas.update({
          where: { id: cuentaId },
          data: { saldo_actual: { decrement: monto } },
        });
      });
      return result;
    } catch (error) {
      if (this.isMissingTableError(error)) {
        this.logger.warn(`Puente Gasto->Tesorería omitido por tablas faltantes (${gastoId})`);
        return {
          warning: 'El gasto quedó pagado, pero no se registró en Tesorería porque faltan tablas de Tesorería en la base actual.',
        };
      }
      const message = (error as Error).message || 'Error desconocido';
      this.logger.error(`Error al sincronizar gasto ${gastoId} con Tesorería: ${message}`);
      return { warning: `El gasto quedó pagado, pero no se pudo registrar en Tesorería. Motivo: ${message}` };
    }
  }

  async revertirGasto(gastoId: string, empresaId: string, motivo?: string): Promise<void> {
    if (!this.hasTesoreriaBridgeSupport(this.prisma as any)) return;

    try {
      await this.prisma.$transaction(async (tx) => {
        const chequesPendientes = await tx.tes_cheques.findMany({
          where: {
            empresa_id: empresaId,
            origen_tipo: TES_ORIGEN_GASTO,
            origen_id: gastoId,
            estado: { in: ['EN_CARTERA', 'DIFERIDO'] },
          },
          select: { id: true },
        });
        if (chequesPendientes.length) {
          await tx.tes_cheques.updateMany({
            where: { id: { in: chequesPendientes.map((c) => c.id) } },
            data: { estado: 'ANULADO', motivo: motivo || 'Anulación de gasto' },
          });
        }

        const movimientos = await tx.tes_movimientos.findMany({
          where: {
            empresa_id: empresaId,
            origen_tipo: TES_ORIGEN_GASTO,
            origen_id: gastoId,
            estado: 'CONFIRMADO',
          },
          select: { id: true, cuenta_id: true, monto: true },
        });
        if (!movimientos.length) return;

        await tx.tes_movimientos.updateMany({
          where: { id: { in: movimientos.map((mov) => mov.id) } },
          data: {
            estado: 'ANULADO',
            motivo_anulacion: motivo || 'Anulación de gasto',
            anulado_en: new Date(),
          },
        });

        for (const mov of movimientos) {
          const monto = this.round(this.toNumber(mov.monto), 2);
          if (monto <= 0) continue;
          await tx.tes_cuentas.update({
            where: { id: mov.cuenta_id },
            data: { saldo_actual: { increment: monto } },
          });
        }
      });
    } catch (error) {
      if (this.isMissingTableError(error)) {
        this.logger.warn(`Reversa Tesorería omitida por tablas faltantes (gasto ${gastoId})`);
        return;
      }
      this.logger.error(`Error al revertir movimiento de Tesorería para gasto ${gastoId}: ${(error as Error).message}`);
    }
  }

  /**
   * ¿El gasto tiene movimientos de tesorería ya conciliados contra un extracto?
   *
   * Si los tiene, no se pueden revertir: la línea del extracto quedaría apuntando a un
   * movimiento anulado y la conciliación del mes se rompe en silencio.
   */
  async tieneMovimientoConciliado(gastoId: string, empresaId: string): Promise<boolean> {
    if (!this.hasTesoreriaBridgeSupport(this.prisma as any)) return false;
    try {
      const conciliada = await this.prisma.tes_extracto_lineas.findFirst({
        where: {
          conciliado: true,
          movimiento: {
            empresa_id: empresaId,
            origen_tipo: TES_ORIGEN_GASTO,
            origen_id: gastoId,
            estado: 'CONFIRMADO',
          },
        },
        select: { id: true },
      });
      return !!conciliada;
    } catch (error) {
      if (this.isMissingTableError(error)) return false;
      // Ante la duda se responde que SÍ está conciliado: es el lado seguro, bloquea
      // la edición en vez de romper una conciliación que no se pudo verificar.
      this.logger.warn(`No se pudo verificar conciliación del gasto ${gastoId}: ${(error as Error).message}`);
      return true;
    }
  }

  /**
   * Rehace el movimiento de tesorería de un gasto que se editó.
   *
   * `integrarGasto` por sí solo no alcanza: tiene guarda de idempotencia y se va sin
   * hacer nada si ya existe un movimiento. Sin esto, editar un gasto pagado dejaba el
   * asiento contable actualizado y el egreso de tesorería con el importe y la cuenta
   * viejos — una diferencia silenciosa que recién aparece al conciliar semanas después.
   */
  async resincronizarGasto(gastoId: string, empresaId: string): Promise<void> {
    await this.revertirGasto(gastoId, empresaId, 'Regenerado por edición del gasto');
    await this.integrarGasto(gastoId);
  }

  private async getCuentaTesoreriaPagoGasto(
    tx: Tx,
    empresaId: string,
    medioPagoId?: string | null,
  ): Promise<{ id: string | null; warning?: string }> {
    const reglaMedio = medioPagoId
      ? await tx.tes_reglas.findFirst({
          where: { empresa_id: empresaId, tipo_operacion: TES_REGLA_PAGO_GASTO, medio_pago_id: medioPagoId },
          select: { activo: true, cuenta_id: true },
        })
      : null;
    const regla =
      reglaMedio ??
      (await tx.tes_reglas.findFirst({
        where: { empresa_id: empresaId, tipo_operacion: TES_REGLA_PAGO_GASTO, medio_pago_id: null },
        select: { activo: true, cuenta_id: true },
      }));

    if (!regla) {
      return {
        id: null,
        warning: 'No existe una regla de Tesorería para "Pago de gasto". Configurá la cuenta en Configuración > Tesorería > Reglas.',
      };
    }
    if (!regla.activo) {
      return {
        id: null,
        warning: 'La regla de Tesorería para "Pago de gasto" está inactiva. Activala en Configuración > Tesorería > Reglas.',
      };
    }
    if (!regla.cuenta_id) {
      return {
        id: null,
        warning: 'La regla de Tesorería para "Pago de gasto" no tiene cuenta asignada. Configurá la cuenta en Configuración > Tesorería > Reglas.',
      };
    }

    const cuenta = await tx.tes_cuentas.findFirst({
      where: { id: regla.cuenta_id, empresa_id: empresaId, activo: true },
      select: { id: true },
    });
    if (!cuenta) {
      return {
        id: null,
        warning: 'La cuenta configurada en Tesorería para "Pago de gasto" está inactiva o no existe. Revisala en Configuración > Tesorería > Reglas.',
      };
    }

    return { id: cuenta.id };
  }

  private async getCategoriaTesoreriaPagoGasto(tx: Tx, empresaId: string): Promise<string> {
    const categoriaEgresoOtro = await tx.tes_categorias.findFirst({
      where: { codigo_interno: TES_CATEGORIA_CODIGO_EGRESO_OTRO, is_sistema: true },
      select: { id: true },
    });
    if (categoriaEgresoOtro) return categoriaEgresoOtro.id;

    const categoriaEmpresa = await tx.tes_categorias.findFirst({
      where: { empresa_id: empresaId, nombre: 'Pago de Gasto', tipo: 'EGRESO' },
      select: { id: true },
    });
    if (categoriaEmpresa) return categoriaEmpresa.id;

    const creada = await tx.tes_categorias.create({
      data: {
        empresa_id: empresaId,
        nombre: 'Pago de Gasto',
        descripcion: 'Egreso generado automáticamente al registrar un gasto de contado',
        tipo: 'EGRESO',
        requiere_comprobante: false,
        is_sistema: false,
        activo: true,
      },
      select: { id: true },
    });
    return creada.id;
  }

  private isMedioCheque(medioPago?: { codigo?: number | null; descripcion?: string | null } | null): boolean {
    const codigo = Number(medioPago?.codigo ?? 0);
    const desc = (medioPago?.descripcion ?? '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
    return codigo === 2 || desc.includes('cheque');
  }

  private toNumber(value: Prisma.Decimal | number | string | null | undefined): number {
    const n = Number(value || 0);
    return Number.isFinite(n) ? n : 0;
  }

  private round(value: number, decimals = 2): number {
    const factor = 10 ** decimals;
    return Math.round((value + Number.EPSILON) * factor) / factor;
  }

  private isMissingTableError(error: unknown) {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      return error.code === 'P2021';
    }
    const message = String((error as any)?.message || '');
    return message.includes('does not exist in the current database');
  }

  private hasTesoreriaBridgeSupport(db: any): boolean {
    return Boolean(
      db?.tes_movimientos && db?.tes_cuentas && db?.tes_reglas && db?.tes_categorias && db?.tes_movimiento_det,
    );
  }
}
