import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { inm_comision_estado, Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { decimalesDe, redondear } from '../common/inm-utils';
import {
  AsignarBrokerDto,
  CreateEsquemaDto,
  EventoComision,
  HitoComisionDto,
  LiquidarComisionesDto,
  UpdateEsquemaDto,
} from './dto/comision.dto';

type Cliente = PrismaService | Prisma.TransactionClient;

/** Qué hitos de comisión alcanzó una venta. Pura, para testearla sola. */
export function hitosAlcanzados(
  hitos: HitoComisionDto[],
  op: {
    estado: string;
    fecha_boleto: Date | null;
    fecha_escritura: Date | null;
    fecha_entrega: Date | null;
    pct_pagado: number;
  },
): number[] {
  if (op.estado === 'DESISTIDA' || op.estado === 'RESCINDIDA') return [];
  const ok = (h: HitoComisionDto) => {
    switch (h.evento) {
      case 'RESERVA':
        return true;
      case 'BOLETO':
        return Boolean(op.fecha_boleto);
      case 'COBRADO':
        return op.pct_pagado + 1e-9 >= Number(h.umbral ?? 100);
      case 'ESCRITURA':
        return Boolean(op.fecha_escritura);
      case 'ENTREGA':
        return Boolean(op.fecha_entrega);
      default:
        return false;
    }
  };
  return hitos.map((h, i) => (ok(h) ? i : -1)).filter((i) => i >= 0);
}

/**
 * Comisiones de la venta (M119). Un esquema define el % del precio para un rol
 * (vendedor de la empresa o inmobiliaria externa) y en qué hitos se devenga.
 *
 * `devengar` es idempotente (índice único por operación + esquema + hito): se llama
 * después de cada evento de la venta y en la tarea diaria, sin miedo a duplicar.
 *
 * Fuera de esta fase (tocan módulos compartidos, ver docs/plan-inmobiliario.md):
 * pasar las de vendedores empleados a la liquidación de RRHH y generar la cuenta
 * por pagar de las inmobiliarias externas. Hoy la liquidación se registra a mano.
 */
@Injectable()
export class ComisionesInmService {
  private readonly logger = new Logger(ComisionesInmService.name);

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

  // ----------------------------------------------------------------- esquemas

  listarEsquemas(empresa_id: string, proyecto_id?: string) {
    return this.prisma.inm_comision_esquemas.findMany({
      where: { empresa_id, activo: true, ...(proyecto_id && { OR: [{ proyecto_id }, { proyecto_id: null }] }) },
      include: { inm_proyectos: { select: { id: true, codigo: true, nombre: true } } },
      orderBy: [{ proyecto_id: 'asc' }, { nombre: 'asc' }],
    });
  }

  async crearEsquema(dto: CreateEsquemaDto, empresa_id: string, user_id?: string) {
    await this.validarEsquema(dto, empresa_id);
    const e = await this.prisma.inm_comision_esquemas.create({
      data: {
        empresa_id,
        proyecto_id: dto.proyecto_id ?? null,
        nombre: dto.nombre.trim(),
        rol: dto.rol,
        pct: dto.pct,
        hitos: dto.hitos as unknown as object,
        revertir_si_rescinde: dto.revertir_si_rescinde ?? true,
        created_by: user_id ?? null,
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_comision_esquemas',
      entity_id: e.id,
      new_value: e,
    });
    return e;
  }

  async actualizarEsquema(id: string, dto: UpdateEsquemaDto, empresa_id: string, user_id?: string) {
    const anterior = await this.esquemaDeEmpresa(id, empresa_id);
    await this.validarEsquema(
      { ...anterior, ...dto, hitos: dto.hitos ?? (anterior.hitos as unknown as HitoComisionDto[]) } as CreateEsquemaDto,
      empresa_id,
    );
    const e = await this.prisma.inm_comision_esquemas.update({
      where: { id },
      data: {
        ...(dto.nombre !== undefined && { nombre: dto.nombre.trim() }),
        ...(dto.proyecto_id !== undefined && { proyecto_id: dto.proyecto_id || null }),
        ...(dto.rol !== undefined && { rol: dto.rol }),
        ...(dto.pct !== undefined && { pct: dto.pct }),
        ...(dto.hitos !== undefined && { hitos: dto.hitos as unknown as object }),
        ...(dto.revertir_si_rescinde !== undefined && { revertir_si_rescinde: dto.revertir_si_rescinde }),
        updated_at: new Date(),
      },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_comision_esquemas',
      entity_id: id,
      old_value: anterior,
      new_value: e,
    });
    return e;
  }

  async darDeBajaEsquema(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.esquemaDeEmpresa(id, empresa_id);
    const e = await this.prisma.inm_comision_esquemas.update({
      where: { id },
      data: { activo: false, updated_at: new Date() },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ELIMINAR',
      entity_type: 'inm_comision_esquemas',
      entity_id: id,
      old_value: anterior,
    });
    return e;
  }

  // ---------------------------------------------------------------- devengos

  /** Devenga los hitos alcanzados de una venta. Idempotente. */
  async devengar(operacion_id: string, empresa_id: string, cliente: Cliente = this.prisma) {
    const op = await cliente.inm_operaciones.findFirst({
      where: { id: operacion_id, empresa_id },
      include: {
        moneda: { select: { codigo: true } },
        inm_operacion_cuotas: { select: { monto_cobrado: true, estado: true } },
      },
    });
    if (!op) return 0;
    const cobrado = op.inm_operacion_cuotas
      .filter((c) => c.estado !== 'ANULADA')
      .reduce((a, c) => a + Number(c.monto_cobrado), 0);
    const precio = Number(op.precio_final);
    const pct_pagado = precio > 0 ? (cobrado / precio) * 100 : 0;
    const esquemas = await cliente.inm_comision_esquemas.findMany({
      where: { empresa_id, activo: true, OR: [{ proyecto_id: op.proyecto_id }, { proyecto_id: null }] },
    });
    const dec = decimalesDe(op.moneda?.codigo);
    const beneficiario = await this.beneficiarios(cliente, empresa_id, op.vendedor_id, op.broker_proveedor_id);
    const filas: Prisma.inm_comisionesCreateManyInput[] = [];
    for (const e of esquemas) {
      const ben = e.rol === 'VENDEDOR' ? beneficiario.vendedor : beneficiario.broker;
      if (!ben) continue;
      const hitos = e.hitos as unknown as HitoComisionDto[];
      for (const i of hitosAlcanzados(hitos, { ...op, pct_pagado })) {
        filas.push({
          empresa_id,
          operacion_id: op.id,
          esquema_id: e.id,
          hito_idx: i,
          evento: hitos[i].evento,
          rol: e.rol,
          usuario_id: e.rol === 'VENDEDOR' ? op.vendedor_id : null,
          proveedor_id: e.rol === 'BROKER' ? op.broker_proveedor_id : null,
          beneficiario_nombre: ben,
          base: precio,
          monto: redondear((precio * Number(e.pct) * Number(hitos[i].porcion)) / 10000, dec),
          moneda_id: op.moneda_id,
        });
      }
    }
    if (!filas.length) return 0;
    const r = await cliente.inm_comisiones.createMany({ data: filas, skipDuplicates: true });
    if (r.count) this.logger.log(`Comisiones devengadas ${op.numero}: ${r.count}`);
    return r.count;
  }

  /** Rescisión o desistimiento: revierte lo devengado y no liquidado de los esquemas que lo piden. */
  async revertir(operacion_id: string, empresa_id: string, motivo: string, cliente: Cliente = this.prisma) {
    const r = await cliente.inm_comisiones.updateMany({
      where: { operacion_id, empresa_id, estado: 'DEVENGADA', inm_comision_esquemas: { revertir_si_rescinde: true } },
      data: { estado: 'REVERTIDA', revertida_motivo: motivo.slice(0, 255) },
    });
    return r.count;
  }

  listar(empresa_id: string, f: { proyecto_id?: string; estado?: string; operacion_id?: string }) {
    const estado = Object.values(inm_comision_estado).includes(f.estado as inm_comision_estado)
      ? (f.estado as inm_comision_estado)
      : undefined;
    return this.prisma.inm_comisiones.findMany({
      where: {
        empresa_id,
        ...(estado && { estado }),
        ...(f.operacion_id && { operacion_id: f.operacion_id }),
        ...(f.proyecto_id && { inm_operaciones: { proyecto_id: f.proyecto_id } }),
      },
      include: {
        moneda: { select: { codigo: true } },
        inm_comision_esquemas: { select: { nombre: true, pct: true } },
        inm_operaciones: {
          select: {
            id: true,
            numero: true,
            inm_unidades: { select: { codigo: true } },
            inm_proyectos: { select: { nombre: true } },
          },
        },
      },
      orderBy: { devengada_at: 'desc' },
      take: 1000,
    });
  }

  async liquidar(dto: LiquidarComisionesDto, empresa_id: string, user_id?: string) {
    const r = await this.prisma.inm_comisiones.updateMany({
      where: { id: { in: dto.ids }, empresa_id, estado: 'DEVENGADA' },
      data: {
        estado: 'LIQUIDADA',
        liquidada_at: new Date(),
        liquidacion_ref: dto.referencia.trim(),
        liquidada_por: user_id ?? null,
      },
    });
    if (r.count !== dto.ids.length) {
      this.logger.warn(
        `Liquidación: pedidas ${dto.ids.length}, liquidadas ${r.count} (las otras no estaban devengadas)`,
      );
    }
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CONFIRMAR',
      entity_type: 'inm_comisiones',
      entity_id: dto.ids[0],
      new_value: { ids: dto.ids, referencia: dto.referencia, liquidadas: r.count },
    });
    return { liquidadas: r.count, ignoradas: dto.ids.length - r.count };
  }

  async asignarBroker(operacion_id: string, dto: AsignarBrokerDto, empresa_id: string, user_id?: string) {
    const op = await this.prisma.inm_operaciones.findFirst({ where: { id: operacion_id, empresa_id } });
    if (!op) throw new NotFoundException('Operación no encontrada');
    if (dto.proveedor_id) {
      const p = await this.prisma.proveedores.findFirst({
        where: { id: dto.proveedor_id, empresa_id },
        select: { id: true },
      });
      if (!p) throw new BadRequestException('El proveedor no existe en esta empresa');
    }
    const liquidadas = await this.prisma.inm_comisiones.count({
      where: { operacion_id, rol: 'BROKER', estado: 'LIQUIDADA' },
    });
    if (liquidadas) throw new BadRequestException('La comisión de la inmobiliaria ya se liquidó: no se puede cambiar');
    await this.prisma.$transaction(async (tx) => {
      await tx.inm_operaciones.update({
        where: { id: operacion_id },
        data: { broker_proveedor_id: dto.proveedor_id ?? null, updated_at: new Date() },
      });
      // Lo devengado al broker anterior (todavía no liquidado) se borra para que el
      // índice único deje devengar al nuevo. El cambio queda en la auditoría.
      await tx.inm_comisiones.deleteMany({ where: { operacion_id, rol: 'BROKER', estado: 'DEVENGADA' } });
    });
    await this.devengar(operacion_id, empresa_id);
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'inm_operaciones',
      entity_id: operacion_id,
      old_value: { broker_proveedor_id: op.broker_proveedor_id },
      new_value: { broker_proveedor_id: dto.proveedor_id ?? null },
    });
    return { ok: true };
  }

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

  private async beneficiarios(
    cliente: Cliente,
    empresa_id: string,
    vendedor_id: string | null,
    proveedor_id: string | null,
  ) {
    const [u, p] = await Promise.all([
      vendedor_id
        ? cliente.usuario.findUnique({
            where: { id: vendedor_id },
            select: { username: true, nombres: true, apellidos: true },
          })
        : null,
      proveedor_id
        ? cliente.proveedores.findFirst({
            where: { id: proveedor_id, empresa_id },
            select: { personas: { select: { razon_social: true } } },
          })
        : null,
    ]);
    return {
      vendedor: u ? [u.nombres, u.apellidos].filter(Boolean).join(' ') || u.username || 'Vendedor' : null,
      broker: p ? (p.personas?.razon_social ?? 'Inmobiliaria') : null,
    };
  }

  private async validarEsquema(dto: CreateEsquemaDto, empresa_id: string) {
    if (dto.proyecto_id) {
      const p = await this.prisma.inm_proyectos.findFirst({
        where: { id: dto.proyecto_id, empresa_id },
        select: { id: true },
      });
      if (!p) throw new BadRequestException('El proyecto no existe en esta empresa');
    }
    const suma = dto.hitos.reduce((a, h) => a + Number(h.porcion), 0);
    if (Math.abs(suma - 100) > 0.001)
      throw new BadRequestException(`Las partes de los hitos suman ${suma} %: tienen que sumar 100 %`);
    if (dto.hitos.some((h) => h.evento === 'COBRADO' && !h.umbral))
      throw new BadRequestException('Un hito "cobrado" necesita el % de cobro que lo dispara');
    const eventos = dto.hitos.map((h) => `${h.evento}${h.evento === 'COBRADO' ? h.umbral : ''}`);
    if (new Set(eventos).size !== eventos.length) throw new BadRequestException('Hay hitos repetidos');
  }

  private async esquemaDeEmpresa(id: string, empresa_id: string) {
    const e = await this.prisma.inm_comision_esquemas.findFirst({ where: { id, empresa_id } });
    if (!e) throw new NotFoundException('Esquema no encontrado');
    return e;
  }
}

export type { EventoComision };
