import { BadRequestException, Injectable, Logger, NotFoundException, OnModuleInit, ConflictException } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { AuditService } from '../../audit/audit.service';
import { ContabilidadIntegracionService } from '../../contabilidad/services/integracion.service';
import { PrismaService } from '../../prisma/prisma.service';
import {
  AcreditarChequeDto,
  CreateTesChequeDto,
  FilterTesChequeDto,
  RechazarChequeDto,
  TesChequeEstado,
  TesChequeTipo,
  UpdateTesChequeDto,
} from '../dto/tes-cheque.dto';

@Injectable()
export class TesChequeService implements OnModuleInit {
  private readonly logger = new Logger(TesChequeService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly contabilidad: ContabilidadIntegracionService,
    private readonly audit: AuditService,
  ) {}

  /**
   * El cron de las 06:00 sólo corre si el proceso está vivo a esa hora. Un
   * deploy o un reinicio nocturno se saltea la ventana y los cheques quedan
   * DIFERIDO para siempre (pasó: 3 cheques vencidos el 2026-08-31 seguían
   * diferidos el 2026-09-05). Al levantar, se pone al día.
   */
  async onModuleInit() {
    await this.transicionarDiferidosAlDia();
  }

  async findAll(empresaId: string, filters: FilterTesChequeDto = {}) {
    const where: any = { empresa_id: empresaId };
    if (filters.tipo) where.tipo = filters.tipo;
    if (filters.estado) where.estado = filters.estado;
    if (filters.cuenta_id) where.cuenta_id = filters.cuenta_id;
    if (filters.proximos_vencer === 'true') {
      const en7dias = new Date();
      en7dias.setDate(en7dias.getDate() + 7);
      where.estado = TesChequeEstado.EN_CARTERA;
      where.fecha_vencimiento = { lte: en7dias };
    }

    return this.prisma.tes_cheques.findMany({
      where,
      include: { cuenta: { select: { id: true, nombre: true, moneda: true } } },
      orderBy: [{ fecha_vencimiento: 'asc' }, { created_at: 'desc' }],
    });
  }

