import {
  BadRequestException, ConflictException, Injectable, Logger,
  NotFoundException, NotImplementedException, UnprocessableEntityException,
} from '@nestjs/common';
import { ClientesService } from 'src/clientes/clientes.service';
import { PedidosService } from 'src/pedidos/pedidos.service';
import { OrdenFacturacionService } from 'src/pedidos/orden-facturacion.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { PersonasService } from 'src/personas/personas.service';
import { CerrarVentaDto } from './dto/cerrar-venta.dto';
import {
  type ItemPayload, type PayloadFacturacion, tipoTransaccionDeItems, totalesDePayload,
} from './payload-facturacion';
import { dvValido } from './ruc.util';
import { OverrideCreditoService } from './override-credito.service';

/**
 * Asistente de cierre (M98). Ver docs/spec-crm-360.md §6.9 y §7.
 *
 * **Regla de oro**: el CRM no arma XML SIFEN, no toca stock y no asienta en
 * contabilidad. Produce un payload validado y se lo entrega a los servicios que
 * ya existen — PedidosService para la orden de venta, OrdenFacturacionService
 * para la factura.
 *
 * Sobre la atomicidad: la spec pide "una transacción Prisma", y para el documento
 * eso no es alcanzable: `FacturasService.create()` abre la suya y al terminar la
 * factura ya está encolada a SIFEN, sin vuelta atrás. Por eso acá se valida TODO
 * antes de emitir —cliente fiscal, DV, crédito, ítems, recurrencia— y recién
 * después se crea el documento. Es el mismo criterio que en PRE-01.
 */
@Injectable()
export class CierreVentaService {
  private readonly logger = new Logger(CierreVentaService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly clientesService: ClientesService,
    private readonly pedidosService: PedidosService,
    private readonly ordenFacturacion: OrdenFacturacionService,
    private readonly override: OverrideCreditoService,
    private readonly personasService: PersonasService,
  ) {}

  private async oportunidadAbierta(id: string, empresa_id: string) {
    const opo = await this.prisma.crm_oportunidades.findFirst({
      where: { id, empresa_id },
      include: { items: { orderBy: { orden: 'asc' } }, crm_etapas: true },
    });
    if (!opo) throw new NotFoundException('Oportunidad no encontrada');
    if (opo.estado === 'ganada') throw new ConflictException('La oportunidad ya fue ganada');
    if (opo.estado === 'perdida') throw new ConflictException('La oportunidad está perdida: reabrila primero');
    if (!opo.items.length) throw new BadRequestException('La oportunidad no tiene ítems: no hay qué facturar');
    return opo;
  }

  /** Paso 1 de la spec: el cliente fiscal tiene que estar en condiciones de recibir una factura. */
  private async validarReceptor(cliente_id: string, empresa_id: string) {
    const cliente = await this.prisma.clientes.findFirst({
      where: { id: cliente_id, deleted: false, personas: { empresa_id } },
      include: { personas: true, tipo_operacion: { select: { codigo: true } } },
    });
    if (!cliente) throw new NotFoundException('El cliente no existe en esta empresa');

    /**
     * Las mismas reglas que valida `FacturasService` al emitir, pero acá — antes
     * de crear nada.
     *
     * Antes el cierre creaba la orden de venta y recién al facturar descubría que
     * el cliente, por ejemplo, tenía un tipo de operación que SIFEN no acepta
     * para su naturaleza. La orden quedaba creada y confirmada, y cada reintento
     * dejaba otra: tres intentos, tres órdenes reservando stock. Pasó cargando
     * datos de prueba y por eso se valida de este lado.
     */
    if (cliente.persona_id) {
      const errores = await this.personasService.validatePersonaById(cliente.persona_id, empresa_id, {
        tipoOperacionCodigo: cliente.tipo_operacion?.codigo ?? null,
      });
      if (errores.length) {
        throw new BadRequestException({
          message: 'El cliente tiene datos incompletos o inválidos. Corregilos antes de cerrar la venta:',
          errores,
          cliente_id: cliente.id,
        });
      }
    }

    const p = cliente.personas;
    const problemas: string[] = [];
    if (!p?.razon_social?.trim()) problemas.push('falta la razón social');

    // Sin RUC se puede facturar (consumidor final), pero CON RUC el DV tiene que
    // cerrar: un DV mal tipeado lo rechaza SIFEN después de emitir, y ahí ya hay
    // que anular el documento.
    if (p?.ruc) {
      if (!dvValido(p.ruc, p.dv)) problemas.push(`el DV del RUC ${p.ruc} no es correcto`);
    } else if (!p?.nro_documento?.trim()) {
      problemas.push('sin RUC hace falta un número de documento para el receptor');
    }

    if (problemas.length) {
      throw new BadRequestException({
        message: `El cliente no está en condiciones de facturar: ${problemas.join('; ')}`,
        problemas,
      });
    }
    return cliente;
  }

