import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { camposFaltantes } from '../common/campos-obligatorios.util';

/**
 * Sincronía presupuesto → oportunidad (ticket CRM-15).
 *
 * Decisión D13: no hay bus de eventos. El módulo de presupuestos llama a estos
 * métodos cuando cambia de estado. Por eso este servicio vive en su propio
 * módulo y sólo depende de Prisma: si importara algo del CRM que a su vez importa
 * presupuestos, Nest no podría resolver el ciclo.
 *
 * **Ninguno de estos métodos propaga errores.** El cliente que acepta un
 * presupuesto desde el portal no tiene por qué recibir un 500 porque la etapa del
 * CRM no se pudo mover; el presupuesto ya quedó aceptado, que es lo que importa.
 * Los fallos quedan en el log.
 */
@Injectable()
export class CrmPresupuestoSyncService {
  private readonly logger = new Logger(CrmPresupuestoSyncService.name);

  constructor(private readonly prisma: PrismaService) {}

  /** Presupuesto enviado al cliente (DRAFT/APPROVED → SENT). */
  async presupuestoEnviado(presupuesto_id: string): Promise<void> {
    await this.seguro('enviado', presupuesto_id, async (ctx) => {
      // El que está en manos del cliente es el presupuesto vigente de la oportunidad.
      if (ctx.opo.presupuesto_id !== presupuesto_id) {
        await this.prisma.crm_oportunidades.update({
          where: { id: ctx.opo.id },
          data: { presupuesto_id, updated_at: new Date() },
        });
        ctx.opo.presupuesto_id = presupuesto_id;
      }
      await this.avanzar(ctx, await this.etapaDePropuesta(ctx.opo.empresa_id, ctx.opo.pipeline_id));
    });
  }

  /**
   * El cliente aceptó. Avanza a la última etapa abierta (negociación) y le deja
   * al vendedor la tarea de cerrar: ganar la oportunidad es del asistente de
   * cierre (M98) y de nadie más, porque es lo que emite los documentos.
   */
  async presupuestoAceptado(presupuesto_id: string): Promise<void> {
    await this.seguro('aceptado', presupuesto_id, async (ctx) => {
      await this.avanzar(ctx, await this.ultimaEtapaAbierta(ctx.opo.empresa_id, ctx.opo.pipeline_id));
      await this.tarea(
        ctx,
        `Presupuesto ${ctx.numero} aceptado: ejecutar el cierre`,
        'El cliente aceptó el presupuesto. Cerrá la venta desde el asistente para emitir la orden o la factura.',
      );
    });
  }

  /**
   * El cliente rechazó. **No** marca la oportunidad perdida: perder exige un
   * `motivo_perdida_id` del catálogo (CRM-12) y lo que escribe el cliente en el
   * portal es texto libre. La decisión —renegociar o dar por perdida con motivo—
   * queda en el vendedor, que se entera por la tarea.
   */
  async presupuestoRechazado(presupuesto_id: string, motivo?: string): Promise<void> {
    await this.seguro('rechazado', presupuesto_id, async (ctx) => {
      await this.tarea(
        ctx,
        `Presupuesto ${ctx.numero} rechazado por el cliente`,
        motivo
          ? `Motivo que dejó el cliente: ${motivo}. Si no hay nada más que ofrecer, marcá la oportunidad perdida con un motivo del catálogo.`
          : 'El cliente rechazó sin dejar motivo. Averiguá el motivo real antes de marcar la oportunidad perdida.',
      );
    });
  }

  // ---------------------------------------------------------------------------

  private async seguro(
    accion: string,
    presupuesto_id: string,
    fn: (ctx: ContextoSync) => Promise<void>,
  ): Promise<void> {
    try {
      const ctx = await this.contexto(presupuesto_id);
      if (!ctx) return;
      await fn(ctx);
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      this.logger.error(`No se pudo sincronizar la oportunidad (presupuesto ${accion} ${presupuesto_id}): ${msg}`);
    }
  }

  /** `null` cuando no hay nada que sincronizar: el presupuesto no nació del CRM. */
  private async contexto(presupuesto_id: string): Promise<ContextoSync | null> {
    const pre = await this.prisma.presupuesto_cab.findUnique({
      where: { id: presupuesto_id },
      select: { id: true, numero: true, oportunidad_id: true },
    });
    if (!pre?.oportunidad_id) return null;

    const opo = await this.prisma.crm_oportunidades.findUnique({
      where: { id: pre.oportunidad_id },
      include: { crm_etapas: true },
    });
    // Una oportunidad ya ganada o perdida no se toca: el documento ya se emitió
    // o el vendedor ya registró el motivo.
    if (!opo || opo.estado !== 'abierta') return null;

    return { numero: pre.numero, opo };
  }

