import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { inm_cotizacion_estado } 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 {
  aFecha,
  datosClientes,
  decimalesDe,
  hoyPy,
  redondear,
  sumarDias,
  tienePrivilegio,
  clienteDe,
} from '../common/inm-utils';
import { NumeracionInmService } from '../common/numeracion-inm.service';
import { ParametrosInmService } from '../config/parametros.service';
import { aTramosPlan } from '../planes/planes.service';
import { CreateCotizacionDto, OpcionCotizacionDto } from './dto/cotizacion.dto';

export const PRIVILEGIO_DESCUENTO = 'INM_COT_DESCUENTO_AUTORIZAR';

/** Opción ya resuelta: tramos, precio final y cronograma estimado. */
export interface OpcionResuelta {
  plan_id: string | null;
  nombre: string;
  descuento_pct: number;
  precio_final: number;
  tramos: TramoPlan[];
  cronograma: ReturnType<typeof generarCronograma>;
  descuento_autorizado_por: string | null;
}

/**
 * Cotizador (M113). Una cotización compara hasta tres planes sobre una unidad
 * disponible. Guarda la foto de los tramos y del cronograma: si después cambia el
 * plan del proyecto o el precio de lista, lo cotizado no cambia.
 *
 * El cronograma de la cotización es estimado (fecha base = hoy). Las fechas
 * reales se fijan al firmar el boleto.
 */