  /**
   * Paso 4: crédito. Se apoya en `verificarCredito()` del ERP, que es el mismo
   * control que usa el POS.
   *
   * El override por PIN **se valida de verdad**: bcrypt contra
   * `usuario.pin_supervisor` más el privilegio `CRM_CIE_CREDITO_AUTORIZAR`. Acá
   * sólo se verifica; el registro en `autorizaciones_caja` lo hace `ejecutar()`,
   * porque el preview no persiste nada.
   */
  private async validarCredito(
    cliente_id: string,
    total: number,
    dto: CerrarVentaDto,
    empresa_id: string,
  ) {
    if (dto.condicion !== 'credito') return { verificado: false as const };

    const r = await this.clientesService.verificarCredito(cliente_id, total);
    if (r.permitido) return { verificado: true as const, ...r };

    if (!dto.pin_supervisor) {
      throw new BadRequestException({
        message: r.mensaje ?? 'El cliente no tiene crédito disponible para este monto',
        requiere: 'override_supervisor',
        saldo_disponible: r.saldo_disponible,
      });
    }

    const supervisor = await this.override.verificar(empresa_id, dto.supervisor_id, dto.pin_supervisor);
    return {
      verificado: true as const,
      override: true,
      autorizado_por: { id: supervisor.id, username: supervisor.username },
      ...r,
    };
  }

  /** Arma el payload sin persistir nada. Es lo que el wizard muestra en el resumen. */
  private async armarPayload(opo: any, cliente: any, dto: CerrarVentaDto): Promise<PayloadFacturacion> {
    const productoIds = opo.items.map((i: any) => i.producto_id).filter(Boolean);
    const productos = productoIds.length
      ? await this.prisma.productos.findMany({
          where: { id: { in: productoIds } },
          // No hay flag de 'servicio': un producto que no maneja inventario lo es.
          select: { id: true, descripcion: true, maneja_inventario: true },
        })
      : [];
    const porId = new Map(productos.map((p) => [p.id, p]));

    const unicos = opo.items.filter((i: any) => i.tipo_cobro !== 'recurrente');
    const items: ItemPayload[] = unicos.map((i: any) => {
      const subtotal = Number(i.subtotal ?? 0);
      const tasa = Number(i.iva_tasa ?? 10);
      // IVA incluido en el precio, que es la convención del ERP.
      const iva_monto = tasa === 0 ? 0 : Number((subtotal - subtotal / (1 + tasa / 100)).toFixed(4));
      return {
        producto_id: i.producto_id ?? null,
        descripcion: i.descripcion,
        cantidad: Number(i.cantidad),
        precio_unitario: Number(i.precio_unitario),
        descuento_pct: Number(i.descuento_pct ?? 0),
        iva_tasa: tasa,
        subtotal,
        iva_monto,
      };
    });

    const p = cliente.personas;
    return {
      empresa_id: opo.empresa_id,
      sucursal_id: opo.sucursal_id ?? null,
      tipo_transaccion: tipoTransaccionDeItems(
        unicos.map((i: any) => ({ es_servicio: !i.producto_id || porId.get(i.producto_id)?.maneja_inventario === false })),
      ),
      moneda_id: opo.moneda_id ?? null,
      receptor: {
        cliente_id: cliente.id,
        razon_social: p?.razon_social ?? '',
        ruc: p?.ruc ?? null,
        dv: p?.dv ?? null,
        documento: p?.nro_documento ?? null,
        email: p?.email ?? null,
        direccion: p?.direccion ?? null,
        naturaleza: p?.ruc ? 1 : 2,
        tipo_operacion: p?.ruc ? 1 : 2,
      },
      items,
      condicion: {
        tipo: dto.condicion === 'credito' ? 'credito' : 'contado',
        ...(dto.condicion === 'credito'
          ? {
              credito: {
                modalidad: (dto.modalidad_credito as any) ?? (dto.cuotas ? 'cuotas' : 'plazo'),
                plazo_dias: dto.plazo_dias,
                cuotas: undefined,
              },
            }
          : {}),
      },
      totales: totalesDePayload(items),
      info_adicional: dto.info_adicional?.trim() || `Oportunidad ${opo.numero}`,
      trazabilidad: {
        oportunidad_id: opo.id,
        oportunidad_numero: opo.numero,
        presupuesto_id: opo.presupuesto_id ?? null,
        vendedor_id: opo.vendedor_id ?? null,
      },
    };
  }