  private async etapaDePropuesta(empresa_id: string, pipeline_id: string) {
    const abiertas = await this.etapasAbiertas(empresa_id, pipeline_id);
    // Sin depender del nombre, que el cliente puede cambiar: la etapa de propuesta
    // es la que exige tener presupuesto vinculado (así la define el seed de F0).
    return abiertas.find((e) => camposObligatoriosDe(e).includes('presupuesto_id')) ?? null;
  }

  private async ultimaEtapaAbierta(empresa_id: string, pipeline_id: string) {
    const abiertas = await this.etapasAbiertas(empresa_id, pipeline_id);
    return abiertas.length ? abiertas[abiertas.length - 1] : null;
  }

  private etapasAbiertas(empresa_id: string, pipeline_id: string) {
    return this.prisma.crm_etapas.findMany({
      where: { empresa_id, pipeline_id, activo: true, es_ganada: false, es_perdida: false },
      orderBy: { orden: 'asc' },
    });
  }

  /**
   * Mueve sólo hacia adelante. Si el cliente acepta un presupuesto de una
   * oportunidad que ya está en negociación, volverla a "Propuesta enviada" sería
   * perder información.
   */
  private async avanzar(ctx: ContextoSync, destino: EtapaMinima | null): Promise<void> {
    if (!destino) return;
    if (destino.id === ctx.opo.etapa_id) return;
    if (destino.orden <= ctx.opo.crm_etapas.orden) return;

    const faltantes = camposFaltantes(ctx.opo as unknown as Record<string, unknown>, destino.campos_obligatorios);
    if (faltantes.length) {
      // No forzamos la etapa: la regla de campos obligatorios es la que sostiene
      // que el embudo signifique algo. Se avisa y decide el vendedor.
      await this.tarea(
        ctx,
        `Completar datos para avanzar a "${destino.nombre}"`,
        `La oportunidad no pudo avanzar sola porque falta: ${faltantes.join(', ')}.`,
      );
      return;
    }

    await this.prisma.crm_oportunidades.update({
      where: { id: ctx.opo.id },
      data: { etapa_id: destino.id, probabilidad: destino.probabilidad, updated_at: new Date() },
    });
    ctx.opo.etapa_id = destino.id;
    ctx.opo.crm_etapas = { ...ctx.opo.crm_etapas, ...destino };
  }

  /** Tarea para hoy, asignada al vendedor de la oportunidad. */
  private async tarea(ctx: ContextoSync, asunto: string, detalle: string): Promise<void> {
    const usuario_id = ctx.opo.vendedor_id
      ? (
          await this.prisma.vendedores_cobradores.findUnique({
            where: { id: ctx.opo.vendedor_id },
            select: { usuario_id: true },
          })
        )?.usuario_id ?? null
      : null;

    await this.prisma.crm_actividades.create({
      data: {
        empresa_id: ctx.opo.empresa_id,
        sucursal_id: ctx.opo.sucursal_id,
        tipo: 'tarea',
        asunto: asunto.slice(0, 200),
        detalle,
        oportunidad_id: ctx.opo.id,
        cliente_id: ctx.opo.cliente_id,
        vendedor_id: ctx.opo.vendedor_id,
        usuario_id,
        fecha_programada: new Date(),
      },
    });
    await this.prisma.crm_oportunidades.update({
      where: { id: ctx.opo.id },
      data: { proxima_actividad_at: new Date(), updated_at: new Date() },
    });
  }
}

interface EtapaMinima {
  id: string;
  nombre: string;
  orden: number;
  probabilidad: number;
  campos_obligatorios: unknown;
}

interface ContextoSync {
  numero: string;
  opo: {
    id: string;
    empresa_id: string;
    sucursal_id: string | null;
    cliente_id: string | null;
    pipeline_id: string;
    etapa_id: string;
    vendedor_id: string | null;
    presupuesto_id: string | null;
    crm_etapas: EtapaMinima;
  };
}

function camposObligatoriosDe(etapa: EtapaMinima): string[] {
  return Array.isArray(etapa.campos_obligatorios) ? (etapa.campos_obligatorios as string[]) : [];
}
