import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { inm_inusual_estado, inm_inusual_regla, Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { aFecha, clienteDe, datosClientes, hoyPy, isoDia, sumarDias } from '../common/inm-utils';
import { NumeracionInmService } from '../common/numeracion-inm.service';
import { ParametrosInmService } from '../config/parametros.service';
import { detectarFraccionamiento } from './cumplimiento-reglas';
import { CrearInusualDto, GestionarInusualDto } from './dto/cumplimiento.dto';

/** Código SIFEN del medio de pago "Efectivo". */
export const MEDIO_EFECTIVO = 1;

const fmtGs = (n: number) => `Gs. ${Math.round(n).toLocaleString('es-PY')}`;

/**
 * Registro de operaciones inusuales (M123) para el oficial de cumplimiento.
 *
 * Automáticas (idempotentes por `clave`):
 *  - EFECTIVO_ALTO: un cobro en efectivo igual o mayor a `umbral_efectivo_gs`.
 *  - FRACCIONAMIENTO: varios cobros en efectivo del mismo cliente que en
 *    `dias_fraccionamiento` suman el umbral sin que ninguno lo alcance (tarea diaria).
 *  - PEP: al aprobar el legajo de un cliente expuesto políticamente.
 * Manuales: las carga cualquiera con permiso; las resuelve el oficial.
 *
 * El ROS a la SEPRELAD se presenta por su sistema: acá se registra número y fecha.
 */
@Injectable()
export class InusualesInmService {
  private readonly logger = new Logger(InusualesInmService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly numeracion: NumeracionInmService,
    private readonly parametros: ParametrosInmService,
  ) {}

  async listar(
    empresa_id: string,
    f: { estado?: inm_inusual_estado; regla?: inm_inusual_regla; cliente_id?: string; abiertas?: boolean },
  ) {
    const filas = await this.prisma.inm_inusuales.findMany({
      where: {
        empresa_id,
        ...(f.estado && { estado: f.estado }),
        ...(f.abiertas && { estado: { in: ['PENDIENTE', 'EN_ANALISIS'] } }),
        ...(f.regla && { regla: f.regla }),
        ...(f.cliente_id && { cliente_id: f.cliente_id }),
      },
      include: { inm_operaciones: { select: { id: true, numero: true } } },
      orderBy: [{ fecha: 'desc' }, { numero: 'desc' }],
      take: 500,
    });
    const clientes = await datosClientes(
      this.prisma,
      empresa_id,
      filas.map((i) => i.cliente_id ?? ''),
    );
    return filas.map((i) => this.armar(i, clientes));
  }

  async detalle(id: string, empresa_id: string) {
    const i = await this.prisma.inm_inusuales.findFirst({
      where: { id, empresa_id },
      include: { inm_operaciones: { select: { id: true, numero: true } } },
    });
    if (!i) throw new NotFoundException('Registro no encontrado');
    const clientes = await datosClientes(this.prisma, empresa_id, [i.cliente_id ?? '']);
    return this.armar(i, clientes);
  }

  async crearManual(dto: CrearInusualDto, empresa_id: string, user_id?: string) {
    if (!dto.cliente_id && !dto.operacion_id) throw new BadRequestException('Indicá el cliente o la operación');
    let cliente_id = dto.cliente_id ?? null;
    if (dto.operacion_id) {
      const op = await this.prisma.inm_operaciones.findFirst({
        where: { id: dto.operacion_id, empresa_id },
        select: { cliente_id: true },
      });
      if (!op) throw new BadRequestException('La operación no existe');
      cliente_id ??= op.cliente_id;
    }
    if (cliente_id && !(await datosClientes(this.prisma, empresa_id, [cliente_id])).size)
      throw new BadRequestException('El cliente no existe en esta empresa');
    const fecha = dto.fecha?.slice(0, 10) ?? hoyPy();
    if (fecha > hoyPy()) throw new BadRequestException('La fecha no puede ser futura');
    const r = await this.prisma.$transaction(async (tx) => {
      const numero = await this.numeracion.siguiente(empresa_id, 'INU', tx);
      return tx.inm_inusuales.create({
        data: {
          empresa_id,
          numero,
          clave: `MAN:${numero}`,
          cliente_id,
          operacion_id: dto.operacion_id ?? null,
          fecha: aFecha(fecha),
          origen: 'MANUAL',
          regla: 'MANUAL',
          descripcion: dto.descripcion.trim(),
          monto: dto.monto ?? null,
          moneda: dto.moneda ?? null,
          created_by: user_id ?? null,
        },
      });
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_inusuales',
      entity_id: r.id,
      new_value: { numero: r.numero, cliente_id, operacion_id: dto.operacion_id },
    });
    return this.detalle(r.id, empresa_id);
  }

  async gestionar(id: string, dto: GestionarInusualDto, empresa_id: string, user_id?: string) {
    const i = await this.prisma.inm_inusuales.findFirst({ where: { id, empresa_id } });
    if (!i) throw new NotFoundException('Registro no encontrado');
    if (i.estado === 'DESCARTADA' || i.estado === 'REPORTADA')
      throw new BadRequestException('El registro ya está resuelto');
    const final = dto.estado === 'DESCARTADA' || dto.estado === 'REPORTADA';
    if (final && !dto.analisis?.trim())
      throw new BadRequestException('Escribí el análisis: queda como respaldo de la decisión');
    if (dto.estado === 'REPORTADA' && (!dto.ros_numero?.trim() || !dto.ros_fecha))
      throw new BadRequestException('Indicá el número y la fecha del ROS presentado a la SEPRELAD');
    if (dto.ros_fecha && dto.ros_fecha.slice(0, 10) > hoyPy())
      throw new BadRequestException('La fecha del ROS no puede ser futura');
    await this.prisma.inm_inusuales.updateMany({
      where: { id, empresa_id },
      data: {
        estado: dto.estado,
        ...(dto.analisis !== undefined && { analisis: dto.analisis.trim() || null }),
        ...(dto.estado === 'REPORTADA' && {
          ros_numero: dto.ros_numero?.trim(),
          ros_fecha: aFecha(dto.ros_fecha),
        }),
        ...(final && { resuelto_por: user_id ?? null, resuelto_at: new Date() }),
      },
    });
    // Un ROS presentado hace ALTO el riesgo del cliente (mismo criterio que calcularRiesgo).
    if (dto.estado === 'REPORTADA' && i.cliente_id)
      await this.prisma.inm_legajos.updateMany({
        where: { empresa_id, cliente_id: i.cliente_id, nivel_riesgo: { not: 'ALTO' } },
        data: { nivel_riesgo: 'ALTO', updated_at: new Date() },
      });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_inusuales',
      entity_id: id,
      old_value: { estado: i.estado },
      new_value: { estado: dto.estado, ros_numero: dto.ros_numero },
    });
    return this.detalle(id, empresa_id);
  }

  /** Crea el registro si no existe uno con la misma clave. Devuelve si lo creó. */
  async registrarAutomatica(
    empresa_id: string,
    r: {
      clave: string;
      regla: inm_inusual_regla;
      descripcion: string;
      cliente_id?: string | null;
      operacion_id?: string | null;
      cobro_id?: string | null;
      monto?: number | null;
      moneda?: string | null;
      fecha?: string;
    },
  ): Promise<boolean> {
    const ya = await this.prisma.inm_inusuales.findFirst({
      where: { empresa_id, clave: r.clave },
      select: { id: true },
    });
    if (ya) return false;
    try {
      await this.prisma.$transaction(async (tx) => {
        const numero = await this.numeracion.siguiente(empresa_id, 'INU', tx);
        await tx.inm_inusuales.create({
          data: {
            empresa_id,
            numero,
            clave: r.clave,
            cliente_id: r.cliente_id ?? null,
            operacion_id: r.operacion_id ?? null,
            cobro_id: r.cobro_id ?? null,
            fecha: aFecha(r.fecha ?? hoyPy()),
            origen: 'AUTOMATICA',
            regla: r.regla,
            descripcion: r.descripcion.slice(0, 1000),
            monto: r.monto ?? null,
            moneda: r.moneda ?? null,
          },
        });
      });
      return true;
    } catch (e) {
      // Carrera con otro proceso que creó la misma clave: ya está registrada.
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') return false;
      throw e;
    }
  }

  /** Después de cada cobro: alerta si fue en efectivo por el umbral o más. Nunca frena el cobro. */
  async evaluarCobro(
    empresa_id: string,
    c: {
      cobro_id: string;
      operacion_id: string;
      numero: string;
      cliente_id: string;
      medio_codigo: number | null;
      monto_gs: number;
    },
  ) {
    if (c.medio_codigo !== MEDIO_EFECTIVO) return false;
    const params = await this.parametros.obtener(empresa_id);
    const umbral = Number(params.umbral_efectivo_gs);
    if (!(umbral > 0) || c.monto_gs < umbral) return false;
    return this.registrarAutomatica(empresa_id, {
      clave: `EFE:${c.cobro_id}`,
      regla: 'EFECTIVO_ALTO',
      cliente_id: c.cliente_id,
      operacion_id: c.operacion_id,
      cobro_id: c.cobro_id,
      monto: Math.round(c.monto_gs),
      moneda: 'PYG',
      descripcion: `Cobro en efectivo de ${fmtGs(c.monto_gs)} en ${c.numero} (umbral ${fmtGs(umbral)}).`,
    });
  }

  /** Tarea diaria: fraccionamiento de efectivo, por empresa. Idempotente por cliente y día. */
  async detectarFraccionamientos(empresa_id: string, hoy = hoyPy()) {
    const params = await this.parametros.obtener(empresa_id);
    const umbral = Number(params.umbral_efectivo_gs);
    if (!(umbral > 0)) return 0;
    const desde = sumarDias(hoy, -(params.dias_fraccionamiento - 1));
    const cobros = await this.prisma.inm_cobros.findMany({
      where: {
        empresa_id,
        estado: 'EMITIDO',
        medio_pago_codigo: MEDIO_EFECTIVO,
        monto_gs: { not: null },
        fecha: { gte: aFecha(desde), lte: aFecha(hoy) },
      },
      select: { monto_gs: true, inm_operaciones: { select: { cliente_id: true } } },
    });
    const casos = detectarFraccionamiento(
      cobros.map((c) => ({ cliente_id: c.inm_operaciones.cliente_id, monto_gs: Number(c.monto_gs) })),
      umbral,
    );
    let n = 0;
    for (const caso of casos) {
      // Una alerta por cliente y ventana: la clave usa el primer día de la ventana
      // que la disparó, así no se repite cada día mientras siga dentro.
      const abierta = await this.prisma.inm_inusuales.findFirst({
        where: {
          empresa_id,
          cliente_id: caso.cliente_id,
          regla: 'FRACCIONAMIENTO',
          fecha: { gte: aFecha(desde) },
        },
        select: { id: true },
      });
      if (abierta) continue;
      const creada = await this.registrarAutomatica(empresa_id, {
        clave: `FRA:${caso.cliente_id}:${hoy}`,
        regla: 'FRACCIONAMIENTO',
        cliente_id: caso.cliente_id,
        monto: caso.total,
        moneda: 'PYG',
        fecha: hoy,
        descripcion: `${caso.cantidad} cobros en efectivo en ${params.dias_fraccionamiento} días suman ${fmtGs(caso.total)}; ninguno alcanza solo el umbral de ${fmtGs(umbral)}.`,
      });
      if (creada) n++;
    }
    if (n) this.logger.log(`Empresa ${empresa_id}: ${n} alerta(s) de fraccionamiento`);
    return n;
  }

  private armar<T extends { cliente_id: string | null; fecha: Date; ros_fecha: Date | null }>(
    i: T,
    clientes: Awaited<ReturnType<typeof datosClientes>>,
  ) {
    return { ...i, fecha: isoDia(i.fecha), ros_fecha: isoDia(i.ros_fecha), cliente: clienteDe(clientes, i.cliente_id) };
  }
}
