import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { aFecha, hoyPy, isoDia, redondear } from '../common/inm-utils';
import { PresupuestoObraInmService } from '../obra/presupuesto-obra.service';
import { EmitirInformeDto, GenerarInformeDto } from './dto/fideicomiso.dto';

const VIVAS = ['RESERVADA', 'BOLETO_FIRMADO', 'CANCELADA_TOTAL', 'ESCRITURADA', 'ENTREGADA'] as const;

/** Primer y último día del mes anterior a `hoy` (AAAA-MM-DD). */
export function mesAnterior(hoy: string) {
  const d = new Date(`${hoy.slice(0, 7)}-01T00:00:00.000Z`);
  d.setUTCDate(0);
  const hasta = d.toISOString().slice(0, 10);
  return { desde: `${hasta.slice(0, 7)}-01`, hasta };
}

/**
 * Informe periódico del proyecto para la fiduciaria y los inversores (M123): ventas,
 * ingresos (cobros y aportes), egresos (costos de obra y distribuciones pagadas),
 * avance de obra y cartera. Sirve para cualquier proyecto; la tarea mensual lo arma
 * en borrador sólo para los que tienen titular FIDEICOMISO.
 *
 * BORRADOR se puede recalcular; EMITIDO guarda la foto y ya no cambia.
 */
@Injectable()
export class FideicomisoInmService {
  private readonly logger = new Logger(FideicomisoInmService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly presupuesto: PresupuestoObraInmService,
  ) {}

  async listar(proyecto_id: string, empresa_id: string) {
    const filas = await this.prisma.inm_informes_fideicomiso.findMany({
      where: { proyecto_id, empresa_id },
      select: {
        id: true,
        desde: true,
        hasta: true,
        estado: true,
        emitido_at: true,
        created_at: true,
        updated_at: true,
      },
      orderBy: { desde: 'desc' },
    });
    return filas.map((f) => ({ ...f, desde: isoDia(f.desde), hasta: isoDia(f.hasta) }));
  }

  async detalle(id: string, empresa_id: string) {
    const f = await this.prisma.inm_informes_fideicomiso.findFirst({ where: { id, empresa_id } });
    if (!f) throw new NotFoundException('Informe no encontrado');
    return { ...f, desde: isoDia(f.desde), hasta: isoDia(f.hasta) };
  }

  /** Crea (o recalcula, si está en borrador) el informe del período. */
  async generar(proyecto_id: string, dto: GenerarInformeDto, empresa_id: string, user_id?: string) {
    const desde = dto.desde.slice(0, 10);
    const hasta = dto.hasta.slice(0, 10);
    if (hasta < desde) throw new BadRequestException('El período termina antes de empezar');
    if (desde > hoyPy()) throw new BadRequestException('El período no puede empezar en el futuro');
    const previo = await this.prisma.inm_informes_fideicomiso.findFirst({
      where: { proyecto_id, empresa_id, desde: aFecha(desde), hasta: aFecha(hasta) },
    });
    if (previo?.estado === 'EMITIDO') throw new ConflictException('El informe de ese período ya está emitido');
    const datos = (await this.calcular(proyecto_id, empresa_id, desde, hasta)) as unknown as Prisma.InputJsonValue;
    const r = previo
      ? await this.prisma.inm_informes_fideicomiso.update({
          where: { id: previo.id },
          data: { datos, updated_at: new Date() },
        })
      : await this.prisma.inm_informes_fideicomiso.create({
          data: {
            empresa_id,
            proyecto_id,
            desde: aFecha(desde),
            hasta: aFecha(hasta),
            datos,
            created_by: user_id ?? null,
          },
        });
    await this.audit.log({
      empresa_id,
      user_id,
      action: previo ? 'EDITAR' : 'CREAR',
      entity_type: 'inm_informes_fideicomiso',
      entity_id: r.id,
      new_value: { desde, hasta },
    });
    return this.detalle(r.id, empresa_id);
  }

