import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { aFecha, hoyPy, isoDia, redondear, transicionarUnidad } from '../common/inm-utils';
import { NumeracionInmService } from '../common/numeracion-inm.service';
import { proyectoObra } from '../obra/presupuesto-obra.service';
import {
  CreateAporteDto,
  CreateCanjeDto,
  CreateDistribucionDto,
  CreateInversorDto,
  MotivoInversoresDto,
  PagarLineaDto,
  UpdateInversorDto,
} from './dto/inversores.dto';
import { estadoCuentaInversor, repartirPorParticipacion } from './inversores-calculos';

const noFutura = (f: string, que: string) => {
  if (f.slice(0, 10) > hoyPy()) throw new BadRequestException(`La fecha ${que} no puede ser futura`);
};

/**
 * Inversores del proyecto (M123): aportes en dinero, terreno o servicios, unidades
 * entregadas en canje, distribuciones por participación y estado de cuenta.
 * Montos en la moneda del proyecto.
 *
 * Los pagos a inversores se hacen por Tesorería; acá se registra la referencia
 * (integración con órdenes de pago: pendiente, ver docs/plan-inmobiliario.md).
 */
@Injectable()
export class InversoresInmService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly numeracion: NumeracionInmService,
  ) {}

  // --------------------------------------------------------------- inversores

  /** Inversores del proyecto con su estado de cuenta y los totales. */
  async resumenProyecto(proyecto_id: string, empresa_id: string) {
    const proyecto = await proyectoObra(this.prisma, empresa_id, proyecto_id);
    const inversores = await this.prisma.inm_inversores.findMany({
      where: { proyecto_id, empresa_id, activo: true },
      include: {
        inm_aportes: { where: { estado: 'VIGENTE' }, select: { tipo: true, monto: true } },
        inm_canjes: { where: { estado: 'VIGENTE' }, select: { valor: true } },
        inm_distribucion_lineas: {
          where: { inm_distribuciones: { estado: 'APROBADA' } },
          select: { monto: true, pagado_at: true },
        },
      },
      orderBy: { nombre: 'asc' },
    });
    const filas = inversores.map(({ inm_aportes, inm_canjes, inm_distribucion_lineas, ...i }) => ({
      ...i,
      cuenta: estadoCuentaInversor(
        {
          aportes: inm_aportes.map((a) => ({ tipo: a.tipo, monto: Number(a.monto) })),
          canjes: inm_canjes.map((c) => ({ valor: Number(c.valor) })),
          distribuciones: inm_distribucion_lineas.map((d) => ({
            monto: Number(d.monto),
            pagado: Boolean(d.pagado_at),
          })),
        },
        proyecto.dec,
      ),
    }));
    const sum = (f: (x: (typeof filas)[number]) => number) =>
      redondear(
        filas.reduce((a, x) => a + f(x), 0),
        proyecto.dec,
      );
    return {
      proyecto: { id: proyecto.id, codigo: proyecto.codigo, nombre: proyecto.nombre },
      moneda: { codigo: proyecto.moneda_codigo },
      inversores: filas,
      totales: {
        participacion_pct: redondear(
          filas.reduce((a, x) => a + Number(x.participacion_pct), 0),
          4,
        ),
        aportado: sum((x) => x.cuenta.aportado),
        distribuido: sum((x) => x.cuenta.distribuido),
        pagado: sum((x) => x.cuenta.pagado),
        canjes: sum((x) => x.cuenta.canjes),
      },
    };
  }

  /** Estado de cuenta detallado de un inversor: aportes, canjes y distribuciones, con su saldo. */
  async estadoCuenta(id: string, empresa_id: string) {
    const i = await this.prisma.inm_inversores.findFirst({
      where: { id, empresa_id },
      include: {
        inm_aportes: { orderBy: { fecha: 'asc' } },
        inm_canjes: {
          orderBy: { fecha: 'asc' },
          include: { inm_unidades: { select: { id: true, codigo: true, tipo: true } } },
        },
        inm_distribucion_lineas: {
          include: {
            inm_distribuciones: { select: { id: true, numero: true, fecha: true, concepto: true, estado: true } },
          },
        },
      },
    });
    if (!i) throw new NotFoundException('Inversor no encontrado');
    const proyecto = await proyectoObra(this.prisma, empresa_id, i.proyecto_id);
    const aportes = i.inm_aportes.filter((a) => a.estado === 'VIGENTE');
    const canjes = i.inm_canjes.filter((c) => c.estado === 'VIGENTE');
    const lineas = i.inm_distribucion_lineas.filter((l) => l.inm_distribuciones.estado === 'APROBADA');
    const { inm_aportes, inm_canjes, inm_distribucion_lineas, ...inversor } = i;
    return {
      inversor,
      proyecto: { id: proyecto.id, codigo: proyecto.codigo, nombre: proyecto.nombre },
      moneda: { codigo: proyecto.moneda_codigo },
      cuenta: estadoCuentaInversor(
        {
          aportes: aportes.map((a) => ({ tipo: a.tipo, monto: Number(a.monto) })),
          canjes: canjes.map((c) => ({ valor: Number(c.valor) })),
          distribuciones: lineas.map((l) => ({ monto: Number(l.monto), pagado: Boolean(l.pagado_at) })),
        },
        proyecto.dec,
      ),
      aportes: inm_aportes.map((a) => ({ ...a, fecha: isoDia(a.fecha) })),
      canjes: inm_canjes.map((c) => ({ ...c, fecha: isoDia(c.fecha) })),
      distribuciones: inm_distribucion_lineas
        .filter((l) => l.inm_distribuciones.estado !== 'ANULADA')
        .map((l) => ({
          ...l,
          pagado_at: isoDia(l.pagado_at),
          inm_distribuciones: { ...l.inm_distribuciones, fecha: isoDia(l.inm_distribuciones.fecha) },
        })),
    };
  }

  async crear(dto: CreateInversorDto, empresa_id: string, user_id?: string) {
    await proyectoObra(this.prisma, empresa_id, dto.proyecto_id);
    await this.validarParticipacion(empresa_id, dto.proyecto_id, dto.participacion_pct);
    if (dto.proveedor_id) await this.validarProveedor(empresa_id, dto.proveedor_id);
    const i = await this.prisma.inm_inversores.create({
      data: {
        empresa_id,
        proyecto_id: dto.proyecto_id,
        nombre: dto.nombre.trim(),
        documento: dto.documento?.trim() || null,
        email: dto.email?.trim() || null,
        telefono: dto.telefono?.trim() || null,
        proveedor_id: dto.proveedor_id ?? null,
        participacion_pct: dto.participacion_pct,
        notas: dto.notas?.trim() || null,
        created_by: user_id ?? null,
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_inversores',
      entity_id: i.id,
      new_value: { nombre: i.nombre, participacion_pct: dto.participacion_pct },
    });
    return i;
  }

  async actualizar(id: string, dto: UpdateInversorDto, empresa_id: string, user_id?: string) {
    const i = await this.buscarInversor(id, empresa_id);
    if (dto.participacion_pct !== undefined)
      await this.validarParticipacion(empresa_id, i.proyecto_id, dto.participacion_pct, id);
    if (dto.proveedor_id) await this.validarProveedor(empresa_id, dto.proveedor_id);
    const r = await this.prisma.inm_inversores.update({
      where: { id },
      data: {
        ...(dto.nombre !== undefined && { nombre: dto.nombre.trim() }),
        ...(dto.documento !== undefined && { documento: dto.documento?.trim() || null }),
        ...(dto.email !== undefined && { email: dto.email?.trim() || null }),
        ...(dto.telefono !== undefined && { telefono: dto.telefono?.trim() || null }),
        ...(dto.proveedor_id !== undefined && { proveedor_id: dto.proveedor_id }),
        ...(dto.participacion_pct !== undefined && { participacion_pct: dto.participacion_pct }),
        ...(dto.notas !== undefined && { notas: dto.notas?.trim() || null }),
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_inversores',
      entity_id: id,
      old_value: { participacion_pct: Number(i.participacion_pct), nombre: i.nombre },
      new_value: dto as Record<string, unknown>,
    });
    return r;
  }

  async darDeBaja(id: string, empresa_id: string, user_id?: string) {
    const i = await this.buscarInversor(id, empresa_id);
    const [aportes, canjes, pendientes] = await Promise.all([
      this.prisma.inm_aportes.count({ where: { inversor_id: id, empresa_id, estado: 'VIGENTE' } }),
      this.prisma.inm_canjes.count({ where: { inversor_id: id, empresa_id, estado: 'VIGENTE' } }),
      this.prisma.inm_distribucion_lineas.count({
        where: { inversor_id: id, empresa_id, pagado_at: null, inm_distribuciones: { estado: { not: 'ANULADA' } } },
      }),
    ]);
    if (aportes || canjes || pendientes)
      throw new BadRequestException(
        'El inversor tiene aportes, canjes o distribuciones sin pagar: no se puede dar de baja',
      );
    await this.prisma.inm_inversores.updateMany({
      where: { id, empresa_id },
      data: { activo: false, updated_at: new Date() },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ELIMINAR',
      entity_type: 'inm_inversores',
      entity_id: id,
      old_value: { nombre: i.nombre },
    });
    return { ok: true };
  }

  // ------------------------------------------------------------------ aportes

  async crearAporte(dto: CreateAporteDto, empresa_id: string, user_id?: string) {
    const i = await this.buscarInversor(dto.inversor_id, empresa_id, true);
    noFutura(dto.fecha, 'del aporte');
    const a = await this.prisma.inm_aportes.create({
      data: {
        empresa_id,
        proyecto_id: i.proyecto_id,
        inversor_id: i.id,
        fecha: aFecha(dto.fecha),
        tipo: dto.tipo,
        monto: dto.monto,
        descripcion: dto.descripcion?.trim() || null,
        referencia: dto.referencia?.trim() || null,
        created_by: user_id ?? null,
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_aportes',
      entity_id: a.id,
      new_value: { inversor: i.nombre, tipo: dto.tipo, monto: dto.monto },
    });
    return { ...a, fecha: isoDia(a.fecha) };
  }

  async anularAporte(id: string, dto: MotivoInversoresDto, empresa_id: string, user_id?: string) {
    const a = await this.prisma.inm_aportes.findFirst({ where: { id, empresa_id } });
    if (!a) throw new NotFoundException('Aporte no encontrado');
    if (a.estado === 'ANULADO') throw new BadRequestException('El aporte ya está anulado');
    await this.prisma.inm_aportes.updateMany({
      where: { id, empresa_id },
      data: { estado: 'ANULADO', motivo_anulacion: dto.motivo.trim() },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ANULAR',
      entity_type: 'inm_aportes',
      entity_id: id,
      old_value: { monto: Number(a.monto), tipo: a.tipo },
      new_value: { motivo: dto.motivo },
    });
    return { ok: true };
  }

  // ------------------------------------------------------------------- canjes

  /** Unidad entregada al inversor en canje: queda bloqueada con el motivo, fuera de la venta. */
  async crearCanje(dto: CreateCanjeDto, empresa_id: string, user_id?: string) {
    const i = await this.buscarInversor(dto.inversor_id, empresa_id, true);
    noFutura(dto.fecha, 'del canje');
    const u = await this.prisma.inm_unidades.findFirst({
      where: { id: dto.unidad_id, empresa_id, activo: true },
      select: { id: true, codigo: true, proyecto_id: true, estado: true, version: true, precio_lista: true },
    });
    if (!u) throw new BadRequestException('La unidad no existe');
    if (u.proyecto_id !== i.proyecto_id) throw new BadRequestException('La unidad es de otro proyecto');
    if (u.estado !== 'DISPONIBLE' && u.estado !== 'BORRADOR')
      throw new ConflictException(`La unidad ${u.codigo} no está disponible (${u.estado.toLowerCase()})`);
    const valor = dto.valor ?? Number(u.precio_lista);
    if (!(valor > 0)) throw new BadRequestException('Indicá el valor del canje: la unidad no tiene precio de lista');
    const c = await this.prisma.$transaction(async (tx) => {
      await transicionarUnidad(tx, {
        id: u.id,
        empresa_id,
        desde: ['DISPONIBLE', 'BORRADOR'],
        hacia: 'BLOQUEADA',
        version: u.version,
        extra: { bloqueo_motivo: `Canje con el inversor ${i.nombre}`.slice(0, 255) },
      });
      return tx.inm_canjes.create({
        data: {
          empresa_id,
          proyecto_id: i.proyecto_id,
          inversor_id: i.id,
          unidad_id: u.id,
          fecha: aFecha(dto.fecha),
          valor,
          created_by: user_id ?? null,
        },
      });
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_canjes',
      entity_id: c.id,
      new_value: { inversor: i.nombre, unidad: u.codigo, valor },
    });
    return { ...c, fecha: isoDia(c.fecha) };
  }

  async anularCanje(id: string, dto: MotivoInversoresDto, empresa_id: string, user_id?: string) {
    const c = await this.prisma.inm_canjes.findFirst({ where: { id, empresa_id } });
    if (!c) throw new NotFoundException('Canje no encontrado');
    if (c.estado === 'ANULADO') throw new BadRequestException('El canje ya está anulado');
    await this.prisma.$transaction(async (tx) => {
      await tx.inm_canjes.updateMany({
        where: { id, empresa_id },
        data: { estado: 'ANULADO', motivo_anulacion: dto.motivo.trim() },
      });
      // Si la unidad sigue bloqueada por el canje, vuelve a la venta.
      await tx.inm_unidades.updateMany({
        where: {
          id: c.unidad_id,
          empresa_id,
          estado: 'BLOQUEADA',
          bloqueo_motivo: { startsWith: 'Canje con el inversor' },
        },
        data: { estado: 'DISPONIBLE', bloqueo_motivo: null, version: { increment: 1 }, updated_at: new Date() },
      });
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ANULAR',
      entity_type: 'inm_canjes',
      entity_id: id,
      new_value: { motivo: dto.motivo },
    });
    return { ok: true };
  }

  // ------------------------------------------------------------ distribuciones

  async listarDistribuciones(proyecto_id: string, empresa_id: string) {
    const proyecto = await proyectoObra(this.prisma, empresa_id, proyecto_id);
    const ds = await this.prisma.inm_distribuciones.findMany({
      where: { proyecto_id, empresa_id },
      include: {
        inm_distribucion_lineas: {
          include: { inm_inversores: { select: { id: true, nombre: true } } },
          orderBy: { monto: 'desc' },
        },
      },
      orderBy: [{ fecha: 'desc' }, { numero: 'desc' }],
    });
    return ds.map((d) => ({ ...this.armarDistribucion(d), moneda: { codigo: proyecto.moneda_codigo } }));
  }

  /** Crea la distribución en borrador, repartida según la participación vigente de cada inversor. */
  async crearDistribucion(dto: CreateDistribucionDto, empresa_id: string, user_id?: string) {
    const proyecto = await proyectoObra(this.prisma, empresa_id, dto.proyecto_id);
    noFutura(dto.fecha, 'de la distribución');
    const inversores = await this.prisma.inm_inversores.findMany({
      where: { proyecto_id: dto.proyecto_id, empresa_id, activo: true },
      select: { id: true, participacion_pct: true },
    });
    const reparto = repartirPorParticipacion(
      dto.monto_total,
      inversores.map((i) => ({ id: i.id, pct: Number(i.participacion_pct) })),
      proyecto.dec,
    );
    if (!reparto.lineas.length)
      throw new BadRequestException('El proyecto no tiene inversores con participación: no hay a quién distribuir');
    const d = await this.prisma.$transaction(async (tx) => {
      const numero = await this.numeracion.siguiente(empresa_id, 'DIS', tx);
      return tx.inm_distribuciones.create({
        data: {
          empresa_id,
          numero,
          proyecto_id: dto.proyecto_id,
          fecha: aFecha(dto.fecha),
          concepto: dto.concepto.trim(),
          monto_total: dto.monto_total,
          created_by: user_id ?? null,
          inm_distribucion_lineas: {
            create: reparto.lineas.map((l) => ({
              empresa_id,
              inversor_id: l.id,
              participacion_pct: l.pct,
              monto: l.monto,
            })),
          },
        },
      });
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_distribuciones',
      entity_id: d.id,
      new_value: { numero: d.numero, monto_total: dto.monto_total, para_inversores: reparto.para_inversores },
    });
    return this.detalleDistribucion(d.id, empresa_id);
  }

  async detalleDistribucion(id: string, empresa_id: string) {
    const d = await this.prisma.inm_distribuciones.findFirst({
      where: { id, empresa_id },
      include: {
        inm_distribucion_lineas: {
          include: { inm_inversores: { select: { id: true, nombre: true } } },
          orderBy: { monto: 'desc' },
        },
      },
    });
    if (!d) throw new NotFoundException('Distribución no encontrada');
    const proyecto = await proyectoObra(this.prisma, empresa_id, d.proyecto_id);
    return { ...this.armarDistribucion(d), moneda: { codigo: proyecto.moneda_codigo } };
  }

  async aprobarDistribucion(id: string, empresa_id: string, user_id?: string) {
    const r = await this.prisma.inm_distribuciones.updateMany({
      where: { id, empresa_id, estado: 'BORRADOR' },
      data: { estado: 'APROBADA', aprobado_por: user_id ?? null, aprobado_at: new Date(), updated_at: new Date() },
    });
    if (!r.count) throw new BadRequestException('Sólo se aprueba una distribución en borrador');
    await this.audit.log({ empresa_id, user_id, action: 'APROBAR', entity_type: 'inm_distribuciones', entity_id: id });
    return this.detalleDistribucion(id, empresa_id);
  }

  async anularDistribucion(id: string, dto: MotivoInversoresDto, empresa_id: string, user_id?: string) {
    const d = await this.prisma.inm_distribuciones.findFirst({
      where: { id, empresa_id },
      include: { inm_distribucion_lineas: { select: { pagado_at: true } } },
    });
    if (!d) throw new NotFoundException('Distribución no encontrada');
    if (d.estado === 'ANULADA') throw new BadRequestException('La distribución ya está anulada');
    if (d.inm_distribucion_lineas.some((l) => l.pagado_at))
      throw new BadRequestException('Hay pagos registrados: no se puede anular la distribución');
    await this.prisma.inm_distribuciones.updateMany({
      where: { id, empresa_id },
      data: { estado: 'ANULADA', motivo_anulacion: dto.motivo.trim(), updated_at: new Date() },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ANULAR',
      entity_type: 'inm_distribuciones',
      entity_id: id,
      old_value: { estado: d.estado },
      new_value: { motivo: dto.motivo },
    });
    return this.detalleDistribucion(id, empresa_id);
  }

  async pagarLinea(linea_id: string, dto: PagarLineaDto, empresa_id: string, user_id?: string) {
    const l = await this.prisma.inm_distribucion_lineas.findFirst({
      where: { id: linea_id, empresa_id },
      include: { inm_distribuciones: { select: { id: true, estado: true, fecha: true, numero: true } } },
    });
    if (!l) throw new NotFoundException('Línea no encontrada');
    if (l.inm_distribuciones.estado !== 'APROBADA')
      throw new BadRequestException('Sólo se registran pagos de una distribución aprobada');
    if (l.pagado_at) throw new BadRequestException('La línea ya está pagada');
    noFutura(dto.fecha, 'del pago');
    if (dto.fecha.slice(0, 10) < (isoDia(l.inm_distribuciones.fecha) ?? ''))
      throw new BadRequestException('El pago no puede ser anterior a la distribución');
    await this.prisma.inm_distribucion_lineas.updateMany({
      where: { id: linea_id, empresa_id },
      data: { pagado_at: aFecha(dto.fecha), referencia_pago: dto.referencia.trim() },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_distribucion_lineas',
      entity_id: linea_id,
      new_value: { distribucion: l.inm_distribuciones.numero, pagado: dto.fecha, referencia: dto.referencia },
    });
    return this.detalleDistribucion(l.inm_distribuciones.id, empresa_id);
  }

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

  private async buscarInversor(id: string, empresa_id: string, soloActivo = false) {
    const i = await this.prisma.inm_inversores.findFirst({
      where: { id, empresa_id, ...(soloActivo && { activo: true }) },
    });
    if (!i) throw new NotFoundException('Inversor no encontrado');
    return i;
  }

  private async validarParticipacion(empresa_id: string, proyecto_id: string, pct: number, excluir?: string) {
    const otros = await this.prisma.inm_inversores.aggregate({
      where: { proyecto_id, empresa_id, activo: true, ...(excluir && { id: { not: excluir } }) },
      _sum: { participacion_pct: true },
    });
    const suma = Number(otros._sum.participacion_pct ?? 0) + pct;
    if (suma > 100.0001)
      throw new BadRequestException(
        `Las participaciones del proyecto sumarían ${redondear(suma, 4)} %: no pueden pasar del 100 %`,
      );
  }

  private async validarProveedor(empresa_id: string, proveedor_id: string) {
    const p = await this.prisma.proveedores.findFirst({
      where: { id: proveedor_id, empresa_id },
      select: { id: true },
    });
    if (!p) throw new BadRequestException('El proveedor no existe en esta empresa');
  }

  private armarDistribucion<
    T extends {
      fecha: Date;
      monto_total: unknown;
      inm_distribucion_lineas: Array<{ monto: unknown; pagado_at: Date | null }>;
    },
  >(d: T) {
    const paraInversores = d.inm_distribucion_lineas.reduce((a, l) => a + Number(l.monto), 0);
    return {
      ...d,
      fecha: isoDia(d.fecha),
      para_inversores: redondear(paraInversores, 2),
      para_desarrolladora: redondear(Number(d.monto_total) - paraInversores, 2),
      inm_distribucion_lineas: d.inm_distribucion_lineas.map((l) => ({ ...l, pagado_at: isoDia(l.pagado_at) })),
    };
  }
}