@Injectable()
export class CotizacionesInmService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly numeracion: NumeracionInmService,
    private readonly parametros: ParametrosInmService,
  ) {}

  /**
   * Resuelve una opción contra una unidad: tramos (del plan o a medida), precio con
   * descuento y cronograma. Un descuento por encima del tope exige el privilegio
   * de autorizar descuentos. Lo usan la cotización y la reserva directa.
   */
  async resolverOpcion(
    empresa_id: string,
    unidad: { proyecto_id: string; precio_lista: unknown; moneda_codigo: string },
    op: OpcionCotizacionDto,
    user_id: string | undefined,
    reglas: { pctDescuentoMax: number; pctSenaMin: number },
    fechaBase = hoyPy(),
  ): Promise<OpcionResuelta> {
    const pctDescuentoMax = reglas.pctDescuentoMax;
    let tramos: TramoPlan[];
    let nombre = op.nombre?.trim() || 'A medida';
    if (op.plan_id) {
      const plan = await this.prisma.inm_planes_pago.findFirst({
        where: { id: op.plan_id, empresa_id, proyecto_id: unidad.proyecto_id, activo: true },
        include: { inm_plan_pago_tramos: { orderBy: { orden: 'asc' } } },
      });
      if (!plan) throw new BadRequestException('El plan no existe en este proyecto');
      tramos = aTramosPlan(plan.inm_plan_pago_tramos);
      nombre = op.nombre?.trim() || plan.nombre;
    } else if (op.tramos?.length) {
      tramos = aTramosPlan(op.tramos as unknown as Array<Record<string, unknown>>);
    } else {
      throw new BadRequestException('Cada opción necesita un plan o sus tramos');
    }

    const dec = decimalesDe(unidad.moneda_codigo);
    const descuento = op.descuento_pct ?? 0;
    let autorizado: string | null = null;
    if (descuento > pctDescuentoMax) {
      if (!(await tienePrivilegio(this.prisma, user_id, PRIVILEGIO_DESCUENTO))) {
        throw new ForbiddenException(
          `El descuento de ${descuento} % pasa el tope de ${pctDescuentoMax} % sin autorización. Pedile a un supervisor que la cargue.`,
        );
      }
      autorizado = user_id ?? null;
    }
    const precio_final = redondear(Number(unidad.precio_lista) * (1 - descuento / 100), dec);
    try {
      const cronograma = generarCronograma(precio_final, tramos, fechaBase, dec);
      // La reserva se hace con la seña: se valida al cotizar para no prometer un plan
      // que después no se puede reservar.
      const sena = cronograma.filter((c) => c.tipo === 'SENA').reduce((a, c) => a + c.monto, 0);
      if (sena <= 0)
        throw new BadRequestException(`${nombre}: el plan no tiene seña y la reserva se hace con el cobro de una seña`);
      const minimo = redondear((precio_final * reglas.pctSenaMin) / 100, dec);
      if (sena + 1e-9 < minimo) {
        throw new BadRequestException(
          `${nombre}: la seña (${sena}) es menor al mínimo del ${reglas.pctSenaMin} % del precio (${minimo})`,
        );
      }
      return {
        plan_id: op.plan_id ?? null,
        nombre,
        descuento_pct: descuento,
        precio_final,
        tramos,
        cronograma,
        descuento_autorizado_por: autorizado,
      };
    } catch (e) {
      if (e instanceof CronogramaError) throw new BadRequestException(`${nombre}: ${e.message}`);
      throw e;
    }
  }

  async crear(dto: CreateCotizacionDto, empresa_id: string, user_id?: string) {
    if (!dto.cliente_id && !dto.prospecto_nombre?.trim()) {
      throw new BadRequestException('Indicá el cliente o el nombre del interesado');
    }
    const unidad = await this.prisma.inm_unidades.findFirst({
      where: { id: dto.unidad_id, empresa_id, activo: true },
      include: { moneda: { select: { codigo: true } } },
    });
    if (!unidad) throw new NotFoundException('Unidad no encontrada');
    if (unidad.estado !== 'DISPONIBLE')
      throw new BadRequestException(`La unidad ${unidad.codigo} no está disponible (${unidad.estado.toLowerCase()})`);
    if (!(Number(unidad.precio_lista) > 0)) throw new BadRequestException('La unidad no tiene precio de lista');
    if (dto.cliente_id) {
      const c = await datosClientes(this.prisma, empresa_id, [dto.cliente_id]);
      if (!c.size) throw new BadRequestException('El cliente no existe en esta empresa');
    }

    const params = await this.parametros.obtener(empresa_id);
    const monedaCodigo = unidad.moneda?.codigo ?? 'PYG';
    const opciones: OpcionResuelta[] = [];
    for (const op of dto.opciones) {
      opciones.push(
        await this.resolverOpcion(empresa_id, { ...unidad, moneda_codigo: monedaCodigo }, op, user_id, {
          pctDescuentoMax: Number(params.pct_descuento_max),
          pctSenaMin: Number(params.pct_sena_min),
        }),
      );
    }

    const cotizacion = await this.prisma.$transaction(async (tx) => {
      const numero = await this.numeracion.siguiente(empresa_id, 'COT', tx);
      return tx.inm_cotizaciones.create({
        data: {
          empresa_id,
          numero,
          proyecto_id: unidad.proyecto_id,
          unidad_id: unidad.id,
          cliente_id: dto.cliente_id ?? null,
          prospecto_nombre: dto.prospecto_nombre?.trim() || null,
          prospecto_telefono: dto.prospecto_telefono?.trim() || null,
          prospecto_email: dto.prospecto_email?.trim() || null,
          vendedor_id: user_id ?? null,
          moneda_id: unidad.moneda_id,
          precio_lista: unidad.precio_lista,
          valida_hasta: aFecha(sumarDias(hoyPy(), params.dias_validez_cotizacion)),
          notas: dto.notas?.trim() || null,
          created_by: user_id ?? null,
          inm_cotizacion_opciones: {
            create: opciones.map((o, i) => ({
              empresa_id,
              plan_id: o.plan_id,
              nombre: o.nombre,
              orden: i + 1,
              descuento_pct: o.descuento_pct,
              precio_final: o.precio_final,
              tramos: o.tramos as unknown as object,
              cronograma: o.cronograma as unknown as object,
              descuento_autorizado_por: o.descuento_autorizado_por,
            })),
          },
        },
      });
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'inm_cotizaciones',
      entity_id: cotizacion.id,
      new_value: { numero: cotizacion.numero, unidad: unidad.codigo },
    });
    return this.detalle(cotizacion.id, empresa_id);
  }

  async listar(
    empresa_id: string,
    f: { proyecto_id?: string; unidad_id?: string; estado?: inm_cotizacion_estado; q?: string },
  ) {
    const q = f.q?.trim();
    const filas = await this.prisma.inm_cotizaciones.findMany({
      where: {
        empresa_id,
        ...(f.proyecto_id && { proyecto_id: f.proyecto_id }),
        ...(f.unidad_id && { unidad_id: f.unidad_id }),
        ...(f.estado && { estado: f.estado }),
        ...(q && {
          OR: [
            { numero: { contains: q, mode: 'insensitive' } },
            { prospecto_nombre: { contains: q, mode: 'insensitive' } },
            { inm_unidades: { codigo: { contains: q, mode: 'insensitive' } } },
          ],
        }),
      },
      include: {
        moneda: { select: { codigo: true } },
        inm_unidades: { select: { id: true, codigo: true } },
        inm_proyectos: { select: { id: true, codigo: true, nombre: true } },
        inm_cotizacion_opciones: { select: { nombre: true, precio_final: true }, orderBy: { orden: 'asc' } },
      },
      orderBy: { created_at: 'desc' },
      take: 300,
    });
    const clientes = await datosClientes(
      this.prisma,
      empresa_id,
      filas.map((c) => c.cliente_id),
    );
    return filas.map((c) => ({ ...c, cliente: clienteDe(clientes, c.cliente_id) }));
  }

  async detalle(id: string, empresa_id: string) {
    const c = await this.prisma.inm_cotizaciones.findFirst({
      where: { id, empresa_id },
      include: {
        moneda: { select: { id: true, codigo: true } },
        inm_unidades: true,
        inm_proyectos: { select: { id: true, codigo: true, nombre: true, ciudad: true, direccion: true } },
        inm_cotizacion_opciones: { orderBy: { orden: 'asc' } },
        inm_operaciones: { select: { id: true, numero: true, estado: true } },
      },
    });
    if (!c) throw new NotFoundException('Cotización no encontrada');
    const clientes = c.cliente_id ? await datosClientes(this.prisma, empresa_id, [c.cliente_id]) : new Map();
    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresa_id },
      select: { razon_social: true, nombre_fantasia: true },
    });
    const v = c.vendedor_id
      ? await this.prisma.usuario.findUnique({
          where: { id: c.vendedor_id },
          select: { username: true, nombres: true, apellidos: true },
        })
      : null;
    const vendedor = v ? { nombre: [v.nombres, v.apellidos].filter(Boolean).join(' ') || v.username } : null;
    return { ...c, cliente: clienteDe(clientes, c.cliente_id), empresa, vendedor };
  }

  async anular(id: string, empresa_id: string, user_id?: string) {
    const c = await this.prisma.inm_cotizaciones.findFirst({ where: { id, empresa_id } });
    if (!c) throw new NotFoundException('Cotización no encontrada');
    if (c.estado !== 'VIGENTE') throw new BadRequestException('Sólo se anula una cotización vigente');
    const r = await this.prisma.inm_cotizaciones.update({
      where: { id },
      data: { estado: 'ANULADA', updated_at: new Date() },
    });
    await this.audit.log({
      empresa_id,
      user_id,
      action: 'ANULAR',
      entity_type: 'inm_cotizaciones',
      entity_id: id,
      old_value: { estado: c.estado },
    });
    return r;
  }

  /** Job diario: las vigentes con la fecha pasada quedan vencidas. Idempotente. */
  async vencerCotizaciones(hoy = hoyPy()) {
    const r = await this.prisma.inm_cotizaciones.updateMany({
      where: { estado: 'VIGENTE', valida_hasta: { lt: aFecha(hoy) } },
      data: { estado: 'VENCIDA', updated_at: new Date() },
    });
    return r.count;
  }
}