  /**
   * Vista previa: valida todo y devuelve el payload. No persiste nada.
   * La factura emitida tiene que coincidir 100% con esto (criterio de §13.3).
   */
  async preview(oportunidad_id: string, dto: CerrarVentaDto, empresa_id: string) {
    if (dto.items?.length) {
      throw new NotImplementedException('El cierre parcial por ítem todavía no está implementado');
    }

    const opo = await this.oportunidadAbierta(oportunidad_id, empresa_id);

    const recurrentes = opo.items.filter((i: any) => i.tipo_cobro === 'recurrente');
    if (recurrentes.length) {
      // F1 no tiene contratos: emitir sólo la parte única dejaría la recurrente
      // sin facturar y nadie se enteraría hasta el mes siguiente.
      throw new UnprocessableEntityException({
        message:
          'La oportunidad tiene ítems recurrentes y los contratos son parte de CRM Pro (Fase 3). ' +
          'Sacá los ítems recurrentes o esperá a que esté disponible.',
        requiere: 'CRM_RECURRENTE',
        items_recurrentes: recurrentes.length,
      });
    }

    const cliente_id = dto.cliente_id ?? opo.cliente_id;
    if (!cliente_id) {
      throw new BadRequestException('La oportunidad cuelga de un lead: hay que indicar el cliente fiscal');
    }

    const cliente = await this.validarReceptor(cliente_id, empresa_id);
    const payload = await this.armarPayload(opo, cliente, dto);
    const credito = await this.validarCredito(cliente_id, payload.totales.total, dto, empresa_id);

    const porOrden = !!dto.requiere_entrega || Number(dto.anticipo ?? 0) > 0;

    return {
      payload,
      // Lo que el wizard necesita para el paso 7 sin tener que deducirlo.
      destino: porOrden ? ('orden_venta' as const) : ('factura' as const),
      credito,
    };
  }

  /**
   * Ejecuta el cierre. Devuelve el documento creado y deja la oportunidad ganada.
   *
   * El orden importa: primero el documento, después la oportunidad. Si se marcara
   * ganada antes y fallara la emisión, quedaría una oportunidad ganada sin nada
   * detrás, que es exactamente lo que el 409 de `moverEtapa` quiere evitar.
   */
  async ejecutar(oportunidad_id: string, dto: CerrarVentaDto, empresa_id: string, user_id: string) {
    const { payload, destino, credito } = await this.preview(oportunidad_id, dto, empresa_id);

    const items = payload.items.map((i) => ({
      producto_id: i.producto_id,
      cantidad: i.cantidad,
      precio_negociado: i.precio_unitario,
      descuento_porcentaje: i.descuento_pct,
    }));
    if (items.some((i) => !i.producto_id)) {
      throw new BadRequestException(
        'Hay ítems sin producto del catálogo: la orden de venta y la factura los necesitan',
      );
    }

    const orden: any = await this.pedidosService.crearOrden(
      {
        cliente_id: payload.receptor.cliente_id,
        sucursal_id: payload.sucursal_id ?? undefined,
        deposito_id: dto.deposito_id,
        vendedor_id: payload.trazabilidad.vendedor_id ?? undefined,
        moneda_id: payload.moneda_id ?? undefined,
        description: payload.info_adicional ?? `Cierre de ${payload.trazabilidad.oportunidad_numero}`,
        items,
      } as any,
      empresa_id,
      user_id,
    );
    const orden_id: string = orden?.data?.id ?? orden?.id;
    if (!orden_id) throw new BadRequestException('No se pudo crear la orden de venta del cierre');

    await this.prisma.pedidos.update({ where: { id: orden_id }, data: { oportunidad_id } });
    await this.pedidosService.confirmarOrden(orden_id, empresa_id, user_id);

    let factura_id: string | null = null;
    if (destino === 'factura') {
      const r: any = await this.ordenFacturacion.convertirAFactura(orden_id, empresa_id, user_id, {
        numeracion_id: dto.numeracion_id,
      });
      factura_id = r?.data?.factura_id ?? null;
      if (factura_id) {
        await this.prisma.factura_cab.update({ where: { id: factura_id }, data: { oportunidad_id } });
      }
    }

    const etapaGanada = await this.prisma.crm_etapas.findFirst({
      where: { pipeline_id: (await this.prisma.crm_oportunidades.findUniqueOrThrow({
        where: { id: oportunidad_id }, select: { pipeline_id: true },
      })).pipeline_id, es_ganada: true, activo: true },
      select: { id: true, probabilidad: true },
    });

    const ganada = await this.prisma.crm_oportunidades.update({
      where: { id: oportunidad_id },
      data: {
        estado: 'ganada',
        fecha_cierre_real: new Date(),
        pedido_id: orden_id,
        ...(etapaGanada ? { etapa_id: etapaGanada.id, probabilidad: etapaGanada.probabilidad } : {}),
        updated_at: new Date(),
      },
    });

    // La autorización se registra recién acá, con el documento ya emitido. Al
    // revés quedarían overrides aprobados de ventas que nunca existieron.
    let autorizacion_id: string | null = null;
    if ('override' in credito && credito.override && dto.supervisor_id) {
      autorizacion_id = await this.override.registrar(empresa_id, user_id, dto.supervisor_id, {
        oportunidad_id,
        cliente_id: payload.receptor.cliente_id,
        total: payload.totales.total,
        motivo: `Cierre de ${payload.trazabilidad.oportunidad_numero} por encima del límite de crédito`,
      });
    }

    this.logger.log(
      `Cierre de ${payload.trazabilidad.oportunidad_numero}: orden ${orden_id}` +
        (factura_id ? ` · factura ${factura_id}` : ' (queda pendiente de facturar)'),
    );
    return {
      message: 'Oportunidad cerrada',
      data: { oportunidad: ganada, orden_id, factura_id, destino, autorizacion_id },
    };
  }
}