  async create(dto: CreateTesChequeDto, empresaId: string, userId: string) {
    // Duplicados: el mismo cheque cargado dos veces mostraba dos ingresos por la
    // misma plata (caso DOBA, cheque 5792344 cargado tres veces). No se bloquea
    // duro —dos bancos distintos pueden repetir número— sino que se avisa cuál es
    // y se exige confirmación. Sólo cuentan los cheques vivos: uno rechazado,
    // devuelto o anulado no impide volver a cargarlo.
    if (!dto.confirmar_duplicado) {
      const existente = await this.prisma.tes_cheques.findFirst({
        where: {
          empresa_id: empresaId,
          tipo: dto.tipo,
          numero_cheque: dto.numero_cheque,
          estado: { in: [TesChequeEstado.EN_CARTERA, TesChequeEstado.DIFERIDO, TesChequeEstado.DEPOSITADO, TesChequeEstado.DEBITADO] },
          ...(dto.banco_emisor?.trim() ? { banco_emisor: dto.banco_emisor.trim() } : {}),
        },
        select: { id: true, numero_cheque: true, banco_emisor: true, monto: true, estado: true, fecha_emision: true },
      });
      if (existente) {
        const emision = existente.fecha_emision
          ? ` emitido el ${existente.fecha_emision.toISOString().slice(0, 10)}`
          : '';
        throw new ConflictException({
          message:
            `Ya existe el cheque Nº ${existente.numero_cheque}${existente.banco_emisor ? ` de ${existente.banco_emisor}` : ''}` +
            ` por ${Number(existente.monto).toLocaleString('es-PY')}${emision}, en estado ${existente.estado}. ` +
            'Si de verdad es otro cheque distinto, confirmá para cargarlo igual.',
          codigo: 'CHEQUE_DUPLICADO',
          cheque_existente: existente,
        });
      }
    }

    // Un cheque con fecha de vencimiento futura nace DIFERIDO: todavía no se
    // puede depositar/debitar. Es el mismo criterio que ya aplican recibos,
    // pagos a proveedor y rendiciones al generar cheques; el alta manual era la
    // única puerta que lo dejaba EN_CARTERA y permitía cobrarlo antes de fecha.
    const esDiferido =
      !!dto.fecha_vencimiento && dto.fecha_vencimiento.slice(0, 10) > hoyLocalISO();

    // El cheque EMITIDO se libra contra una cuenta propia: sin ella no se sabe
    // de dónde va a salir la plata, y el banco emisor es el de esa cuenta (no
    // algo que el usuario deba volver a tipear). El RECIBIDO es al revés: el
    // banco es el del cliente y la cuenta de depósito se decide al depositar.
    let bancoEmisor = dto.banco_emisor?.trim() || null;
    let cuentaBancariaOrigen = dto.cuenta_bancaria_origen?.trim() || null;
    if (dto.tipo === TesChequeTipo.EMITIDO) {
      if (!dto.cuenta_id) {
        throw new BadRequestException(
          'Indicá la cuenta de tesorería contra la que se libra el cheque emitido.',
        );
      }
      const cuenta = await this.prisma.tes_cuentas.findFirst({
        where: { id: dto.cuenta_id, empresa_id: empresaId, activo: true },
        select: { banco_id: true, numero_cuenta: true },
      });
      if (!cuenta) throw new NotFoundException('Cuenta de tesorería no encontrada o inactiva');
      bancoEmisor = (await this.nombreBanco(cuenta.banco_id)) ?? bancoEmisor;
      cuentaBancariaOrigen = cuenta.numero_cuenta?.trim() || cuentaBancariaOrigen;
    } else if (dto.cuenta_id) {
      const cuenta = await this.prisma.tes_cuentas.findFirst({
        where: { id: dto.cuenta_id, empresa_id: empresaId, activo: true },
        select: { id: true },
      });
      if (!cuenta) throw new NotFoundException('Cuenta de tesorería no encontrada o inactiva');
    }

    const cheque = await this.prisma.tes_cheques.create({
      data: {
        empresa_id: empresaId,
        tipo: dto.tipo,
        estado: esDiferido ? TesChequeEstado.DIFERIDO : TesChequeEstado.EN_CARTERA,
        numero_cheque: dto.numero_cheque,
        banco_emisor: bancoEmisor,
        cuenta_bancaria_origen: cuentaBancariaOrigen,
        titular: dto.titular ?? null,
        monto: dto.monto,
        moneda: dto.moneda ?? 'PYG',
        fecha_emision: new Date(dto.fecha_emision),
        fecha_vencimiento: dto.fecha_vencimiento ? new Date(dto.fecha_vencimiento) : null,
        cuenta_id: dto.cuenta_id ?? null,
        referencia: dto.referencia ?? null,
        registrado_por: userId,
      },
      include: { cuenta: { select: { id: true, nombre: true, moneda: true } } },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'TES_CHEQUE_REGISTRADO',
      entity_type: 'tes_cheques',
      entity_id: cheque.id,
      new_value: { tipo: cheque.tipo, numero: cheque.numero_cheque, monto: cheque.monto, moneda: cheque.moneda },
      descripcion: `Cheque ${cheque.tipo} Nº ${cheque.numero_cheque} registrado por ${Number(cheque.monto).toLocaleString()} ${cheque.moneda}`,
    });

    return cheque;
  }

