import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { CrmSeedService } from '../seeds/crm-seed.service';
import { CreateEtapaDto } from './dto/create-etapa.dto';
import { CreatePipelineDto } from './dto/create-pipeline.dto';
import { ReordenarEtapasDto } from './dto/reordenar-etapas.dto';
import { UpdateEtapaDto } from './dto/update-etapa.dto';
import { UpdatePipelineDto } from './dto/update-pipeline.dto';

/**
 * Configuración del CRM (M90): embudos y etapas.
 * Ver docs/spec-crm-360.md §6.1 y §5.3, y docs/plan-crm-360.md (CRM-01).
 *
 * Nada se borra físicamente: una etapa vieja sigue referenciada por las
 * oportunidades que pasaron por ella, así que la baja es lógica, igual que en
 * rubros y en el resto del ERP.
 */
@Injectable()
export class CrmConfigService {
  private readonly logger = new Logger(CrmConfigService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
    private readonly seed: CrmSeedService,
  ) {}

  // ---------------------------------------------------------------- pipelines

  async findAllPipelines(empresa_id: string, soloActivos = false) {
    // La primera vez que alguien abre el CRM la empresa no tiene nada configurado.
    // Sembrarlo acá evita un paso manual después del alta y funciona igual para
    // las empresas que contraten el módulo mañana, sin barrido ni cron.
    if (await this.seed.necesitaSeed(empresa_id)) {
      await this.seed.asegurarConfiguracion(empresa_id);
    }

    return this.prisma.crm_pipelines.findMany({
      where: soloActivos ? { empresa_id, activo: true } : { empresa_id },
      include: {
        crm_etapas: {
          where: { activo: true },
          orderBy: [{ orden: 'asc' }, { nombre: 'asc' }],
        },
      },
      orderBy: [{ orden: 'asc' }, { nombre: 'asc' }],
    });
  }

  private async pipelineDeEmpresa(id: string, empresa_id: string) {
    const pipeline = await this.prisma.crm_pipelines.findFirst({ where: { id, empresa_id } });
    if (!pipeline) throw new NotFoundException('Pipeline no encontrado');
    return pipeline;
  }

