import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { CheckGeoDto, CompletarActividadDto, CreateActividadDto, UpdateActividadDto } from './dto/actividad.dto';

/** Días sin actividad por defecto, cuando ni la etapa ni los parámetros lo fijan. */
const DIAS_ESTANCAMIENTO_FALLBACK = 7;

/**
 * Actividades, agenda y visitas (M94). Ver docs/spec-crm-360.md §6.5.
 *
 * Esta tabla es la que mantiene vivo el pipeline: de acá salen
 * `ultima_actividad_at` (estancamiento) y `proxima_actividad_at` (la disciplina
 * de "siempre una próxima actividad"). Las dos se actualizan desde el servicio
 * y no por trigger: el día que un número no cuadre, un trigger invisible es lo
 * último donde alguien va a mirar.
 */
@Injectable()
export class ActividadesService {
  private readonly logger = new Logger(ActividadesService.name);

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

  /** Recalcula los dos sellos de la oportunidad a partir de sus actividades. */
  private async refrescarSellos(oportunidad_id: string, tx?: Prisma.TransactionClient) {
    const c = tx ?? this.prisma;

    const ultima = await c.crm_actividades.findFirst({
      where: { oportunidad_id, estado: 'realizada' },
      orderBy: { fecha_realizada: 'desc' },
      select: { fecha_realizada: true },
    });
    const proxima = await c.crm_actividades.findFirst({
      where: { oportunidad_id, estado: 'pendiente' },
      orderBy: { fecha_programada: 'asc' },
      select: { fecha_programada: true },
    });

    await c.crm_oportunidades.update({
      where: { id: oportunidad_id },
      data: {
        ultima_actividad_at: ultima?.fecha_realizada ?? null,
        proxima_actividad_at: proxima?.fecha_programada ?? null,
        // Cualquier movimiento la saca de estancada; el job la volverá a marcar
        // si corresponde. Dejarla roja tras registrar una llamada sería mentir.
        ...(ultima ? { estancada: false, estancada_desde: null } : {}),
      },
    });
  }

  private async deEmpresa(id: string, empresa_id: string) {
    const act = await this.prisma.crm_actividades.findFirst({ where: { id, empresa_id } });
    if (!act) throw new NotFoundException('Actividad no encontrada');
    return act;
  }