  async depositar(id: string, dto: AcreditarChequeDto, empresaId: string, userId: string) {
    const cheque = await this.prisma.tes_cheques.findFirst({ where: { id, empresa_id: empresaId } });
    if (!cheque) throw new NotFoundException('Cheque no encontrado');
    if (cheque.tipo !== TesChequeTipo.RECIBIDO) throw new BadRequestException('Solo se pueden depositar cheques recibidos');
    this.assertOperable(cheque, 'depositar');
    const fechaMovimiento = this.validarFechaMovimiento(dto.fecha_movimiento, cheque.fecha_emision);

    const cuenta = await this.prisma.tes_cuentas.findFirst({ where: { id: dto.cuenta_id, empresa_id: empresaId, activo: true } });
    if (!cuenta) throw new NotFoundException('Cuenta de tesorería no encontrada');

    let categoria = await this.prisma.tes_categorias.findFirst({ where: { codigo_interno: 'CHEQUE_DEPOSITADO', is_sistema: true } });
    if (!categoria) {
      categoria = await this.prisma.tes_categorias.create({
        data: { nombre: 'Cheque Depositado', codigo_interno: 'CHEQUE_DEPOSITADO', tipo: 'INGRESO', is_sistema: true },
      });
    }

    let movimientoId: string;
    const resultado = await this.prisma.$transaction(async (tx) => {
      const movimiento = await tx.tes_movimientos.create({
        data: {
          empresa_id: empresaId,
          cuenta_id: dto.cuenta_id,
          tipo: 'INGRESO',
          estado: 'CONFIRMADO',
          fecha: fechaMovimiento,
          monto: cheque.monto,
          descripcion: dto.referencia ?? `Depósito cheque Nº ${cheque.numero_cheque}${cheque.titular ? ` — ${cheque.titular}` : ''}`,
          // Marca el movimiento como generado por el cheque: TesMovimientosService.anular()
          // lo bloquea y obliga a pasar por Cheques → Rechazar, que revierte las dos
          // puntas. Sin esto el movimiento se anulaba solo y el cheque quedaba colgado
          // en DEPOSITADO apuntando a un movimiento anulado.
          origen_tipo: 'CHEQUE_DEPOSITADO',
          origen_id: id,
          created_by: userId,
          confirmado_por: userId,
          confirmado_en: new Date(),
          detalles: {
            create: [{ categoria_id: categoria.id, monto: cheque.monto }],
          },
        },
      });
      movimientoId = movimiento.id;

      await tx.tes_cuentas.update({
        where: { id: dto.cuenta_id },
        data: { saldo_actual: { increment: Number(cheque.monto) } },
      });

      return tx.tes_cheques.update({
        where: { id },
        data: {
          estado: TesChequeEstado.DEPOSITADO,
          cuenta_id: dto.cuenta_id,
          movimiento_id: movimiento.id,
          acreditado_en: new Date(),
        },
        include: { cuenta: { select: { id: true, nombre: true, moneda: true } } },
      });
    });

    this.contabilidad.integrarMovimientoTesoreria(movimientoId!).catch((e) =>
      this.logger.error(`Error al contabilizar depósito cheque ${id}: ${e.message}`),
    );

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'TES_CHEQUE_DEPOSITADO',
      entity_type: 'tes_cheques',
      entity_id: id,
      new_value: { cuenta_id: dto.cuenta_id, movimiento_id: movimientoId },
      descripcion: `Cheque Nº ${cheque.numero_cheque} depositado en cuenta ${cuenta.nombre}`,
    });

    return resultado;
  }

  async debitar(id: string, dto: AcreditarChequeDto, empresaId: string, userId: string) {
    const cheque = await this.prisma.tes_cheques.findFirst({ where: { id, empresa_id: empresaId } });
    if (!cheque) throw new NotFoundException('Cheque no encontrado');
    if (cheque.tipo !== TesChequeTipo.EMITIDO) throw new BadRequestException('Solo se pueden debitar cheques emitidos');
    // El cheque emitido se libra contra una cuenta concreta. Debitarlo de otra
    // deja la cuenta real con saldo de más y la elegida con saldo de menos, y
    // ninguna de las dos concilia contra el extracto del banco.
    if (cheque.cuenta_id && dto.cuenta_id !== cheque.cuenta_id) {
      throw new BadRequestException(
        'El cheque fue librado contra otra cuenta. Debitalo desde esa misma cuenta, o corregí el cheque antes.',
      );
    }
    this.assertOperable(cheque, 'debitar');
    const fechaMovimiento = this.validarFechaMovimiento(dto.fecha_movimiento, cheque.fecha_emision);

    const cuenta = await this.prisma.tes_cuentas.findFirst({ where: { id: dto.cuenta_id, empresa_id: empresaId, activo: true } });
    if (!cuenta) throw new NotFoundException('Cuenta de tesorería no encontrada');

    let categoria = await this.prisma.tes_categorias.findFirst({ where: { codigo_interno: 'CHEQUE_DEBITADO', is_sistema: true } });
    if (!categoria) {
      categoria = await this.prisma.tes_categorias.create({
        data: { nombre: 'Cheque Debitado', codigo_interno: 'CHEQUE_DEBITADO', tipo: 'EGRESO', is_sistema: true },
      });
    }

    let movimientoId: string;
    const resultado = await this.prisma.$transaction(async (tx) => {
      const movimiento = await tx.tes_movimientos.create({
        data: {
          empresa_id: empresaId,
          cuenta_id: dto.cuenta_id,
          tipo: 'EGRESO',
          estado: 'CONFIRMADO',
          fecha: fechaMovimiento,
          monto: cheque.monto,
          descripcion: dto.referencia ?? `Débito cheque Nº ${cheque.numero_cheque}${cheque.titular ? ` — ${cheque.titular}` : ''}`,
          // Ver comentario en depositar(): el origen es lo que impide anular este
          // movimiento por separado y descuadrar la cartera de cheques.
          origen_tipo: 'CHEQUE_DEBITADO',
          origen_id: id,
          created_by: userId,
          confirmado_por: userId,
          confirmado_en: new Date(),
          detalles: {
            create: [{ categoria_id: categoria.id, monto: cheque.monto }],
          },
        },
      });
      movimientoId = movimiento.id;

      await tx.tes_cuentas.update({
        where: { id: dto.cuenta_id },
        data: { saldo_actual: { increment: -Number(cheque.monto) } },
      });

      return tx.tes_cheques.update({
        where: { id },
        data: {
          estado: TesChequeEstado.DEBITADO,
          cuenta_id: dto.cuenta_id,
          movimiento_id: movimiento.id,
          acreditado_en: new Date(),
        },
        include: { cuenta: { select: { id: true, nombre: true, moneda: true } } },
      });
    });

    this.contabilidad.integrarMovimientoTesoreria(movimientoId!).catch((e) =>
      this.logger.error(`Error al contabilizar débito cheque ${id}: ${e.message}`),
    );

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'TES_CHEQUE_DEBITADO',
      entity_type: 'tes_cheques',
      entity_id: id,
      new_value: { cuenta_id: dto.cuenta_id, movimiento_id: movimientoId },
      descripcion: `Cheque emitido Nº ${cheque.numero_cheque} debitado de cuenta ${cuenta.nombre}`,
    });

    return resultado;
  }

  async rechazar(id: string, dto: RechazarChequeDto, empresaId: string, userId: string) {
    const cheque = await this.prisma.tes_cheques.findFirst({
      where: { id, empresa_id: empresaId },
      include: { movimiento: { select: { id: true, cuenta_id: true, monto: true } } },
    });
    if (!cheque) throw new NotFoundException('Cheque no encontrado');
    if (![TesChequeEstado.EN_CARTERA, TesChequeEstado.DEPOSITADO].includes(cheque.estado as TesChequeEstado)) {
      throw new BadRequestException(`No se puede rechazar un cheque en estado ${cheque.estado}`);
    }

    const eraDepositado = (cheque.estado as TesChequeEstado) === TesChequeEstado.DEPOSITADO && cheque.movimiento;

    let reversaMovimientoId: string | undefined;

    if (eraDepositado && cheque.movimiento) {
      // El banco rechazó el cheque ya depositado → revertir el crédito en la cuenta de tesorería
      let categoriaReversa = await this.prisma.tes_categorias.findFirst({ where: { codigo_interno: 'CHEQUE_RECHAZADO', is_sistema: true } });
      if (!categoriaReversa) {
        categoriaReversa = await this.prisma.tes_categorias.create({
          data: { nombre: 'Cheque Rechazado', codigo_interno: 'CHEQUE_RECHAZADO', tipo: 'EGRESO', is_sistema: true },
        });
      }

      const monto = Number(cheque.movimiento.monto);
      const cuentaId = cheque.movimiento.cuenta_id;

      await this.prisma.$transaction(async (tx) => {
        const reversa = await tx.tes_movimientos.create({
          data: {
            empresa_id: empresaId,
            cuenta_id: cuentaId,
            tipo: 'EGRESO',
            estado: 'CONFIRMADO',
            fecha: new Date(),
            monto,
            descripcion: `Reversa cheque rechazado Nº ${cheque.numero_cheque}${dto.motivo ? ` — ${dto.motivo}` : ''}`,
            created_by: userId,
            confirmado_por: userId,
            confirmado_en: new Date(),
            origen_tipo: 'CHEQUE_RECHAZADO',
            origen_id: id,
            detalles: { create: [{ categoria_id: categoriaReversa!.id, monto }] },
          },
        });
        reversaMovimientoId = reversa.id;

        await tx.tes_cuentas.update({
          where: { id: cuentaId },
          data: { saldo_actual: { decrement: monto } },
        });

        await tx.tes_cheques.update({
          where: { id },
          data: { estado: TesChequeEstado.RECHAZADO, motivo: dto.motivo },
        });
      });
    } else {
      await this.prisma.tes_cheques.update({
        where: { id },
        data: { estado: TesChequeEstado.RECHAZADO, motivo: dto.motivo },
      });
    }

    const resultado = await this.prisma.tes_cheques.findFirst({
      where: { id },
      include: { cuenta: { select: { id: true, nombre: true, moneda: true } } },
    });

    if (reversaMovimientoId) {
      this.contabilidad.integrarMovimientoTesoreria(reversaMovimientoId).catch((e) =>
        this.logger.error(`Error al contabilizar reversa cheque rechazado ${id}: ${e.message}`),
      );
    }

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'TES_CHEQUE_RECHAZADO',
      entity_type: 'tes_cheques',
      entity_id: id,
      old_value: { estado: cheque.estado },
      new_value: { estado: TesChequeEstado.RECHAZADO, motivo: dto.motivo, reversa_movimiento_id: reversaMovimientoId },
      descripcion: `Cheque Nº ${cheque.numero_cheque} rechazado. Motivo: ${dto.motivo}${eraDepositado ? ' (reversa de saldo aplicada)' : ''}`,
    });

    return resultado;
  }

  async devolver(id: string, dto: RechazarChequeDto, empresaId: string, userId: string) {
    const cheque = await this.prisma.tes_cheques.findFirst({ where: { id, empresa_id: empresaId } });
    if (!cheque) throw new NotFoundException('Cheque no encontrado');
    if (cheque.tipo !== TesChequeTipo.RECIBIDO) throw new BadRequestException('Solo aplica a cheques recibidos');
    if (cheque.estado !== TesChequeEstado.EN_CARTERA && cheque.estado !== TesChequeEstado.DIFERIDO)
      throw new BadRequestException(`El cheque está en estado ${cheque.estado}`);
    const resultado = await this.prisma.tes_cheques.update({
      where: { id },
      data: { estado: TesChequeEstado.DEVUELTO, motivo: dto.motivo },
      include: { cuenta: { select: { id: true, nombre: true, moneda: true } } },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'TES_CHEQUE_DEVUELTO',
      entity_type: 'tes_cheques',
      entity_id: id,
      new_value: { estado: TesChequeEstado.DEVUELTO, motivo: dto.motivo },
      descripcion: `Cheque Nº ${cheque.numero_cheque} devuelto. Motivo: ${dto.motivo}`,
    });

    return resultado;
  }

  async anular(id: string, dto: RechazarChequeDto, empresaId: string, userId: string) {
    const cheque = await this.prisma.tes_cheques.findFirst({
      where: { id, empresa_id: empresaId },
      include: { movimiento: { select: { id: true, cuenta_id: true, monto: true } } },
    });
    if (!cheque) throw new NotFoundException('Cheque no encontrado');
    if (cheque.tipo !== TesChequeTipo.EMITIDO) throw new BadRequestException('Solo aplica a cheques emitidos');
    if (![TesChequeEstado.EN_CARTERA, TesChequeEstado.DEBITADO].includes(cheque.estado as TesChequeEstado)) {
      throw new BadRequestException(`No se puede anular un cheque en estado ${cheque.estado}`);
    }

    const eraDebitado = (cheque.estado as TesChequeEstado) === TesChequeEstado.DEBITADO && cheque.movimiento;
    let reversaMovimientoId: string | undefined;

    if (eraDebitado && cheque.movimiento) {
      // Cheque ya debitado → revertir el egreso en la cuenta de tesorería
      let categoriaReversa = await this.prisma.tes_categorias.findFirst({ where: { codigo_interno: 'CHEQUE_ANULADO', is_sistema: true } });
      if (!categoriaReversa) {
        categoriaReversa = await this.prisma.tes_categorias.create({
          data: { nombre: 'Cheque Anulado', codigo_interno: 'CHEQUE_ANULADO', tipo: 'INGRESO', is_sistema: true },
        });
      }

      const monto = Number(cheque.movimiento.monto);
      const cuentaId = cheque.movimiento.cuenta_id;

      await this.prisma.$transaction(async (tx) => {
        const reversa = await tx.tes_movimientos.create({
          data: {
            empresa_id: empresaId,
            cuenta_id: cuentaId,
            tipo: 'INGRESO',
            estado: 'CONFIRMADO',
            fecha: new Date(),
            monto,
            descripcion: `Reversa cheque anulado Nº ${cheque.numero_cheque}${dto.motivo ? ` — ${dto.motivo}` : ''}`,
            created_by: userId,
            confirmado_por: userId,
            confirmado_en: new Date(),
            origen_tipo: 'CHEQUE_ANULADO',
            origen_id: id,
            detalles: { create: [{ categoria_id: categoriaReversa!.id, monto }] },
          },
        });
        reversaMovimientoId = reversa.id;

        await tx.tes_cuentas.update({
          where: { id: cuentaId },
          data: { saldo_actual: { increment: monto } },
        });

        await tx.tes_cheques.update({
          where: { id },
          data: { estado: TesChequeEstado.ANULADO, motivo: dto.motivo },
        });
      });
    } else {
      await this.prisma.tes_cheques.update({
        where: { id },
        data: { estado: TesChequeEstado.ANULADO, motivo: dto.motivo },
      });
    }

    const resultado = await this.prisma.tes_cheques.findFirst({
      where: { id },
      include: { cuenta: { select: { id: true, nombre: true, moneda: true } } },
    });

    if (reversaMovimientoId) {
      this.contabilidad.integrarMovimientoTesoreria(reversaMovimientoId).catch((e) =>
        this.logger.warn(`integrar reversa anulacion cheque: ${e?.message}`),
      );
    }

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'TES_CHEQUE_ANULADO',
      entity_type: 'tes_cheques',
      entity_id: id,
      new_value: { estado: TesChequeEstado.ANULADO, motivo: dto.motivo, reversa_movimiento_id: reversaMovimientoId },
      descripcion: `Cheque emitido Nº ${cheque.numero_cheque} anulado${eraDebitado ? ' (con reversa de débito)' : ''}. Motivo: ${dto.motivo}`,
    });

    return resultado;
  }

  /**
   * Edición acotada de campos no contables. No permite cambiar monto/cuenta ni
   * estado: para eso existen las acciones específicas (depositar/anular/...).
   * Sólo aplica a cheques EN_CARTERA o DIFERIDO (aún no acreditados/rechazados).
   */
  /**
   * Deshace un depósito (o un débito) que nunca ocurrió en el banco: fue un
   * error de carga.
   *
   * NO crea un movimiento de egreso como `rechazar`. Cuando el banco rechaza un
   * cheque hay dos hechos reales —entró y salió— y el extracto debe mostrarlos;
   * acá no hubo ninguno, así que un egreso mostraría una salida de plata que el
   * banco nunca tuvo (el reclamo de DOBA: "me aparece en mayo como egreso y no
   * es egreso").
   *
   * Hace lo mismo que `TesMovimientosService.anular`: devuelve el saldo, deja el
   * movimiento en ANULADO —no lo borra, para no perder la trazabilidad— y
   * revierte el asiento contable. El cheque vuelve a la cartera, desde donde se
   * puede editar o eliminar.
   */
  async deshacerDeposito(id: string, empresaId: string, userId: string) {
    const cheque = await this.prisma.tes_cheques.findFirst({
      where: { id, empresa_id: empresaId },
      include: { movimiento: { select: { id: true, cuenta_id: true, monto: true, estado: true, tipo: true } } },
    });
    if (!cheque) throw new NotFoundException('Cheque no encontrado');

    const estadosConMovimiento: string[] = [TesChequeEstado.DEPOSITADO, TesChequeEstado.DEBITADO];
    if (!estadosConMovimiento.includes(cheque.estado)) {
      throw new BadRequestException(
        `El cheque está en estado ${cheque.estado}: no hay un depósito o débito que deshacer.`,
      );
    }
    if (!cheque.movimiento) {
      throw new BadRequestException('El cheque no tiene un movimiento de tesorería asociado.');
    }
    if (cheque.movimiento.estado !== 'CONFIRMADO') {
      throw new BadRequestException(
        `El movimiento está en estado ${cheque.movimiento.estado}: no se puede deshacer.`,
      );
    }

    // Conciliado contra el extracto del banco: el hecho ya quedó firmado y
    // deshacerlo descuadraría la conciliación. Ahí corresponde rechazar.
    const conciliado = await this.prisma.tes_extracto_lineas.findFirst({
      where: { movimiento_id: cheque.movimiento.id, conciliado: true },
      select: { id: true },
    });
    if (conciliado) {
      throw new BadRequestException(
        'El movimiento ya fue conciliado con el extracto del banco. Si el banco rechazó el cheque, usá "Rechazar"; si no, primero deshacé la conciliación.',
      );
    }

    const monto = Number(cheque.movimiento.monto);
    const esIngreso = cheque.movimiento.tipo === 'INGRESO';
    // Vuelve a diferido si su fecha todavía no llegó; si no, a la cartera.
    const estadoDestino =
      cheque.fecha_vencimiento && cheque.fecha_vencimiento.toISOString().slice(0, 10) > hoyLocalISO()
        ? TesChequeEstado.DIFERIDO
        : TesChequeEstado.EN_CARTERA;

    const resultado = await this.prisma.$transaction(async (tx) => {
      await tx.tes_movimientos.update({
        where: { id: cheque.movimiento!.id },
        data: { estado: 'ANULADO', updated_at: new Date() },
      });

      if (cheque.movimiento!.cuenta_id) {
        await tx.tes_cuentas.update({
          where: { id: cheque.movimiento!.cuenta_id },
          // Operación inversa a la que hizo el depósito (ingreso) o el débito (egreso).
          data: esIngreso ? { saldo_actual: { decrement: monto } } : { saldo_actual: { increment: monto } },
        });
      }

      return tx.tes_cheques.update({
        where: { id },
        data: {
          estado: estadoDestino,
          // Se suelta el movimiento anulado: si mañana se deposita de nuevo, el
          // cheque apunta al movimiento vigente y no a uno muerto.
          movimiento_id: null,
          acreditado_en: null,
        },
      });
    });

    this.contabilidad
      .revertirDocumento('tes_movimientos', cheque.movimiento.id, userId)
      .catch((e) =>
        this.logger.error(`Error al revertir asiento del movimiento ${cheque.movimiento!.id}: ${e.message}`),
      );

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'TES_CHEQUE_DEPOSITO_DESHECHO',
      entity_type: 'tes_cheques',
      entity_id: id,
      old_value: { estado: cheque.estado, movimiento_id: cheque.movimiento.id },
      new_value: { estado: estadoDestino, movimiento_anulado: cheque.movimiento.id },
      descripcion: `Se deshizo el ${esIngreso ? 'depósito' : 'débito'} del cheque Nº ${cheque.numero_cheque}: el movimiento quedó anulado`,
    });

    return resultado;
  }

  async update(id: string, dto: UpdateTesChequeDto, empresaId: string, userId: string) {
    const cheque = await this.prisma.tes_cheques.findFirst({ where: { id, empresa_id: empresaId } });
    if (!cheque) throw new NotFoundException('Cheque no encontrado');
    if (![TesChequeEstado.EN_CARTERA, TesChequeEstado.DIFERIDO].includes(cheque.estado as TesChequeEstado)) {
      throw new BadRequestException(
        `No se puede editar un cheque en estado ${cheque.estado}. Sólo cheques EN_CARTERA o DIFERIDO admiten edición.`,
      );
    }

    const data: any = {};
    const cambios: Record<string, { antes: any; despues: any }> = {};
    const campos: Array<keyof UpdateTesChequeDto> = [
      'numero_cheque',
      'banco_emisor',
      'cuenta_bancaria_origen',
      'titular',
      'fecha_emision',
      'fecha_vencimiento',
      'referencia',
    ];

    for (const campo of campos) {
      if (dto[campo] === undefined) continue;
      let nuevoValor: any = dto[campo];
      if ((campo === 'fecha_emision' || campo === 'fecha_vencimiento') && nuevoValor) {
        nuevoValor = new Date(nuevoValor as string);
      }
      if (campo === 'fecha_vencimiento' && (nuevoValor === '' || nuevoValor === null)) {
        nuevoValor = null;
      }
      const actual = (cheque as any)[campo];
      const sonIguales =
        nuevoValor instanceof Date && actual instanceof Date
          ? nuevoValor.getTime() === actual.getTime()
          : String(actual ?? '') === String(nuevoValor ?? '');
      if (sonIguales) continue;
      data[campo] = nuevoValor;
      cambios[campo] = { antes: actual, despues: nuevoValor };
    }

    if (Object.keys(data).length === 0) {
      throw new BadRequestException('No hay cambios para guardar');
    }

    const actualizado = await this.prisma.tes_cheques.update({
      where: { id },
      data: { ...data, updated_at: new Date() },
      include: { cuenta: { select: { id: true, nombre: true, moneda: true } } },
    });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'TES_CHEQUE_EDITADO',
      entity_type: 'tes_cheques',
      entity_id: id,
      old_value: Object.fromEntries(Object.entries(cambios).map(([k, v]) => [k, v.antes])),
      new_value: { cambios: Object.fromEntries(Object.entries(cambios).map(([k, v]) => [k, v.despues])), motivo: dto.motivo },
      descripcion: `Cheque Nº ${cheque.numero_cheque} editado (${Object.keys(cambios).join(', ')}). Motivo: ${dto.motivo}`,
    });

    return actualizado;
  }

  async remove(id: string, empresaId: string, userId: string) {
    const cheque = await this.prisma.tes_cheques.findFirst({ where: { id, empresa_id: empresaId } });
    if (!cheque) throw new NotFoundException('Cheque no encontrado');
    if (cheque.estado !== TesChequeEstado.EN_CARTERA) throw new BadRequestException('Solo se pueden eliminar cheques en estado EN_CARTERA');
    const resultado = await this.prisma.tes_cheques.delete({ where: { id } });

    await this.audit.log({
      empresa_id: empresaId,
      user_id: userId,
      action: 'TES_CHEQUE_ELIMINADO',
      entity_type: 'tes_cheques',
      entity_id: id,
      old_value: { tipo: cheque.tipo, numero: cheque.numero_cheque, monto: cheque.monto },
      descripcion: `Cheque Nº ${cheque.numero_cheque} eliminado`,
    });

    return resultado;
  }

  /**
   * Acepta el cheque para operar (depositar/debitar) si está EN_CARTERA, o si
   * está DIFERIDO pero su fecha de vencimiento ya llegó. Rechaza el resto.
   */
  /**
   * Resuelve la fecha del movimiento contable de un depósito/débito de cheque.
   * Es la fecha real en el banco, no el día en que alguien lo marca en el sistema
   * (a veces pasan varios días entre una cosa y la otra). Sin valor, usa hoy.
   *
   * Compara por string "YYYY-MM-DD", no con Date + setHours: un date-only string
   * como "2026-09-01" se parsea como medianoche UTC, que en un huso horario al
   * oeste de UTC (Paraguay, -03) cae dentro del día anterior en hora local —
   * setHours(0,0,0,0), que opera en local, corría "mañana" un día para atrás y
   * lo hacía pasar como "hoy". Comparar los strings evita el desfasaje.
   */
  private validarFechaMovimiento(fechaStr: string | undefined, fechaEmision: Date): Date {
    if (!fechaStr) return new Date();

    if (fechaStr > hoyLocalISO()) {
      throw new BadRequestException('La fecha del movimiento no puede ser futura');
    }
    if (fechaStr < fechaEmision.toISOString().slice(0, 10)) {
      throw new BadRequestException('La fecha del movimiento no puede ser anterior a la emisión del cheque');
    }
    return new Date(fechaStr);
  }

  /** Nombre del banco de una cuenta propia, para no pedirlo tipeado. */
  private async nombreBanco(bancoId: string | null): Promise<string | null> {
    if (!bancoId) return null;
    const banco = await this.prisma.bancos.findUnique({
      where: { id: bancoId },
      select: { descripcion: true },
    });
    return banco?.descripcion?.trim() || null;
  }

  private assertOperable(cheque: { estado: string; fecha_vencimiento: Date | null }, accion: string) {
    if (cheque.estado === TesChequeEstado.EN_CARTERA) return;
    if (cheque.estado === TesChequeEstado.DIFERIDO) {
      const venc = cheque.fecha_vencimiento ? startOfDay(new Date(cheque.fecha_vencimiento)) : null;
      const hoy = startOfDay(new Date());
      if (venc && venc <= hoy) return; // ya está al día → permitido
      throw new BadRequestException(
        `El cheque está diferido${venc ? ` hasta el ${venc.toLocaleDateString('es-PY')}` : ''}. No se puede ${accion} antes de esa fecha.`,
      );
    }
    throw new BadRequestException(`El cheque está en estado ${cheque.estado}`);
  }

  /**
   * Cada día a las 06:00 mueve los cheques DIFERIDO cuya fecha_vencimiento ya
   * llegó al estado EN_CARTERA. Multi-tenant: corre sobre todas las empresas.
   */
  @Cron(CronExpression.EVERY_DAY_AT_6AM)
  async transicionarDiferidosAlDia() {
    try {
      // Buscar los cheques que pasan a EN_CARTERA hoy, agrupados por empresa,
      // para poder dejar trazabilidad por tenant en auditoría.
      const candidatos = await this.prisma.tes_cheques.findMany({
        where: {
          estado: TesChequeEstado.DIFERIDO,
          fecha_vencimiento: { lte: new Date() },
        },
        select: { id: true, empresa_id: true, numero_cheque: true, monto: true, moneda: true, fecha_vencimiento: true },
      });
      if (candidatos.length === 0) return;

      const ids = candidatos.map((c) => c.id);
      await this.prisma.tes_cheques.updateMany({
        where: { id: { in: ids } },
        data: { estado: TesChequeEstado.EN_CARTERA, updated_at: new Date() },
      });

      // Una entrada de auditoría por empresa con el detalle de los cheques.
      const porEmpresa = new Map<string, typeof candidatos>();
      for (const c of candidatos) {
        const arr = porEmpresa.get(c.empresa_id) ?? [];
        arr.push(c);
        porEmpresa.set(c.empresa_id, arr);
      }

      for (const [empresaId, lista] of porEmpresa) {
        await this.audit.log({
          empresa_id: empresaId,
          action: 'TES_CHEQUES_DIFERIDO_TRANSICION',
          entity_type: 'tes_cheques',
          new_value: {
            cantidad: lista.length,
            cheques: lista.map((c) => ({
              id: c.id,
              numero: c.numero_cheque,
              monto: c.monto,
              moneda: c.moneda,
              fecha_vencimiento: c.fecha_vencimiento,
            })),
          },
          descripcion: `Cron: ${lista.length} cheque(s) pasaron de DIFERIDO a EN_CARTERA por vencimiento cumplido`,
        });
      }

      this.logger.log(
        `Cheques DIFERIDO → EN_CARTERA: ${candidatos.length} (en ${porEmpresa.size} empresa/s)`,
      );
    } catch (err) {
      this.logger.error(`Error transicionando cheques diferidos: ${(err as Error).message}`);
    }
  }
}

function startOfDay(d: Date): Date {
  const x = new Date(d);
  x.setHours(0, 0, 0, 0);
  return x;
}

/** Fecha calendario de hoy en hora local del servidor, como "YYYY-MM-DD". */
function hoyLocalISO(): string {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