  async createPipeline(dto: CreatePipelineDto, empresa_id: string, user_id?: string) {
    const nombre = dto.nombre.trim();

    const existente = await this.prisma.crm_pipelines.findFirst({ where: { empresa_id, nombre } });
    if (existente) throw new ConflictException(`Ya existe un pipeline llamado "${nombre}"`);

    // El default es excluyente: si éste lo pide, el anterior lo pierde. Se hace
    // en transacción para que no queden dos defaults si algo falla en el medio.
    const pipeline = await this.prisma.$transaction(async (tx) => {
      if (dto.es_default) {
        await tx.crm_pipelines.updateMany({ where: { empresa_id, es_default: true }, data: { es_default: false } });
      }
      return tx.crm_pipelines.create({
        data: {
          empresa_id,
          nombre,
          descripcion: dto.descripcion?.trim() || null,
          orden: dto.orden ?? 0,
          es_default: dto.es_default ?? false,
          created_by: user_id ?? null,
        },
      });
    });

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'crm_pipelines',
      entity_id: pipeline.id,
      new_value: pipeline,
    });
    this.logger.log(`Pipeline creado: ${nombre} (${pipeline.id}) empresa=${empresa_id}`);
    return pipeline;
  }

  async updatePipeline(id: string, dto: UpdatePipelineDto, empresa_id: string, user_id?: string) {
    const anterior = await this.pipelineDeEmpresa(id, empresa_id);
    const nombre = dto.nombre?.trim();

    if (nombre && nombre !== anterior.nombre) {
      const choque = await this.prisma.crm_pipelines.findFirst({ where: { empresa_id, nombre, id: { not: id } } });
      if (choque) throw new ConflictException(`Ya existe un pipeline llamado "${nombre}"`);
    }

    const pipeline = await this.prisma.$transaction(async (tx) => {
      if (dto.es_default === true) {
        await tx.crm_pipelines.updateMany({
          where: { empresa_id, es_default: true, id: { not: id } },
          data: { es_default: false },
        });
      }
      return tx.crm_pipelines.update({
        where: { id },
        data: {
          ...(nombre !== undefined ? { nombre } : {}),
          ...(dto.descripcion !== undefined ? { descripcion: dto.descripcion?.trim() || null } : {}),
          ...(dto.orden !== undefined ? { orden: dto.orden } : {}),
          ...(dto.es_default !== undefined ? { es_default: dto.es_default } : {}),
          ...(dto.activo !== undefined ? { activo: dto.activo } : {}),
          updated_at: new Date(),
        },
      });
    });

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'crm_pipelines',
      entity_id: id,
      old_value: anterior,
      new_value: pipeline,
    });
    return pipeline;
  }

  /** Baja lógica. Un pipeline con oportunidades no se puede borrar ni aunque esté vacío de etapas. */
  async removePipeline(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.pipelineDeEmpresa(id, empresa_id);

    const pipeline = await this.prisma.crm_pipelines.update({
      where: { id },
      data: { activo: false, updated_at: new Date() },
    });

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'ELIMINAR',
      entity_type: 'crm_pipelines',
      entity_id: id,
      old_value: anterior,
      new_value: pipeline,
    });
    return pipeline;
  }

  // ------------------------------------------------------------------ etapas

  async findEtapas(pipeline_id: string, empresa_id: string, soloActivas = false) {
    await this.pipelineDeEmpresa(pipeline_id, empresa_id);
    return this.prisma.crm_etapas.findMany({
      where: soloActivas ? { pipeline_id, empresa_id, activo: true } : { pipeline_id, empresa_id },
      orderBy: [{ orden: 'asc' }, { nombre: 'asc' }],
    });
  }

  private async etapaDeEmpresa(id: string, empresa_id: string) {
    const etapa = await this.prisma.crm_etapas.findFirst({ where: { id, empresa_id } });
    if (!etapa) throw new NotFoundException('Etapa no encontrada');
    return etapa;
  }

  /**
   * Ganada y perdida son únicas por pipeline. No está como CHECK en la base
   * porque el usuario reordena y edita etapas en caliente y un constraint lo
   * trabaría a mitad de camino; se valida acá, que es donde hay contexto.
   */
  private async validarResultadoUnico(
    pipeline_id: string,
    dto: { es_ganada?: boolean; es_perdida?: boolean },
    excluirEtapaId?: string,
  ) {
    // Validación de entrada pura: se chequea antes de ir a la base.
    if (dto.es_ganada && dto.es_perdida) {
      throw new BadRequestException('Una etapa no puede ser ganada y perdida a la vez');
    }

    for (const campo of ['es_ganada', 'es_perdida'] as const) {
      if (dto[campo] !== true) continue;
      const otra = await this.prisma.crm_etapas.findFirst({
        where: { pipeline_id, [campo]: true, activo: true, ...(excluirEtapaId ? { id: { not: excluirEtapaId } } : {}) },
        select: { nombre: true },
      });
      if (otra) {
        const etiqueta = campo === 'es_ganada' ? 'ganada' : 'perdida';
        throw new ConflictException(`El pipeline ya tiene una etapa ${etiqueta}: "${otra.nombre}"`);
      }
    }
  }

  async createEtapa(pipeline_id: string, dto: CreateEtapaDto, empresa_id: string, user_id?: string) {
    await this.pipelineDeEmpresa(pipeline_id, empresa_id);
    const nombre = dto.nombre.trim();

    const existente = await this.prisma.crm_etapas.findFirst({ where: { pipeline_id, nombre } });
    if (existente) throw new ConflictException(`El pipeline ya tiene una etapa llamada "${nombre}"`);

    await this.validarResultadoUnico(pipeline_id, dto);

    // Sin orden explícito va al final, que es lo que espera quien agrega una
    // etapa desde el ABM sin pensar en la posición.
    let orden = dto.orden;
    if (orden === undefined) {
      const ultima = await this.prisma.crm_etapas.findFirst({
        where: { pipeline_id },
        orderBy: { orden: 'desc' },
        select: { orden: true },
      });
      orden = (ultima?.orden ?? -1) + 1;
    }

    const etapa = await this.prisma.crm_etapas.create({
      data: {
        empresa_id,
        pipeline_id,
        nombre,
        orden,
        probabilidad: dto.probabilidad ?? 0,
        campos_obligatorios: dto.campos_obligatorios ?? [],
        dias_estancamiento: dto.dias_estancamiento ?? null,
        es_ganada: dto.es_ganada ?? false,
        es_perdida: dto.es_perdida ?? false,
        created_by: user_id ?? null,
      },
    });

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'CREAR',
      entity_type: 'crm_etapas',
      entity_id: etapa.id,
      new_value: etapa,
    });
    return etapa;
  }

  async updateEtapa(id: string, dto: UpdateEtapaDto, empresa_id: string, user_id?: string) {
    const anterior = await this.etapaDeEmpresa(id, empresa_id);
    const nombre = dto.nombre?.trim();

    if (nombre && nombre !== anterior.nombre) {
      const choque = await this.prisma.crm_etapas.findFirst({
        where: { pipeline_id: anterior.pipeline_id, nombre, id: { not: id } },
      });
      if (choque) throw new ConflictException(`El pipeline ya tiene una etapa llamada "${nombre}"`);
    }

    await this.validarResultadoUnico(anterior.pipeline_id, dto, id);

    const etapa = await this.prisma.crm_etapas.update({
      where: { id },
      data: {
        ...(nombre !== undefined ? { nombre } : {}),
        ...(dto.orden !== undefined ? { orden: dto.orden } : {}),
        ...(dto.probabilidad !== undefined ? { probabilidad: dto.probabilidad } : {}),
        ...(dto.campos_obligatorios !== undefined ? { campos_obligatorios: dto.campos_obligatorios } : {}),
        ...(dto.dias_estancamiento !== undefined ? { dias_estancamiento: dto.dias_estancamiento } : {}),
        ...(dto.es_ganada !== undefined ? { es_ganada: dto.es_ganada } : {}),
        ...(dto.es_perdida !== undefined ? { es_perdida: dto.es_perdida } : {}),
        ...(dto.activo !== undefined ? { activo: dto.activo } : {}),
        updated_at: new Date(),
      },
    });

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'crm_etapas',
      entity_id: id,
      old_value: anterior,
      new_value: etapa,
    });
    return etapa;
  }

  /**
   * Reordena en bloque, que es lo que manda el drag & drop del kanban.
   * Exige la lista COMPLETA de etapas activas del pipeline: si llegara parcial,
   * las que faltan quedarían con un orden que choca con las reordenadas y el
   * tablero se vería mezclado sin que nadie entienda por qué.
   */
  async reordenarEtapas(pipeline_id: string, dto: ReordenarEtapasDto, empresa_id: string, user_id?: string) {
    await this.pipelineDeEmpresa(pipeline_id, empresa_id);

    const activas = await this.prisma.crm_etapas.findMany({
      where: { pipeline_id, empresa_id, activo: true },
      select: { id: true },
    });
    const esperadas = new Set(activas.map((e) => e.id));
    const recibidas = new Set(dto.etapa_ids);

    const ajenas = dto.etapa_ids.filter((id) => !esperadas.has(id));
    if (ajenas.length) {
      throw new BadRequestException(`Estas etapas no pertenecen al pipeline: ${ajenas.join(', ')}`);
    }
    const faltantes = [...esperadas].filter((id) => !recibidas.has(id));
    if (faltantes.length) {
      throw new BadRequestException(
        `Faltan ${faltantes.length} etapa(s) en el reordenamiento: hay que mandar las ${esperadas.size} activas`,
      );
    }

    await this.prisma.$transaction(
      dto.etapa_ids.map((id, i) =>
        this.prisma.crm_etapas.update({ where: { id }, data: { orden: i, updated_at: new Date() } }),
      ),
    );

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'EDITAR',
      entity_type: 'crm_etapas',
      entity_id: pipeline_id,
      new_value: { orden: dto.etapa_ids },
    });

    return this.findEtapas(pipeline_id, empresa_id, true);
  }

  /** Baja lógica: las oportunidades que pasaron por la etapa la siguen referenciando. */
  async removeEtapa(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.etapaDeEmpresa(id, empresa_id);

    const etapa = await this.prisma.crm_etapas.update({
      where: { id },
      data: { activo: false, updated_at: new Date() },
    });

    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'ELIMINAR',
      entity_type: 'crm_etapas',
      entity_id: id,
      old_value: anterior,
      new_value: etapa,
    });
    return etapa;
  }
}
