import {
  BadRequestException, ConflictException, Injectable, NotFoundException,
  UnprocessableEntityException,
} from '@nestjs/common';
import { presupuesto_estado, presupuesto_item_tipo, presupuesto_iva_categoria } from '@prisma/client';
import { PresupuestosService } from 'src/presupuestos/presupuestos.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { PresupuestoItemDto } from 'src/presupuestos/dto/presupuesto-item.dto';
import { GenerarPresupuestoDto } from './dto/generar-presupuesto.dto';

/**
 * Presupuesto desde la oportunidad (ticket CRM-15). Ver docs/spec-crm-360.md §6.3.
 *
 * El CRM no arma presupuestos: traduce los ítems de la oportunidad al DTO de M36
 * y deja que `PresupuestosService` haga el resto —numeración, vigencia, totales,
 * historial—. Lo único propio es el vínculo en las dos direcciones
 * (`presupuesto_cab.oportunidad_id` ↔ `crm_oportunidades.presupuesto_id`), que es
 * lo que después permite sincronizar la etapa cuando el cliente responde.
 */
@Injectable()
export class PresupuestoOportunidadService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly presupuestos: PresupuestosService,
  ) {}

  async generar(oportunidad_id: string, dto: GenerarPresupuestoDto, empresa_id: string, user_id?: string, usuario?: string) {
    const opo = await this.prisma.crm_oportunidades.findFirst({
      where: { id: oportunidad_id, empresa_id },
      include: { items: { orderBy: { orden: 'asc' } } },
    });
    if (!opo) throw new NotFoundException('Oportunidad no encontrada');
    if (opo.estado !== 'abierta') throw new ConflictException(`La oportunidad está ${opo.estado}`);
    if (!opo.items.length) throw new BadRequestException('La oportunidad no tiene ítems para cotizar');

    // Un presupuesto es un documento dirigido a un cliente del padrón; una
    // oportunidad todavía en lead no tiene a quién dirigirlo.
    if (!opo.cliente_id) {
      throw new BadRequestException('Convertí el lead en cliente antes de generar el presupuesto');
    }

    // M36 no tiene noción de recurrencia: cotizar sólo la parte única sería
    // cotizar de menos. Mismo criterio que el asistente de cierre.
    if (opo.items.some((i) => i.tipo_cobro !== 'unico')) {
      throw new UnprocessableEntityException({
        message: 'La oportunidad tiene ítems recurrentes y el presupuesto todavía no los soporta',
        requiere: 'CRM_RECURRENTE',
      });
    }

    const vigente = await this.prisma.presupuesto_cab.findFirst({
      where: {
        oportunidad_id,
        deleted: false,
        estado: { notIn: [presupuesto_estado.REJECTED, presupuesto_estado.EXPIRED, presupuesto_estado.CANCELLED, presupuesto_estado.SUPERSEDED] },
      },
      select: { id: true, numero: true, estado: true },
    });
    if (vigente) {
      throw new ConflictException({
        message: `La oportunidad ya tiene el presupuesto ${vigente.numero} (${vigente.estado}). Para cotizar de nuevo, creá una versión nueva desde el presupuesto.`,
        presupuesto_id: vigente.id,
      });
    }

    const tipos = await this.tiposDeItem(opo.items.map((i) => i.producto_id).filter((id): id is string => !!id));

    const items: PresupuestoItemDto[] = opo.items.map((i, idx) => ({
      orden: idx + 1,
      tipo: i.producto_id ? tipos.get(i.producto_id) ?? presupuesto_item_tipo.PRODUCTO : presupuesto_item_tipo.SERVICIO,
      producto_id: i.producto_id ?? undefined,
      descripcion: i.descripcion,
      cantidad: Number(i.cantidad),
      precio_unitario: Number(i.precio_unitario),
      descuento_pct: Number(i.descuento_pct),
      iva_categoria: categoriaIva(i.iva_tasa),
    }));

    const creado = await this.presupuestos.create(
      {
        cliente_id: opo.cliente_id,
        titulo: dto.titulo?.trim() || opo.titulo,
        contacto_cliente: dto.contacto_cliente,
        vendedor_id: opo.vendedor_id ?? undefined,
        sucursal_id: opo.sucursal_id ?? undefined,
        moneda_id: opo.moneda_id ?? undefined,
        tipo_cambio_usd: dto.tipo_cambio_usd,
        fecha_vigencia: dto.fecha_vigencia,
        condicion_pago_id: dto.condicion_pago_id,
        notas_cliente: dto.notas_cliente,
        observaciones_internas: `Generado desde la oportunidad ${opo.numero}`,
        descuento_global_pct: dto.descuento_global_pct,
        items,
      },
      empresa_id,
      user_id,
      usuario,
    );

    // El vínculo en las dos direcciones. La etapa NO se mueve todavía: el
    // presupuesto nace en DRAFT y la oportunidad avanza cuando se envía al
    // cliente (CrmPresupuestoSyncService.presupuestoEnviado).
    await this.prisma.$transaction([
      this.prisma.presupuesto_cab.update({ where: { id: creado.id }, data: { oportunidad_id } }),
      this.prisma.crm_oportunidades.update({
        where: { id: oportunidad_id },
        data: { presupuesto_id: creado.id, updated_at: new Date() },
      }),
    ]);

    return {
      message: `Presupuesto ${creado.numero} generado desde la oportunidad ${opo.numero}`,
      data: { ...creado, oportunidad_id },
    };
  }

  /**
   * `productos.maneja_inventario === false` es cómo el ERP marca un servicio
   * (no existe `es_servicio`). Importa porque M36 sólo suma al total los ítems
   * PRODUCTO y SERVICIO: un ítem mal tipado saldría cotizado en cero.
   */
  private async tiposDeItem(ids: string[]): Promise<Map<string, presupuesto_item_tipo>> {
    if (!ids.length) return new Map();
    const productos = await this.prisma.productos.findMany({
      where: { id: { in: ids } },
      select: { id: true, maneja_inventario: true },
    });
    return new Map(
      productos.map((p) => [
        p.id,
        p.maneja_inventario === false ? presupuesto_item_tipo.SERVICIO : presupuesto_item_tipo.PRODUCTO,
      ]),
    );
  }
}

function categoriaIva(tasa: number): presupuesto_iva_categoria {
  if (tasa === 5) return presupuesto_iva_categoria.IVA_5;
  if (tasa === 0) return presupuesto_iva_categoria.EXENTO;
  return presupuesto_iva_categoria.IVA_10;
}
