import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import {
  CreateCalendarioDto, CreateMotivoDto, CreateOrigenDto, CreateSlaDto,
  UpdateCalendarioDto, UpdateMotivoDto, UpdateOrigenDto, UpdateSlaDto,
} from './dto/catalogos.dto';

/**
 * ABM de los catálogos que CRM-04 siembra y que hasta ahora sólo se podían editar
 * por SQL: orígenes, motivos, calendarios y políticas de SLA.
 *
 * No tenía ticket. Se detectó cerrando F1: el seed dejaba datos razonables, pero
 * una empresa que vende por TikTok no tenía forma de agregar ese origen, y sin
 * origen propio sus reportes por canal no dicen nada.
 *
 * **Baja lógica siempre.** Un origen o un motivo ya usado está referenciado por
 * leads y oportunidades históricos; borrarlo rompería los reportes del año pasado.
 * Por eso se desactiva: deja de ofrecerse en los formularios y lo viejo sigue
 * explicándose.
 */
@Injectable()
export class CatalogosService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  // ── Orígenes ────────────────────────────────────────────────────────────────

  findOrigenes(empresa_id: string, soloActivos = false) {
    return this.prisma.crm_origenes.findMany({
      where: { empresa_id, ...(soloActivos ? { activo: true } : {}) },
      orderBy: [{ orden: 'asc' }, { descripcion: 'asc' }],
    });
  }

  async createOrigen(dto: CreateOrigenDto, empresa_id: string, user_id?: string) {
    const codigo = dto.codigo.trim().toUpperCase();
    const existente = await this.prisma.crm_origenes.findFirst({ where: { empresa_id, codigo } });
    if (existente) {
      // Reactivar en vez de fallar: el caso real es dar de baja "FERIA" y volver
      // a necesitarlo el año siguiente. Crear "FERIA_2" partiría los reportes.
      if (!existente.activo) return this.updateOrigen(existente.id, { activo: true, descripcion: dto.descripcion }, empresa_id, user_id);
      throw new ConflictException(`Ya existe un origen con el código ${codigo}`);
    }

    const origen = await this.prisma.crm_origenes.create({
      data: { empresa_id, codigo, descripcion: dto.descripcion.trim(), orden: dto.orden ?? 0, created_by: user_id ?? null },
    });
    await this.auditar(empresa_id, user_id, 'CREAR', 'crm_origenes', origen.id, undefined, origen);
    return origen;
  }

  async updateOrigen(id: string, dto: UpdateOrigenDto, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa('crm_origenes', id, empresa_id);
    const origen = await this.prisma.crm_origenes.update({
      where: { id },
      data: {
        ...(dto.descripcion !== undefined ? { descripcion: dto.descripcion.trim() } : {}),
        ...(dto.orden !== undefined ? { orden: dto.orden } : {}),
        ...(dto.activo !== undefined ? { activo: dto.activo } : {}),
        updated_at: new Date(),
      },
    });
    await this.auditar(empresa_id, user_id, 'EDITAR', 'crm_origenes', id, anterior, origen);
    return origen;
  }

  async removeOrigen(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa('crm_origenes', id, empresa_id);
    const origen = await this.prisma.crm_origenes.update({ where: { id }, data: { activo: false, updated_at: new Date() } });
    await this.auditar(empresa_id, user_id, 'ELIMINAR', 'crm_origenes', id, anterior, origen);
    return origen;
  }

  // ── Motivos ─────────────────────────────────────────────────────────────────

  findMotivos(empresa_id: string, tipo?: string, soloActivos = false) {
    return this.prisma.crm_motivos.findMany({
      where: { empresa_id, ...(tipo ? { tipo } : {}), ...(soloActivos ? { activo: true } : {}) },
      orderBy: [{ tipo: 'asc' }, { orden: 'asc' }, { descripcion: 'asc' }],
    });
  }

  async createMotivo(dto: CreateMotivoDto, empresa_id: string, user_id?: string) {
    const descripcion = dto.descripcion.trim();
    const existente = await this.prisma.crm_motivos.findFirst({ where: { empresa_id, tipo: dto.tipo, descripcion } });
    if (existente) {
      if (!existente.activo) return this.updateMotivo(existente.id, { activo: true }, empresa_id, user_id);
      throw new ConflictException(`Ya existe el motivo "${descripcion}" para ${dto.tipo}`);
    }

    const motivo = await this.prisma.crm_motivos.create({
      data: { empresa_id, tipo: dto.tipo, descripcion, orden: dto.orden ?? 0, created_by: user_id ?? null },
    });
    await this.auditar(empresa_id, user_id, 'CREAR', 'crm_motivos', motivo.id, undefined, motivo);
    return motivo;
  }

  async updateMotivo(id: string, dto: UpdateMotivoDto, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa('crm_motivos', id, empresa_id);
    const motivo = await this.prisma.crm_motivos.update({
      where: { id },
      data: {
        ...(dto.descripcion !== undefined ? { descripcion: dto.descripcion.trim() } : {}),
        ...(dto.orden !== undefined ? { orden: dto.orden } : {}),
        ...(dto.activo !== undefined ? { activo: dto.activo } : {}),
        updated_at: new Date(),
      },
    });
    await this.auditar(empresa_id, user_id, 'EDITAR', 'crm_motivos', id, anterior, motivo);
    return motivo;
  }

  /**
   * El tipo no se edita: un motivo de pérdida que pasa a ser de descarte dejaría
   * oportunidades históricas apuntando a un motivo que ya no les corresponde.
   */
  async removeMotivo(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa('crm_motivos', id, empresa_id);
    const motivo = await this.prisma.crm_motivos.update({ where: { id }, data: { activo: false, updated_at: new Date() } });
    await this.auditar(empresa_id, user_id, 'ELIMINAR', 'crm_motivos', id, anterior, motivo);
    return motivo;
  }

  // ── Calendarios ─────────────────────────────────────────────────────────────

  findCalendarios(empresa_id: string) {
    return this.prisma.crm_calendarios.findMany({
      where: { empresa_id },
      orderBy: [{ es_default: 'desc' }, { nombre: 'asc' }],
    });
  }

  async createCalendario(dto: CreateCalendarioDto, empresa_id: string, user_id?: string) {
    this.validarHorario(dto.horario);
    const nombre = dto.nombre.trim();
    if (await this.prisma.crm_calendarios.findFirst({ where: { empresa_id, nombre } })) {
      throw new ConflictException(`Ya existe un calendario llamado "${nombre}"`);
    }

    const calendario = await this.prisma.$transaction(async (tx) => {
      if (dto.es_default) await this.quitarDefault(tx, empresa_id);
      return tx.crm_calendarios.create({
        data: {
          empresa_id,
          nombre,
          zona_horaria: dto.zona_horaria ?? 'America/Asuncion',
          horario: dto.horario as unknown as Prisma.InputJsonValue,
          feriados: (dto.feriados ?? []) as Prisma.InputJsonValue,
          es_default: dto.es_default ?? false,
          created_by: user_id ?? null,
        },
      });
    });
    await this.auditar(empresa_id, user_id, 'CREAR', 'crm_calendarios', calendario.id, undefined, calendario);
    return calendario;
  }

  async updateCalendario(id: string, dto: UpdateCalendarioDto, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa('crm_calendarios', id, empresa_id);
    if (dto.horario) this.validarHorario(dto.horario);

    const calendario = await this.prisma.$transaction(async (tx) => {
      if (dto.es_default) await this.quitarDefault(tx, empresa_id, id);
      return tx.crm_calendarios.update({
        where: { id },
        data: {
          ...(dto.nombre !== undefined ? { nombre: dto.nombre.trim() } : {}),
          ...(dto.zona_horaria !== undefined ? { zona_horaria: dto.zona_horaria } : {}),
          ...(dto.horario !== undefined ? { horario: dto.horario as unknown as Prisma.InputJsonValue } : {}),
          ...(dto.feriados !== undefined ? { feriados: dto.feriados as Prisma.InputJsonValue } : {}),
          ...(dto.es_default !== undefined ? { es_default: dto.es_default } : {}),
          ...(dto.activo !== undefined ? { activo: dto.activo } : {}),
          updated_at: new Date(),
        },
      });
    });
    await this.auditar(empresa_id, user_id, 'EDITAR', 'crm_calendarios', id, anterior, calendario);
    return calendario;
  }

  async removeCalendario(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa('crm_calendarios', id, empresa_id);
    // Sin calendario por defecto el reloj de SLA no sabe cuándo correr.
    if (anterior.es_default) {
      throw new ConflictException('Es el calendario por defecto: marcá otro como default antes de darlo de baja');
    }
    const usado = await this.prisma.crm_sla_politicas.count({ where: { calendario_id: id, activo: true } });
    if (usado) {
      throw new ConflictException(`Lo usan ${usado} política(s) de SLA activas: cambialas primero`);
    }

    const calendario = await this.prisma.crm_calendarios.update({ where: { id }, data: { activo: false, updated_at: new Date() } });
    await this.auditar(empresa_id, user_id, 'ELIMINAR', 'crm_calendarios', id, anterior, calendario);
    return calendario;
  }

  // ── Políticas de SLA ────────────────────────────────────────────────────────

  findSla(empresa_id: string) {
    return this.prisma.crm_sla_politicas.findMany({
      where: { empresa_id },
      orderBy: [{ prioridad: 'asc' }, { nombre: 'asc' }],
    });
  }

  async createSla(dto: CreateSlaDto, empresa_id: string, user_id?: string) {
    this.validarTiempos(dto.min_primera_respuesta, dto.min_resolucion);
    const nombre = dto.nombre.trim();
    if (await this.prisma.crm_sla_politicas.findFirst({ where: { empresa_id, nombre } })) {
      throw new ConflictException(`Ya existe una política llamada "${nombre}"`);
    }
    if (dto.calendario_id) await this.deEmpresa('crm_calendarios', dto.calendario_id, empresa_id);

    const sla = await this.prisma.crm_sla_politicas.create({
      data: {
        empresa_id,
        nombre,
        prioridad: dto.prioridad,
        tipo_cliente: dto.tipo_cliente?.trim() || null,
        min_primera_respuesta: dto.min_primera_respuesta,
        min_resolucion: dto.min_resolucion,
        calendario_id: dto.calendario_id ?? null,
        created_by: user_id ?? null,
      },
    });
    await this.auditar(empresa_id, user_id, 'CREAR', 'crm_sla_politicas', sla.id, undefined, sla);
    return sla;
  }

  async updateSla(id: string, dto: UpdateSlaDto, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa('crm_sla_politicas', id, empresa_id);
    this.validarTiempos(
      dto.min_primera_respuesta ?? anterior.min_primera_respuesta,
      dto.min_resolucion ?? anterior.min_resolucion,
    );
    if (dto.calendario_id) await this.deEmpresa('crm_calendarios', dto.calendario_id, empresa_id);

    const sla = await this.prisma.crm_sla_politicas.update({
      where: { id },
      data: {
        ...(dto.nombre !== undefined ? { nombre: dto.nombre.trim() } : {}),
        ...(dto.prioridad !== undefined ? { prioridad: dto.prioridad } : {}),
        ...(dto.tipo_cliente !== undefined ? { tipo_cliente: dto.tipo_cliente?.trim() || null } : {}),
        ...(dto.min_primera_respuesta !== undefined ? { min_primera_respuesta: dto.min_primera_respuesta } : {}),
        ...(dto.min_resolucion !== undefined ? { min_resolucion: dto.min_resolucion } : {}),
        ...(dto.calendario_id !== undefined ? { calendario_id: dto.calendario_id } : {}),
        ...(dto.activo !== undefined ? { activo: dto.activo } : {}),
        updated_at: new Date(),
      },
    });
    await this.auditar(empresa_id, user_id, 'EDITAR', 'crm_sla_politicas', id, anterior, sla);
    return sla;
  }

  async removeSla(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa('crm_sla_politicas', id, empresa_id);
    const sla = await this.prisma.crm_sla_politicas.update({ where: { id }, data: { activo: false, updated_at: new Date() } });
    await this.auditar(empresa_id, user_id, 'ELIMINAR', 'crm_sla_politicas', id, anterior, sla);
    return sla;
  }

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

  /** Un tramo que termina antes de empezar deja el SLA sin horas hábiles y nada vence nunca. */
  private validarHorario(horario: { dia: number; desde: string; hasta: string }[]) {
    if (!horario.length) throw new BadRequestException('El calendario necesita al menos un tramo horario');
    for (const t of horario) {
      if (t.hasta <= t.desde) {
        throw new BadRequestException(`El tramo del día ${t.dia} termina (${t.hasta}) antes de empezar (${t.desde})`);
      }
    }
  }

  private validarTiempos(primeraRespuesta: number, resolucion: number) {
    if (resolucion < primeraRespuesta) {
      throw new BadRequestException('El tiempo de resolución no puede ser menor que el de primera respuesta');
    }
  }

  private quitarDefault(tx: Prisma.TransactionClient, empresa_id: string, excepto?: string) {
    return tx.crm_calendarios.updateMany({
      where: { empresa_id, es_default: true, ...(excepto ? { id: { not: excepto } } : {}) },
      data: { es_default: false },
    });
  }

  /** Un id de otra empresa tiene que ser un 404, no un 403: no se filtra que existe. */
  private async deEmpresa<T extends 'crm_origenes' | 'crm_motivos' | 'crm_calendarios' | 'crm_sla_politicas'>(
    tabla: T,
    id: string,
    empresa_id: string,
  ) {
    const fila = await (this.prisma[tabla] as { findFirst: (a: unknown) => Promise<unknown> }).findFirst({
      where: { id, empresa_id },
    });
    if (!fila) throw new NotFoundException('No encontrado en esta empresa');
    return fila as { es_default: boolean; min_primera_respuesta: number; min_resolucion: number };
  }

  private auditar(
    empresa_id: string, user_id: string | undefined, action: string,
    entity_type: string, entity_id: string, old_value?: unknown, new_value?: unknown,
  ) {
    return this.auditService.log({
      empresa_id, user_id, action, entity_type, entity_id,
      old_value: old_value as never, new_value: new_value as never,
    });
  }
}