  async emitir(id: string, dto: EmitirInformeDto, empresa_id: string, user_id?: string) {
    const f = await this.prisma.inm_informes_fideicomiso.findFirst({ where: { id, empresa_id } });
    if (!f) throw new NotFoundException('Informe no encontrado');
    if (f.estado === 'EMITIDO') throw new BadRequestException('El informe ya está emitido');
    // Se recalcula al emitir: lo emitido es lo que hay en ese momento.
    const datos = (await this.calcular(
      f.proyecto_id,
      empresa_id,
      isoDia(f.desde),
      isoDia(f.hasta),
    )) as unknown as Prisma.InputJsonValue;
    await this.prisma.inm_informes_fideicomiso.updateMany({
      where: { id, empresa_id, estado: 'BORRADOR' },
      data: {
        datos,
        estado: 'EMITIDO',
        observaciones: dto.observaciones?.trim() || null,
        emitido_por: user_id ?? null,
        emitido_at: new Date(),
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CONFIRMAR',
      entity_type: 'inm_informes_fideicomiso',
      entity_id: id,
      new_value: { desde: isoDia(f.desde), hasta: isoDia(f.hasta) },
    });
    return this.detalle(id, empresa_id);
  }

  /** Tarea mensual: borrador del mes anterior para cada proyecto en fideicomiso. Idempotente. */
  async borradoresMensuales(hoy = hoyPy()) {
    const { desde, hasta } = mesAnterior(hoy);
    const proyectos = await this.prisma.inm_proyectos.findMany({
      where: { titular_tipo: 'FIDEICOMISO', activo: true, estado: { not: 'CERRADO' } },
      select: { id: true, empresa_id: true, codigo: true },
    });
    let n = 0;
    for (const p of proyectos) {
      try {
        const ya = await this.prisma.inm_informes_fideicomiso.findFirst({
          where: {
            proyecto_id: p.id,
            empresa_id: p.empresa_id,
            desde: aFecha(desde),
            hasta: aFecha(hasta),
          },
          select: { id: true },
        });
        if (ya) continue;
        await this.generar(p.id, { desde, hasta }, p.empresa_id);
        n++;
      } catch (e) {
        this.logger.error(`Informe de fideicomiso ${p.codigo}: ${(e as Error).message}`);
      }
    }
    return n;
  }

  // ---------------------------------------------------------------- cálculo

  async calcular(proyecto_id: string, empresa_id: string, desde: string, hasta: string) {
    const d = aFecha(desde);
    const h = aFecha(hasta);
    const p = await this.prisma.inm_proyectos.findFirst({
      where: { id: proyecto_id, empresa_id },
      select: {
        id: true,
        codigo: true,
        nombre: true,
        tipo: true,
        titular_tipo: true,
        fiduciaria_nombre: true,
        fiduciaria_ruc: true,
        moneda: { select: { codigo: true } },
        cuenta_bancaria: { select: { nombre: true, numero_cuenta: true, moneda: true, saldo_actual: true } },
      },
    });
    if (!p) throw new NotFoundException('Proyecto no encontrado');
    const moneda = p.moneda?.codigo ?? 'PYG';
    const dec = moneda === 'PYG' ? 0 : 2;
    const monedas = new Map(
      (await this.prisma.moneda.findMany({ select: { id: true, codigo: true } })).map((m) => [m.id, m.codigo ?? '?']),
    );

    const [
      unidades,
      reservas,
      boletos,
      bajas,
      cobros,
      aportes,
      imputaciones,
      pagosInversores,
      hitos,
      cuotas,
      aportesAcum,
      distribuidoAcum,
      resumenObra,
    ] = await Promise.all([
      this.prisma.inm_unidades.groupBy({
        by: ['estado'],
        where: { proyecto_id, empresa_id, activo: true },
        _count: { _all: true },
        _sum: { precio_lista: true },
      }),
      this.prisma.inm_operaciones.count({ where: { proyecto_id, empresa_id, fecha_reserva: { gte: d, lte: h } } }),
      this.prisma.inm_operaciones.groupBy({
        by: ['moneda_id'],
        where: { proyecto_id, empresa_id, fecha_boleto: { gte: d, lte: h }, estado: { in: [...VIVAS] } },
        _count: { _all: true },
        _sum: { precio_final: true },
      }),
      this.prisma.inm_operaciones.count({
        where: {
          proyecto_id,
          empresa_id,
          estado: { in: ['DESISTIDA', 'RESCINDIDA'] },
          updated_at: { gte: d, lte: new Date(h.getTime() + 86_400_000) },
        },
      }),
      this.prisma.inm_cobros.groupBy({
        by: ['moneda_id'],
        where: { empresa_id, estado: 'EMITIDO', fecha: { gte: d, lte: h }, inm_operaciones: { proyecto_id } },
        _count: { _all: true },
        _sum: { total: true },
      }),
      this.prisma.inm_aportes.groupBy({
        by: ['tipo'],
        where: { proyecto_id, empresa_id, estado: 'VIGENTE', fecha: { gte: d, lte: h } },
        _sum: { monto: true },
      }),
      this.prisma.inm_imputaciones.findMany({
        where: { proyecto_id, empresa_id, fecha: { gte: d, lte: h } },
        select: { monto: true, inm_partidas: { select: { rubro: true } } },
      }),
      this.prisma.inm_distribucion_lineas.aggregate({
        where: { empresa_id, pagado_at: { gte: d, lte: h }, inm_distribuciones: { proyecto_id, estado: 'APROBADA' } },
        _count: { _all: true },
        _sum: { monto: true },
      }),
      this.prisma.inm_hitos.findMany({
        where: { proyecto_id, empresa_id, activo: true },
        select: { nombre: true, fecha_estimada: true, fecha_cumplido: true },
        orderBy: [{ orden: 'asc' }, { fecha_estimada: 'asc' }],
      }),
      this.prisma.inm_operacion_cuotas.findMany({
        where: {
          empresa_id,
          estado: { in: ['PENDIENTE', 'PARCIAL'] },
          inm_operaciones: { proyecto_id, estado: { in: [...VIVAS] } },
        },
        select: {
          monto: true,
          monto_cobrado: true,
          fecha_vencimiento: true,
          inm_operaciones: { select: { moneda_id: true } },
        },
      }),
      this.prisma.inm_aportes.aggregate({
        where: { proyecto_id, empresa_id, estado: 'VIGENTE', fecha: { lte: h } },
        _sum: { monto: true },
      }),
      this.prisma.inm_distribucion_lineas.aggregate({
        where: { empresa_id, inm_distribuciones: { proyecto_id, estado: 'APROBADA', fecha: { lte: h } } },
        _sum: { monto: true },
      }),
      this.presupuesto.resumen(proyecto_id, empresa_id),
    ]);

    const porRubro = new Map<string, number>();
    for (const i of imputaciones)
      porRubro.set(i.inm_partidas.rubro, (porRubro.get(i.inm_partidas.rubro) ?? 0) + Number(i.monto));
    const cartera = new Map<string, { saldo: number; vencido: number }>();
    for (const c of cuotas) {
      const m = monedas.get(c.inm_operaciones.moneda_id) ?? '?';
      const a = cartera.get(m) ?? { saldo: 0, vencido: 0 };
      const s = Number(c.monto) - Number(c.monto_cobrado);
      a.saldo += s;
      if (c.fecha_vencimiento && isoDia(c.fecha_vencimiento) <= hasta) a.vencido += s;
      cartera.set(m, a);
    }
    const r = (n: number, m = moneda) => redondear(n, m === 'PYG' ? 0 : 2);

    return {
      generado_at: new Date().toISOString(),
      periodo: { desde, hasta },
      proyecto: {
        codigo: p.codigo,
        nombre: p.nombre,
        tipo: p.tipo,
        titular_tipo: p.titular_tipo,
        fiduciaria_nombre: p.fiduciaria_nombre,
        fiduciaria_ruc: p.fiduciaria_ruc,
        moneda,
        cuenta_bancaria: p.cuenta_bancaria
          ? {
              nombre: p.cuenta_bancaria.nombre,
              numero: p.cuenta_bancaria.numero_cuenta,
              moneda: p.cuenta_bancaria.moneda,
              saldo_actual: Number(p.cuenta_bancaria.saldo_actual),
            }
          : null,
      },
      ventas: {
        unidades_por_estado: unidades.map((u) => ({
          estado: u.estado,
          cantidad: u._count._all,
          valor_lista: Number(u._sum.precio_lista ?? 0),
        })),
        total_unidades: unidades.reduce((a, u) => a + u._count._all, 0),
        reservas_periodo: reservas,
        boletos_periodo: boletos.map((b) => ({
          moneda: monedas.get(b.moneda_id) ?? '?',
          cantidad: b._count._all,
          monto: r(Number(b._sum.precio_final ?? 0), monedas.get(b.moneda_id)),
        })),
        bajas_periodo: bajas,
      },
      ingresos: {
        cobros_periodo: cobros.map((c) => ({
          moneda: monedas.get(c.moneda_id) ?? '?',
          cantidad: c._count._all,
          total: r(Number(c._sum.total ?? 0), monedas.get(c.moneda_id)),
        })),
        aportes_periodo: {
          total: r(aportes.reduce((a, x) => a + Number(x._sum.monto ?? 0), 0)),
          por_tipo: aportes.map((x) => ({ tipo: x.tipo, monto: r(Number(x._sum.monto ?? 0)) })),
        },
      },
      egresos: {
        costos_obra_periodo: {
          total: r(imputaciones.reduce((a, i) => a + Number(i.monto), 0)),
          por_rubro: [...porRubro]
            .map(([rubro, monto]) => ({ rubro, monto: r(monto) }))
            .sort((a, b) => b.monto - a.monto),
        },
        distribuciones_pagadas_periodo: {
          cantidad: pagosInversores._count._all,
          total: r(Number(pagosInversores._sum.monto ?? 0)),
        },
      },
      avance: {
        pct_obra: resumenObra.totales.pct_avance,
        presupuesto_total: resumenObra.totales.presupuestado,
        ejecutado_acumulado: resumenObra.totales.ejecutado,
        hitos: hitos.map((x) => ({
          nombre: x.nombre,
          fecha_estimada: isoDia(x.fecha_estimada),
          fecha_cumplido: isoDia(x.fecha_cumplido),
        })),
        hitos_cumplidos_periodo: hitos.filter((x) => {
          const f = isoDia(x.fecha_cumplido);
          return f && f >= desde && f <= hasta;
        }).length,
      },
      cartera: [...cartera].map(([m, a]) => ({ moneda: m, saldo: r(a.saldo, m), vencido: r(a.vencido, m) })),
      inversores: {
        aportado_acumulado: r(Number(aportesAcum._sum.monto ?? 0)),
        distribuido_acumulado: r(Number(distribuidoAcum._sum.monto ?? 0)),
      },
      decimales: dec,
    };
  }
}