  async create(dto: CreateActividadDto, empresa_id: string, user_id?: string) {
    if (!dto.oportunidad_id && !dto.cliente_id && !dto.lead_id && !dto.ticket_id) {
      throw new BadRequestException('La actividad tiene que colgar de una oportunidad, cliente, lead o ticket');
    }

    if (dto.oportunidad_id) {
      const existe = await this.prisma.crm_oportunidades.findFirst({
        where: { id: dto.oportunidad_id, empresa_id },
        select: { id: true },
      });
      if (!existe) throw new NotFoundException('La oportunidad no existe en esta empresa');
    }

    const actividad = await this.prisma.crm_actividades.create({
      data: {
        empresa_id,
        sucursal_id: dto.sucursal_id ?? null,
        tipo: dto.tipo,
        asunto: dto.asunto.trim(),
        detalle: dto.detalle?.trim() || null,
        oportunidad_id: dto.oportunidad_id ?? null,
        cliente_id: dto.cliente_id ?? null,
        lead_id: dto.lead_id ?? null,
        ticket_id: dto.ticket_id ?? null,
        usuario_id: dto.usuario_id ?? user_id ?? null,
        vendedor_id: dto.vendedor_id ?? null,
        fecha_programada: new Date(dto.fecha_programada),
        created_by: user_id ?? null,
      },
    });

    if (actividad.oportunidad_id) await this.refrescarSellos(actividad.oportunidad_id);

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

  async update(id: string, dto: UpdateActividadDto, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa(id, empresa_id);
    if (anterior.estado !== 'pendiente') {
      throw new ConflictException(`La actividad está ${anterior.estado} y ya no se edita`);
    }

    const actividad = await this.prisma.crm_actividades.update({
      where: { id },
      data: {
        ...(dto.asunto !== undefined ? { asunto: dto.asunto.trim() } : {}),
        ...(dto.detalle !== undefined ? { detalle: dto.detalle?.trim() || null } : {}),
        ...(dto.fecha_programada !== undefined ? { fecha_programada: new Date(dto.fecha_programada) } : {}),
        ...(dto.usuario_id !== undefined ? { usuario_id: dto.usuario_id } : {}),
        updated_at: new Date(),
      },
    });

    if (actividad.oportunidad_id) await this.refrescarSellos(actividad.oportunidad_id);
    await this.auditService.log({
      empresa_id, user_id, action: 'EDITAR', entity_type: 'crm_actividades',
      entity_id: id, old_value: anterior, new_value: actividad,
    });
    return actividad;
  }

  /**
   * Completa la actividad. El resultado es obligatorio y lo refuerza un CHECK
   * en la base: es lo primero que se saltea si se permite cerrar en lote.
   *
   * Devuelve `sugerir_proxima` cuando la oportunidad queda sin nada agendado:
   * es la regla de disciplina comercial de la spec, pero se informa en vez de
   * bloquear — frenar al vendedor justo cuando viene de cerrar una visita es la
   * forma más rápida de que deje de registrar actividades.
   */
  async completar(id: string, dto: CompletarActividadDto, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa(id, empresa_id);
    if (anterior.estado === 'realizada') throw new ConflictException('La actividad ya estaba realizada');
    if (anterior.estado === 'cancelada') throw new ConflictException('La actividad está cancelada');

    const resultado = dto.resultado?.trim();
    if (!resultado) throw new BadRequestException('El resultado es obligatorio para completar la actividad');

    const actividad = await this.prisma.crm_actividades.update({
      where: { id },
      data: {
        estado: 'realizada',
        resultado,
        fecha_realizada: dto.fecha_realizada ? new Date(dto.fecha_realizada) : new Date(),
        updated_at: new Date(),
      },
    });

    let sugerir_proxima = false;
    if (actividad.oportunidad_id) {
      await this.refrescarSellos(actividad.oportunidad_id);
      const opo = await this.prisma.crm_oportunidades.findUnique({
        where: { id: actividad.oportunidad_id },
        select: { estado: true, proxima_actividad_at: true },
      });
      sugerir_proxima = opo?.estado === 'abierta' && !opo?.proxima_actividad_at;
    }

    await this.auditService.log({
      empresa_id, user_id, action: 'CONFIRMAR', entity_type: 'crm_actividades',
      entity_id: id, old_value: anterior, new_value: actividad,
    });
    return { ...actividad, sugerir_proxima };
  }

  async cancelar(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa(id, empresa_id);
    if (anterior.estado === 'realizada') throw new ConflictException('Una actividad realizada no se cancela');

    const actividad = await this.prisma.crm_actividades.update({
      where: { id },
      data: { estado: 'cancelada', updated_at: new Date() },
    });
    if (actividad.oportunidad_id) await this.refrescarSellos(actividad.oportunidad_id);

    await this.auditService.log({
      empresa_id, user_id, action: 'CANCELAR', entity_type: 'crm_actividades',
      entity_id: id, old_value: anterior, new_value: actividad,
    });
    return actividad;
  }

  /** Check-in de visita. La posición se guarda una sola vez: la de llegada. */
  async checkIn(id: string, dto: CheckGeoDto, empresa_id: string) {
    const act = await this.deEmpresa(id, empresa_id);
    if (act.tipo !== 'visita') throw new BadRequestException('El check-in es sólo para visitas');
    if (act.checkin_at) throw new ConflictException('La visita ya tiene check-in');

    return this.prisma.crm_actividades.update({
      where: { id },
      data: {
        checkin_at: new Date(),
        geo_lat: new Prisma.Decimal(dto.geo_lat),
        geo_lng: new Prisma.Decimal(dto.geo_lng),
        updated_at: new Date(),
      },
    });
  }

  async checkOut(id: string, empresa_id: string) {
    const act = await this.deEmpresa(id, empresa_id);
    if (!act.checkin_at) throw new ConflictException('No se puede cerrar una visita sin check-in');
    if (act.checkout_at) throw new ConflictException('La visita ya tiene check-out');

    return this.prisma.crm_actividades.update({
      where: { id },
      data: { checkout_at: new Date(), updated_at: new Date() },
    });
  }

  /** Agenda: lo pendiente de un usuario en una ventana de fechas. */
  async agenda(empresa_id: string, usuario_id: string, desde?: string, hasta?: string) {
    return this.prisma.crm_actividades.findMany({
      where: {
        empresa_id,
        usuario_id,
        estado: 'pendiente',
        ...(desde || hasta
          ? {
              fecha_programada: {
                ...(desde ? { gte: new Date(desde) } : {}),
                ...(hasta ? { lte: new Date(hasta) } : {}),
              },
            }
          : {}),
      },
      orderBy: { fecha_programada: 'asc' },
    });
  }

  async findPorOportunidad(oportunidad_id: string, empresa_id: string) {
    return this.prisma.crm_actividades.findMany({
      where: { oportunidad_id, empresa_id },
      orderBy: [{ estado: 'asc' }, { fecha_programada: 'desc' }],
    });
  }

  /**
   * Cuerpo del job diario de estancamiento.
   *
   * El umbral sale de la etapa (`dias_estancamiento`) y, si la etapa no lo fija,
   * de `crm_parametros.dias_estancamiento_default`. Una oportunidad sin ninguna
   * actividad realizada se mide desde su creación: si no, un negocio que nadie
   * tocó nunca jamás se marcaría, que es justo el caso más grave.
   */
  async marcarEstancadas(hoy: Date = new Date()): Promise<{ marcadas: number; liberadas: number }> {
    const abiertas = await this.prisma.crm_oportunidades.findMany({
      where: { estado: 'abierta' },
      select: {
        id: true, empresa_id: true, created_at: true, ultima_actividad_at: true, estancada: true,
        crm_etapas: { select: { dias_estancamiento: true } },
      },
    });
    if (!abiertas.length) return { marcadas: 0, liberadas: 0 };

    const parametros = await this.prisma.crm_parametros.findMany({
      select: { empresa_id: true, dias_estancamiento_default: true },
    });
    const porEmpresa = new Map(parametros.map((p) => [p.empresa_id, p.dias_estancamiento_default]));

    const marcar: string[] = [];
    const liberar: string[] = [];

    for (const o of abiertas) {
      const dias = o.crm_etapas?.dias_estancamiento ?? porEmpresa.get(o.empresa_id) ?? DIAS_ESTANCAMIENTO_FALLBACK;
      const referencia = o.ultima_actividad_at ?? o.created_at;
      if (!referencia) continue;

      const transcurridos = Math.floor((hoy.getTime() - new Date(referencia).getTime()) / 86_400_000);
      const deberia = transcurridos >= dias;

      if (deberia && !o.estancada) marcar.push(o.id);
      else if (!deberia && o.estancada) liberar.push(o.id);
    }

    if (marcar.length) {
      await this.prisma.crm_oportunidades.updateMany({
        where: { id: { in: marcar } },
        data: { estancada: true, estancada_desde: hoy },
      });
    }
    if (liberar.length) {
      await this.prisma.crm_oportunidades.updateMany({
        where: { id: { in: liberar } },
        data: { estancada: false, estancada_desde: null },
      });
    }

    if (marcar.length || liberar.length) {
      this.logger.log(`Estancamiento: ${marcar.length} marcadas, ${liberar.length} liberadas`);
    }
    return { marcadas: marcar.length, liberadas: liberar.length };
  }
}
