import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { CronogramaError, generarCronograma, TramoPlan } from '../common/cronograma';
import { decimalesDe, hoyPy } from '../common/inm-utils';
import { CreatePlanDto, SimularPlanDto, TramoDto, UpdatePlanDto } from './dto/plan.dto';

/** Tramos de Prisma (Decimal) → tramos del generador (number). */
export const aTramosPlan = (tramos: Array<Record<string, unknown>>): TramoPlan[] =>
  tramos.map((t) => ({
    orden: Number(t.orden ?? 0),
    tipo: t.tipo as TramoPlan['tipo'],
    base: t.base as TramoPlan['base'],
    valor: Number(t.valor ?? 0),
    cantidad: Number(t.cantidad ?? 1),
    periodicidad_meses: Number(t.periodicidad_meses ?? 1),
    inicio_meses: Number(t.inicio_meses ?? 0),
    hito_id: (t.hito_id as string | null) ?? null,
    descripcion: (t.descripcion as string | null) ?? null,
  }));

/**
 * Planes de pago por proyecto (M113): plantillas de tramos que el cotizador aplica
 * sobre el precio de una unidad. Editar un plan no cambia cotizaciones ni ventas
 * ya hechas: esas guardan una foto de los tramos.
 */
@Injectable()
export class PlanesPagoInmService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
  ) {}

  listar(empresa_id: string, proyecto_id: string, incluir_inactivos = false) {
    return this.prisma.inm_planes_pago.findMany({
      where: { empresa_id, proyecto_id, ...(incluir_inactivos ? {} : { activo: true }) },
      include: { inm_plan_pago_tramos: { orderBy: { orden: 'asc' } } },
      orderBy: [{ orden: 'asc' }, { nombre: 'asc' }],
    });
  }

  async detalle(id: string, empresa_id: string) {
    const plan = await this.prisma.inm_planes_pago.findFirst({
      where: { id, empresa_id },
      include: { inm_plan_pago_tramos: { orderBy: { orden: 'asc' } } },
    });
    if (!plan) throw new NotFoundException('Plan de pago no encontrado');
    return plan;
  }

  async crear(dto: CreatePlanDto, empresa_id: string, user_id?: string) {
    await this.validar(empresa_id, dto.proyecto_id, dto.tramos);
    try {
      const plan = await this.prisma.inm_planes_pago.create({
        data: {
          empresa_id,
          proyecto_id: dto.proyecto_id,
          nombre: dto.nombre.trim(),
          descripcion: dto.descripcion?.trim() || null,
          orden: dto.orden ?? 0,
          created_by: user_id ?? null,
          inm_plan_pago_tramos: { create: this.tramosData(dto.tramos, empresa_id) },
        },
        include: { inm_plan_pago_tramos: { orderBy: { orden: 'asc' } } },
      });
      await this.audit.log({
        empresa_id,
        user_id,
        action: 'CREAR',
        entity_type: 'inm_planes_pago',
        entity_id: plan.id,
        new_value: plan,
      });
      return plan;
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
        throw new ConflictException(`Ya hay un plan llamado "${dto.nombre}" en el proyecto`);
      }
      throw e;
    }
  }

  /** Reemplaza los tramos enteros: un plan es una lista corta y se edita como un todo. */
  async actualizar(id: string, dto: UpdatePlanDto, empresa_id: string, user_id?: string) {
    const anterior = await this.detalle(id, empresa_id);
    if (dto.tramos) await this.validar(empresa_id, anterior.proyecto_id, dto.tramos);
    try {
      const plan = await this.prisma.$transaction(async (tx) => {
        if (dto.tramos) {
          await tx.inm_plan_pago_tramos.deleteMany({ where: { plan_id: id } });
          await tx.inm_plan_pago_tramos.createMany({
            data: this.tramosData(dto.tramos, empresa_id).map((t) => ({ ...t, plan_id: id })),
          });
        }
        return tx.inm_planes_pago.update({
          where: { id },
          data: {
            ...(dto.nombre !== undefined && { nombre: dto.nombre.trim() }),
            ...(dto.descripcion !== undefined && { descripcion: dto.descripcion?.trim() || null }),
            ...(dto.orden !== undefined && { orden: dto.orden }),
            updated_at: new Date(),
          },
          include: { inm_plan_pago_tramos: { orderBy: { orden: 'asc' } } },
        });
      });
      await this.audit.log({
        empresa_id,
        user_id,
        action: 'EDITAR',
        entity_type: 'inm_planes_pago',
        entity_id: id,
        old_value: anterior,
        new_value: plan,
      });
      return plan;
    } catch (e) {
      if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
        throw new ConflictException(`Ya hay otro plan llamado "${dto.nombre}" en el proyecto`);
      }
      throw e;
    }
  }

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

  /** Cronograma de prueba: lo usa el editor de planes y el cotizador antes de guardar. */
  async simular(dto: SimularPlanDto, empresa_id: string) {
    let tramos: TramoPlan[];
    if (dto.plan_id) tramos = aTramosPlan((await this.detalle(dto.plan_id, empresa_id)).inm_plan_pago_tramos);
    else if (dto.tramos?.length) tramos = aTramosPlan(dto.tramos as unknown as Array<Record<string, unknown>>);
    else throw new BadRequestException('Indicá un plan o sus tramos');
    try {
      const cuotas = generarCronograma(
        dto.precio,
        tramos,
        dto.fecha_base?.slice(0, 10) ?? hoyPy(),
        decimalesDe(dto.moneda_codigo),
      );
      return { cuotas, total: dto.precio, cantidad: cuotas.length };
    } catch (e) {
      if (e instanceof CronogramaError) throw new BadRequestException(e.message);
      throw e;
    }
  }

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

  private tramosData(tramos: TramoDto[], empresa_id: string) {
    return tramos.map((t, i) => ({
      empresa_id,
      orden: t.orden ?? i + 1,
      tipo: t.tipo,
      base: t.base,
      valor: t.base === 'SALDO' ? 0 : t.valor,
      cantidad: t.cantidad ?? 1,
      periodicidad_meses: t.periodicidad_meses ?? 1,
      inicio_meses: t.inicio_meses ?? 0,
      hito_id: t.tipo === 'HITO' ? (t.hito_id ?? null) : null,
      descripcion: t.descripcion?.trim() || null,
    }));
  }

  /** Validación estructural; los montos se validan contra un precio real al cotizar. */
  private async validar(empresa_id: string, proyecto_id: string, tramos: TramoDto[]) {
    const proyecto = await this.prisma.inm_proyectos.findFirst({
      where: { id: proyecto_id, empresa_id },
      select: { id: true },
    });
    if (!proyecto) throw new NotFoundException('Proyecto no encontrado');
    if (tramos.filter((t) => t.base === 'SALDO').length > 1)
      throw new BadRequestException('Sólo puede haber un tramo "saldo"');
    const pct = tramos.filter((t) => t.base === 'PORCENTAJE').reduce((a, t) => a + Number(t.valor), 0);
    if (pct > 100) throw new BadRequestException(`Los porcentajes suman ${pct} %: no pueden pasar del 100 %`);
    const hitos = tramos.filter((t) => t.tipo === 'HITO');
    if (hitos.some((t) => !t.hito_id)) throw new BadRequestException('Cada tramo contra hito necesita su hito de obra');
    if (hitos.length) {
      const ids = [...new Set(hitos.map((t) => t.hito_id))];
      const n = await this.prisma.inm_hitos.count({
        where: { id: { in: ids }, proyecto_id, empresa_id, activo: true },
      });
      if (n !== ids.length) throw new BadRequestException('Hay un hito que no pertenece a este proyecto');
    }
    if (
      !tramos.some((t) => t.base === 'SALDO') &&
      tramos.every((t) => t.base === 'PORCENTAJE') &&
      Math.abs(pct - 100) > 1e-9
    ) {
      throw new BadRequestException(
        `Los porcentajes suman ${pct} %. Tienen que sumar 100 % o tiene que haber un tramo "saldo".`,
      );
    }
  }
}
