import { BadRequestException, Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common';
import { inm_legajo_estado, inm_nivel_riesgo, Prisma } from '@prisma/client';
import { randomUUID } from 'crypto';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { sumarMeses } from '../common/cronograma';
import { aFecha, clienteDe, datosClientes, hoyPy, isoDia, sumarDias } from '../common/inm-utils';
import { ParametrosInmService } from '../config/parametros.service';
import {
  BeneficiarioFinal,
  calcularRiesgo,
  conservarHasta,
  DatosLegajo,
  faltantesParaAprobar,
  riesgoFinal,
  vigenciaLegajo,
} from './cumplimiento-reglas';
import { GuardarLegajoDto, MotivoCumplimientoDto, SubirDocumentoLegajoDto } from './dto/cumplimiento.dto';
import { InmStorageService } from './inm-storage.service';
import { InusualesInmService } from './inusuales.service';

/** Para comparar datos de fondo: "" y null son lo mismo, y sin espacios de más. */
const normalizar = (v: unknown) => {
  if (v === undefined || v === null) return 'null';
  if (typeof v === 'string') return v.trim() === '' ? 'null' : JSON.stringify(v.trim());
  return JSON.stringify(v);
};

const MIMES = ['application/pdf', 'image/jpeg', 'image/png', 'image/webp'];
export const MAX_DOCUMENTO_BYTES = 10 * 1024 * 1024;

/**
 * Legajo de debida diligencia del cliente (M123). Uno por cliente: sirve para todas
 * sus compras. El boleto exige que cada comprador y cotitular lo tenga aprobado y sin
 * vencer (`inm_parametros.exigir_legajo_cliente`, por defecto sí).
 *
 * No se borra nada (archivo de 5 años): los documentos se anulan con motivo y los
 * cambios quedan en la auditoría. Distinto del legajo de la OPERACIÓN (F1), que
 * registra el origen de los fondos de esa compra puntual.
 */
@Injectable()
export class LegajosClienteInmService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly parametros: ParametrosInmService,
    private readonly storage: InmStorageService,
    private readonly inusuales: InusualesInmService,
  ) {}

  async listar(
    empresa_id: string,
    f: { estado?: inm_legajo_estado; riesgo?: inm_nivel_riesgo; vencidos?: boolean; q?: string },
  ) {
    const q = f.q?.trim();
    const filas = await this.prisma.inm_legajos.findMany({
      where: {
        empresa_id,
        ...(f.estado && { estado: f.estado }),
        ...(f.riesgo && { nivel_riesgo: f.riesgo }),
        ...(f.vencidos && { estado: 'APROBADO', vence: { lt: aFecha(hoyPy()) } }),
      },
      include: { _count: { select: { inm_legajo_documentos: { where: { anulado: false } } } } },
      orderBy: [{ updated_at: 'desc' }],
      take: 500,
    });
    const clientes = await datosClientes(
      this.prisma,
      empresa_id,
      filas.map((l) => l.cliente_id),
    );
    const hoy = hoyPy();
    return filas
      .map((l) => ({
        ...this.armar(l),
        documentos: l._count.inm_legajo_documentos,
        cliente: clienteDe(clientes, l.cliente_id),
        ...vigenciaLegajo(l, hoy),
      }))
      .filter(
        (l) => !q || `${l.cliente?.nombre ?? ''} ${l.cliente?.documento ?? ''}`.toLowerCase().includes(q.toLowerCase()),
      );
  }

  /** Legajo del cliente (o `legajo: null` si todavía no tiene), con documentos y antecedentes. */
  async delCliente(cliente_id: string, empresa_id: string) {
    const clientes = await datosClientes(this.prisma, empresa_id, [cliente_id]);
    if (!clientes.size) throw new NotFoundException('Cliente no encontrado en esta empresa');
    const legajo = await this.prisma.inm_legajos.findFirst({
      where: { empresa_id, cliente_id },
      include: { inm_legajo_documentos: { orderBy: { created_at: 'desc' } } },
    });
    const [operaciones, inusuales, ultimoCobro] = await Promise.all([
      this.prisma.inm_operaciones.findMany({
        where: {
          empresa_id,
          OR: [{ cliente_id }, { inm_operacion_titulares: { some: { cliente_id } } }],
        },
        select: { id: true, numero: true, estado: true, fecha_reserva: true, fecha_boleto: true },
        orderBy: { fecha_reserva: 'desc' },
      }),
      this.prisma.inm_inusuales.findMany({
        where: { empresa_id, cliente_id },
        select: { id: true, numero: true, fecha: true, regla: true, estado: true },
        orderBy: { fecha: 'desc' },
      }),
      this.prisma.inm_cobros.findFirst({
        where: { empresa_id, inm_operaciones: { cliente_id } },
        orderBy: { fecha: 'desc' },
        select: { fecha: true },
      }),
    ]);
    const fechas = [
      ...operaciones.map((o) => isoDia(o.fecha_boleto ?? o.fecha_reserva)),
      isoDia(ultimoCobro?.fecha),
    ].filter((x): x is string => Boolean(x));
    const ultima = fechas.sort().pop() ?? null;
    const datos = legajo ? this.aDatos(legajo) : null;
    return {
      cliente: clienteDe(clientes, cliente_id),
      legajo: legajo
        ? {
            ...this.armar(legajo),
            documentos: legajo.inm_legajo_documentos.map((d) => ({ ...d, vence: isoDia(d.vence) })),
            faltantes: faltantesParaAprobar(datos, legajo.inm_legajo_documentos),
          }
        : null,
      ...vigenciaLegajo(legajo, hoyPy()),
      operaciones: operaciones.map((o) => ({
        ...o,
        fecha_reserva: isoDia(o.fecha_reserva),
        fecha_boleto: isoDia(o.fecha_boleto),
      })),
      inusuales: inusuales.map((i) => ({ ...i, fecha: isoDia(i.fecha) })),
      ultima_actividad: ultima,
      conservar_hasta: conservarHasta(ultima),
    };
  }

  async guardar(cliente_id: string, dto: GuardarLegajoDto, empresa_id: string, user_id?: string) {
    const clientes = await datosClientes(this.prisma, empresa_id, [cliente_id]);
    if (!clientes.size) throw new NotFoundException('Cliente no encontrado en esta empresa');
    const previo = await this.prisma.inm_legajos.findFirst({ where: { empresa_id, cliente_id } });
    const datos: DatosLegajo = {
      tipo_persona: dto.tipo_persona ?? previo?.tipo_persona ?? 'FISICA',
      actividad: dto.actividad !== undefined ? dto.actividad : previo?.actividad,
      origen_fondos: dto.origen_fondos !== undefined ? dto.origen_fondos : previo?.origen_fondos,
      pep: dto.pep ?? previo?.pep ?? false,
      beneficiarios_finales:
        dto.beneficiarios_finales ?? ((previo?.beneficiarios_finales as unknown as BeneficiarioFinal[]) || []),
    };
    if (datos.pep && !(dto.pep_cargo ?? previo?.pep_cargo)?.trim() && !(dto.pep_vinculo ?? previo?.pep_vinculo)?.trim())
      throw new BadRequestException('Si es PEP, indicá el cargo o el vínculo con la persona expuesta');
    // El pedido a mano se guarda aparte: si se corrige un dato (p. ej. un PEP marcado
    // por error), el calculado baja; el manual sólo se quita mandando null.
    const manual = dto.nivel_riesgo !== undefined ? dto.nivel_riesgo : (previo?.nivel_riesgo_manual ?? null);
    const riesgo = riesgoFinal(calcularRiesgo(datos, await this.antecedentes(empresa_id, cliente_id)), manual);
    // Cambiar un dato de fondo de un legajo aprobado lo vuelve a revisión.
    const cambiaFondo =
      previo?.estado === 'APROBADO' &&
      (
        [
          'tipo_persona',
          'actividad',
          'origen_fondos',
          'pep',
          'pep_cargo',
          'pep_vinculo',
          'beneficiarios_finales',
        ] as const
      ).some((k) => dto[k] !== undefined && normalizar(dto[k]) !== normalizar(previo[k]));
    const data = {
      tipo_persona: datos.tipo_persona,
      actividad: datos.actividad?.trim() || null,
      ingreso_mensual: dto.ingreso_mensual !== undefined ? dto.ingreso_mensual : previo?.ingreso_mensual,
      ingreso_moneda: dto.ingreso_moneda !== undefined ? dto.ingreso_moneda : previo?.ingreso_moneda,
      origen_fondos: datos.origen_fondos?.trim() || null,
      pep: datos.pep,
      pep_cargo: dto.pep_cargo !== undefined ? dto.pep_cargo?.trim() || null : previo?.pep_cargo,
      pep_vinculo: dto.pep_vinculo !== undefined ? dto.pep_vinculo?.trim() || null : previo?.pep_vinculo,
      beneficiarios_finales: datos.beneficiarios_finales as unknown as Prisma.InputJsonValue,
      nivel_riesgo: riesgo,
      nivel_riesgo_manual: manual,
      observaciones: dto.observaciones !== undefined ? dto.observaciones?.trim() || null : previo?.observaciones,
      updated_at: new Date(),
      ...(cambiaFondo && { estado: 'PENDIENTE' as const, aprobado_por: null, aprobado_at: null, vence: null }),
    };
    const l = previo
      ? await this.prisma.inm_legajos.update({ where: { id: previo.id }, data })
      : await this.prisma.inm_legajos.create({
          data: { ...data, empresa_id, cliente_id, created_by: user_id ?? null },
        });
    await this.audit.log({
      empresa_id,
      user_id,
      action: previo ? 'EDITAR' : 'CREAR',
      entity_type: 'inm_legajos',
      entity_id: l.id,
      old_value: previo ? { estado: previo.estado, nivel_riesgo: previo.nivel_riesgo } : undefined,
      new_value: { ...dto, nivel_riesgo: riesgo, vuelve_a_revision: cambiaFondo } as Record<string, unknown>,
    });
    return this.delCliente(cliente_id, empresa_id);
  }

  async aprobar(cliente_id: string, empresa_id: string, user_id?: string) {
    const l = await this.prisma.inm_legajos.findFirst({
      where: { empresa_id, cliente_id },
      include: { inm_legajo_documentos: { select: { tipo: true, anulado: true } } },
    });
    if (!l) throw new NotFoundException('El cliente no tiene legajo');
    const faltan = faltantesParaAprobar(this.aDatos(l), l.inm_legajo_documentos);
    if (faltan.length) throw new UnprocessableEntityException(`Para aprobar el legajo falta ${faltan.join(', ')}`);
    const params = await this.parametros.obtener(empresa_id);
    const meses = l.nivel_riesgo === 'ALTO' ? params.meses_vigencia_legajo_alto : params.meses_vigencia_legajo;
    const vence = sumarMeses(hoyPy(), meses);
    await this.prisma.inm_legajos.updateMany({
      where: { id: l.id, empresa_id },
      data: {
        estado: 'APROBADO',
        aprobado_por: user_id ?? null,
        aprobado_at: new Date(),
        vence: aFecha(vence),
        updated_at: new Date(),
      },
    });
    if (l.pep || this.aDatos(l).beneficiarios_finales.some((b) => b.pep)) {
      // El oficial de cumplimiento tiene que ver a los PEP: debida diligencia reforzada.
      await this.inusuales.registrarAutomatica(empresa_id, {
        clave: `PEP:${l.id}`,
        regla: 'PEP',
        cliente_id,
        descripcion: `Cliente PEP${l.pep_cargo ? ` (${l.pep_cargo})` : ''}${l.pep_vinculo ? ` — ${l.pep_vinculo}` : ''}: corresponde debida diligencia reforzada y seguimiento de sus operaciones.`,
      });
    }
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'APROBAR',
      entity_type: 'inm_legajos',
      entity_id: l.id,
      new_value: { vence, nivel_riesgo: l.nivel_riesgo },
    });
    return this.delCliente(cliente_id, empresa_id);
  }

  async observar(cliente_id: string, dto: MotivoCumplimientoDto, empresa_id: string, user_id?: string) {
    const l = await this.prisma.inm_legajos.findFirst({ where: { empresa_id, cliente_id } });
    if (!l) throw new NotFoundException('El cliente no tiene legajo');
    await this.prisma.inm_legajos.updateMany({
      where: { id: l.id, empresa_id },
      data: {
        estado: 'OBSERVADO',
        observaciones: dto.motivo.trim(),
        aprobado_por: null,
        aprobado_at: null,
        vence: null,
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_legajos',
      entity_id: l.id,
      old_value: { estado: l.estado },
      new_value: { estado: 'OBSERVADO', motivo: dto.motivo },
    });
    return this.delCliente(cliente_id, empresa_id);
  }

  async subirDocumento(
    cliente_id: string,
    dto: SubirDocumentoLegajoDto,
    archivo: Express.Multer.File | undefined,
    empresa_id: string,
    user_id?: string,
  ) {
    if (!archivo?.buffer?.length) throw new BadRequestException('Adjuntá el archivo');
    if (!MIMES.includes(archivo.mimetype))
      throw new BadRequestException('El archivo tiene que ser PDF, JPG, PNG o WEBP');
    if (archivo.size > MAX_DOCUMENTO_BYTES) throw new BadRequestException('El archivo pasa de 10 MB');
    let l = await this.prisma.inm_legajos.findFirst({ where: { empresa_id, cliente_id } });
    if (!l) {
      // Subir un documento crea el legajo si todavía no existe.
      await this.guardar(cliente_id, {}, empresa_id, user_id);
      l = await this.prisma.inm_legajos.findFirst({ where: { empresa_id, cliente_id } });
    }
    if (!l) throw new NotFoundException('El cliente no tiene legajo');
    const id = randomUUID();
    const key = this.storage.clave(empresa_id, `legajos/${cliente_id}`, id, archivo.originalname);
    await this.storage.subir(key, archivo.buffer, archivo.mimetype);
    const d = await this.prisma.inm_legajo_documentos.create({
      data: {
        id,
        empresa_id,
        legajo_id: l.id,
        tipo: dto.tipo,
        nombre: archivo.originalname.slice(0, 200),
        storage_key: key,
        mime: archivo.mimetype,
        tamano: archivo.size,
        vence: dto.vence ? aFecha(dto.vence) : null,
        created_by: user_id ?? null,
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_legajo_documentos',
      entity_id: d.id,
      new_value: { tipo: d.tipo, nombre: d.nombre },
    });
    return { ...d, vence: isoDia(d.vence) };
  }

  async urlDocumento(id: string, empresa_id: string) {
    const d = await this.prisma.inm_legajo_documentos.findFirst({ where: { id, empresa_id } });
    if (!d) throw new NotFoundException('Documento no encontrado');
    return { url: await this.storage.urlFirmada(d.storage_key, d.nombre, d.mime), nombre: d.nombre, mime: d.mime };
  }

  async anularDocumento(id: string, dto: MotivoCumplimientoDto, empresa_id: string, user_id?: string) {
    const d = await this.prisma.inm_legajo_documentos.findFirst({ where: { id, empresa_id } });
    if (!d) throw new NotFoundException('Documento no encontrado');
    if (d.anulado) throw new BadRequestException('El documento ya está anulado');
    await this.prisma.inm_legajo_documentos.updateMany({
      where: { id, empresa_id },
      data: { anulado: true, motivo_anulacion: dto.motivo.trim() },
    });
    // Si el legajo estaba aprobado y sin este documento ya no cumple, vuelve a revisión.
    const l = await this.prisma.inm_legajos.findFirst({
      where: { id: d.legajo_id, empresa_id },
      include: { inm_legajo_documentos: { select: { tipo: true, anulado: true } } },
    });
    const faltan = l ? faltantesParaAprobar(this.aDatos(l), l.inm_legajo_documentos) : [];
    const vuelveARevision = l?.estado === 'APROBADO' && faltan.length > 0;
    if (vuelveARevision)
      await this.prisma.inm_legajos.updateMany({
        where: { id: d.legajo_id, empresa_id },
        data: { estado: 'PENDIENTE', aprobado_por: null, aprobado_at: null, vence: null, updated_at: new Date() },
      });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ANULAR',
      entity_type: 'inm_legajo_documentos',
      entity_id: id,
      new_value: { motivo: dto.motivo, legajo_vuelve_a_revision: vuelveARevision },
    });
    return {
      ok: true,
      aviso: vuelveARevision ? `El legajo volvió a revisión: falta ${faltan.join(', ')}` : null,
    };
  }

  /**
   * Estado de debida diligencia de cada comprador de una venta, para mostrarlo en la
   * operación (no exige permiso de cumplimiento: sólo dice si está vigente y por qué no).
   */
  async estadoClientes(empresa_id: string, cliente_ids: string[]) {
    const params = await this.parametros.obtener(empresa_id);
    const ids = [...new Set(cliente_ids)];
    const legajos = await this.prisma.inm_legajos.findMany({
      where: { empresa_id, cliente_id: { in: ids } },
      select: { cliente_id: true, estado: true, vence: true, nivel_riesgo: true },
    });
    const hoy = hoyPy();
    return {
      exigido: params.exigir_legajo_cliente,
      clientes: ids.map((id) => {
        const l = legajos.find((x) => x.cliente_id === id) ?? null;
        return {
          cliente_id: id,
          estado: l?.estado ?? null,
          vence: isoDia(l?.vence),
          nivel_riesgo: l?.nivel_riesgo ?? null,
          ...vigenciaLegajo(l, hoy),
        };
      }),
    };
  }

  /**
   * Para el boleto: compradores y cotitulares sin legajo vigente. Vacío = habilitado.
   * Si la empresa desactivó la exigencia, siempre vacío.
   */
  async impedimentosBoleto(empresa_id: string, cliente_ids: string[]) {
    const params = await this.parametros.obtener(empresa_id);
    if (!params.exigir_legajo_cliente) return [];
    const ids = [...new Set(cliente_ids)];
    const [legajos, clientes] = await Promise.all([
      this.prisma.inm_legajos.findMany({ where: { empresa_id, cliente_id: { in: ids } } }),
      datosClientes(this.prisma, empresa_id, ids),
    ]);
    const hoy = hoyPy();
    return ids
      .map((id) => ({ id, v: vigenciaLegajo(legajos.find((l) => l.cliente_id === id) ?? null, hoy) }))
      .filter((x) => !x.v.vigente)
      .map((x) => `${clientes.get(x.id)?.nombre ?? 'Cliente'}: ${x.v.motivo}`);
  }

  /** Tablero del oficial: legajos por estado, vencimientos y compradores activos sin legajo vigente. */
  async resumen(empresa_id: string) {
    const hoy = hoyPy();
    const en30 = sumarDias(hoy, 30);
    const [porEstado, vencidos, porVencer, abiertas, operaciones] = await Promise.all([
      this.prisma.inm_legajos.groupBy({ by: ['estado'], where: { empresa_id }, _count: { _all: true } }),
      this.prisma.inm_legajos.count({ where: { empresa_id, estado: 'APROBADO', vence: { lt: aFecha(hoy) } } }),
      this.prisma.inm_legajos.count({
        where: { empresa_id, estado: 'APROBADO', vence: { gte: aFecha(hoy), lte: aFecha(en30) } },
      }),
      this.prisma.inm_inusuales.count({ where: { empresa_id, estado: { in: ['PENDIENTE', 'EN_ANALISIS'] } } }),
      this.prisma.inm_operaciones.findMany({
        where: { empresa_id, estado: { in: ['RESERVADA', 'BOLETO_FIRMADO', 'CANCELADA_TOTAL', 'ESCRITURADA'] } },
        select: { id: true, numero: true, cliente_id: true, inm_operacion_titulares: { select: { cliente_id: true } } },
      }),
    ]);
    const ids = [
      ...new Set(operaciones.flatMap((o) => [o.cliente_id, ...o.inm_operacion_titulares.map((t) => t.cliente_id)])),
    ];
    const legajos = await this.prisma.inm_legajos.findMany({
      where: { empresa_id, cliente_id: { in: ids } },
      select: { cliente_id: true, estado: true, vence: true },
    });
    const sinVigente = ids.filter(
      (id) => !vigenciaLegajo(legajos.find((l) => l.cliente_id === id) ?? null, hoy).vigente,
    );
    const clientes = await datosClientes(this.prisma, empresa_id, sinVigente.slice(0, 50));
    return {
      legajos: Object.fromEntries(porEstado.map((e) => [e.estado, e._count._all])),
      vencidos,
      por_vencer_30_dias: porVencer,
      inusuales_abiertas: abiertas,
      compradores_sin_legajo_vigente: sinVigente.length,
      compradores_sin_legajo: sinVigente.slice(0, 50).map((id) => ({
        ...clienteDe(clientes, id),
        id,
        operaciones: operaciones
          .filter((o) => o.cliente_id === id || o.inm_operacion_titulares.some((t) => t.cliente_id === id))
          .map((o) => o.numero),
        motivo: vigenciaLegajo(legajos.find((l) => l.cliente_id === id) ?? null, hoy).motivo,
      })),
    };
  }

  // ---------------------------------------------------------------- auxiliares

  private async antecedentes(empresa_id: string, cliente_id: string) {
    const [reportadas, abiertas] = await Promise.all([
      this.prisma.inm_inusuales.count({ where: { empresa_id, cliente_id, estado: 'REPORTADA' } }),
      this.prisma.inm_inusuales.count({
        where: { empresa_id, cliente_id, estado: { in: ['PENDIENTE', 'EN_ANALISIS'] }, regla: { not: 'PEP' } },
      }),
    ]);
    return { inusuales_reportadas: reportadas, inusuales_abiertas: abiertas };
  }

  private aDatos(l: {
    tipo_persona: 'FISICA' | 'JURIDICA';
    actividad: string | null;
    origen_fondos: string | null;
    pep: boolean;
    beneficiarios_finales: Prisma.JsonValue;
  }): DatosLegajo {
    return {
      tipo_persona: l.tipo_persona,
      actividad: l.actividad,
      origen_fondos: l.origen_fondos,
      pep: l.pep,
      beneficiarios_finales: (Array.isArray(l.beneficiarios_finales)
        ? l.beneficiarios_finales
        : []) as unknown as BeneficiarioFinal[],
    };
  }

  private armar<T extends { vence: Date | null; ingreso_mensual: unknown }>(l: T) {
    return { ...l, vence: isoDia(l.vence) };
  }
}
