import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import {
  ClaveServicio,
  ConfigOperativaService,
  DEFECTOS,
  DEFECTOS_SERVICIO,
} from 'src/config-operativa/config-operativa.service';
import { envs } from 'src/config/envs';
import { firmaIncidencia, motivoCargable } from 'src/incidencias/decision';
import { YoutrackClient } from 'src/incidencias/youtrack.client';
import { PROMPT_VERSION } from 'src/pipeline/prompts';
import { ClaveEditable, CLAVES_EDITABLES, validarConfig } from './configuracion.helpers';
import { PrismaService } from 'src/prisma/prisma.service';

/**
 * Reporte para Novasis (plan §7.2). Solo lee la base propia: los tickets traen
 * el snapshot de la empresa, no hay joins contra la base del ERP.
 *
 * Autoresolución honesta (lección §0.2 del plan original): resueltos (con o sin
 * indicaciones; un 👎 ya los pasó a NO_RESUELTO) sobre consultas REALES, que
 * incluyen las abandonadas a mitad de camino. Los saludos no cuentan.
 */
export interface Rango {
  desde: Date;
  hasta: Date;
}

@Injectable()
export class ReporteService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly configOperativa: ConfigOperativaService,
    private readonly youtrack: YoutrackClient,
  ) {}

  // ── Configuración del agente (solo superadmin de Novasis) ───────────

  async configuracion() {
    const [filas, empresasConTickets, bandeja, conError] = await Promise.all([
      this.prisma.config.findMany(),
      this.prisma.tickets.groupBy({
        by: ['empresa_id', 'empresa_ruc', 'empresa_razon_social'],
        _count: { _all: true },
        orderBy: { _count: { empresa_id: 'desc' } },
        take: 200,
      }),
      this.prisma.incidencias.groupBy({ by: ['estado'], _count: { _all: true } }),
      this.prisma.incidencias.findFirst({
        where: { ultimo_error: { not: null } },
        orderBy: { updated_at: 'desc' },
        select: { ultimo_error: true, updated_at: true },
      }),
    ]);
    const valor = (clave: string, ambito: string) => filas.find((f) => f.clave === clave && f.ambito === ambito)?.valor;
    const bool = (v: string | undefined) => (v === undefined ? undefined : v.trim().toLowerCase() === 'true');
    return {
      global: {
        ia_activa: bool(valor('ia_activa', 'global')) ?? DEFECTOS.ia_activa === 'true',
        tope_tokens_diarios: Number(valor('tope_tokens_diarios', 'global') ?? DEFECTOS.tope_tokens_diarios),
        youtrack_activo: bool(valor('youtrack_activo', 'global')) ?? DEFECTOS.youtrack_activo === 'true',
      },
      // Empresas que usaron el agente, con sus excepciones (null = hereda lo global).
      empresas: empresasConTickets.map((e) => {
        const tope = valor('tope_tokens_diarios', `empresa:${e.empresa_id}`);
        return {
          empresa_id: e.empresa_id,
          ruc: e.empresa_ruc,
          razon_social: e.empresa_razon_social,
          tickets: e._count._all,
          ia_activa: bool(valor('ia_activa', `empresa:${e.empresa_id}`)) ?? null,
          tope_tokens_diarios: tope ? Number(tope) : null,
        };
      }),
      youtrack: {
        configurado: this.youtrack.disponible,
        url: envs.youtrack.url,
        proyecto: envs.youtrack.proyecto,
        bandeja: {
          pendientes: bandeja.find((b) => b.estado === 'PENDIENTE')?._count._all ?? 0,
          creadas: bandeja.find((b) => b.estado === 'CREADA')?._count._all ?? 0,
          con_error: bandeja.find((b) => b.estado === 'ERROR')?._count._all ?? 0,
          ultimo_error: conError?.ultimo_error ?? null,
        },
      },
      servicio: {
        prompt_version: PROMPT_VERSION,
        tope_por_defecto: Number(DEFECTOS.tope_tokens_diarios),
        // Editables (ámbito global): lo configurado o, si no hay fila, el valor del .env.
        campos: Object.fromEntries(
          (Object.keys(DEFECTOS_SERVICIO) as ClaveServicio[]).map((clave) => {
            const fila = valor(clave, 'global');
            return [
              clave,
              {
                valor: fila ?? DEFECTOS_SERVICIO[clave](),
                por_defecto: DEFECTOS_SERVICIO[clave](),
                editado: fila !== undefined,
              },
            ];
          }),
        ),
      },
    };
  }

  async guardarConfiguracion(clave: ClaveEditable, empresaId: string | null, valor: string | null) {
    if (!CLAVES_EDITABLES.includes(clave)) throw new BadRequestException('Clave desconocida.');
    const ambito = empresaId ? `empresa:${empresaId}` : 'global';
    if (valor === null || valor === undefined) {
      await this.prisma.config.deleteMany({ where: { clave, ambito } });
    } else {
      const limpio = validarConfig(clave, ambito, valor);
      await this.prisma.config.upsert({
        where: { clave_ambito: { clave, ambito } },
        create: { clave, ambito, valor: limpio },
        update: { valor: limpio },
      });
    }
    this.configOperativa.invalidar();
    return this.configuracion();
  }

  probarYoutrack() {
    return this.youtrack.probar();
  }

  async resumen({ desde, hasta }: Rango) {
    const enRango = Prisma.sql`created_at >= ${desde} AND created_at < ${hasta}`;

    const [totales] = await this.prisma.$queryRaw<
      Array<{
        tickets: bigint;
        consultas_reales: bigint;
        resueltos: bigint;
        con_indicaciones: bigint;
        no_resueltos: bigint;
        abandonados: bigint;
        abiertos: bigint;
        conformidad_positiva: bigint;
        conformidad_negativa: bigint;
        empresas: bigint;
      }>
    >`
      SELECT count(*)                                                            AS tickets,
             count(*) FILTER (WHERE es_consulta_real AND resultado IS DISTINCT FROM 'SIN_CONSULTA') AS consultas_reales,
             count(*) FILTER (WHERE resultado IN ('RESUELTO','RESUELTO_AUTOSERVICIO')) AS resueltos,
             count(*) FILTER (WHERE resultado = 'RESUELTO_AUTOSERVICIO')        AS con_indicaciones,
             count(*) FILTER (WHERE resultado = 'NO_RESUELTO')                  AS no_resueltos,
             count(*) FILTER (WHERE resultado = 'ABANDONADO')                   AS abandonados,
             count(*) FILTER (WHERE estado = 'ABIERTO')                         AS abiertos,
             count(*) FILTER (WHERE conformidad = 'POSITIVA')                   AS conformidad_positiva,
             count(*) FILTER (WHERE conformidad = 'NEGATIVA')                   AS conformidad_negativa,
             count(DISTINCT empresa_id)                                         AS empresas
        FROM tickets WHERE ${enRango}`;

    const porMotivo = await this.prisma.$queryRaw<Array<{ motivo: string; tickets: bigint; empresas: bigint }>>`
      SELECT motivo_no_resuelto::text AS motivo, count(*) AS tickets, count(DISTINCT empresa_id) AS empresas
        FROM tickets
       WHERE ${enRango} AND resultado = 'NO_RESUELTO'
       GROUP BY 1 ORDER BY 2 DESC`;

    const porCategoria = await this.prisma.$queryRaw<
      Array<{ categoria: string | null; tickets: bigint; resueltos: bigint }>
    >`
      SELECT categoria::text AS categoria, count(*) AS tickets,
             count(*) FILTER (WHERE resultado IN ('RESUELTO','RESUELTO_AUTOSERVICIO')) AS resueltos
        FROM tickets
       WHERE ${enRango} AND es_consulta_real
       GROUP BY 1 ORDER BY 2 DESC`;

    const porEmpresa = await this.prisma.$queryRaw<
      Array<{
        empresa_id: string;
        empresa: string | null;
        ruc: string | null;
        tickets: bigint;
        resueltos: bigint;
        no_resueltos: bigint;
        costo_usd: Prisma.Decimal | null;
      }>
    >`
      SELECT t.empresa_id, max(t.empresa_razon_social) AS empresa, max(t.empresa_ruc) AS ruc,
             count(*) AS tickets,
             count(*) FILTER (WHERE t.resultado IN ('RESUELTO','RESUELTO_AUTOSERVICIO')) AS resueltos,
             count(*) FILTER (WHERE t.resultado = 'NO_RESUELTO') AS no_resueltos,
             (SELECT sum(c.costo_usd) FROM consumo_diario c
               WHERE c.empresa_id = t.empresa_id AND c.fecha >= ${desde}::date
                 AND c.fecha <= (${hasta}::timestamptz - interval '1 millisecond')::date) AS costo_usd
        FROM tickets t
       WHERE t.created_at >= ${desde} AND t.created_at < ${hasta} AND t.es_consulta_real
       GROUP BY t.empresa_id
       ORDER BY count(*) DESC
       LIMIT 50`;

    const [latencia] = await this.prisma.$queryRaw<
      Array<{ p50: number | null; p95: number | null; respuestas: bigint; costo_usd: Prisma.Decimal | null }>
    >`
      SELECT percentile_cont(0.5)  WITHIN GROUP (ORDER BY primer_token_ms) AS p50,
             percentile_cont(0.95) WITHIN GROUP (ORDER BY primer_token_ms) AS p95,
             count(*) AS respuestas,
             sum(costo_usd) AS costo_usd
        FROM mensajes
       WHERE autor = 'bot' AND created_at >= ${desde} AND created_at < ${hasta}`;

    const [alcance] = await this.prisma.$queryRaw<Array<{ fuera_de_alcance: bigint }>>`
      SELECT count(*) AS fuera_de_alcance FROM trazas
       WHERE fuera_de_alcance AND created_at >= ${desde} AND created_at < ${hasta}`;

    const n = (v: bigint | number | null | undefined) => Number(v ?? 0);
    const consultas = n(totales.consultas_reales);
    const resueltos = n(totales.resueltos);
    const respondidas = n(totales.conformidad_positiva) + n(totales.conformidad_negativa);

    return {
      rango: { desde, hasta },
      totales: {
        tickets: n(totales.tickets),
        consultas_reales: consultas,
        resueltos,
        con_indicaciones: n(totales.con_indicaciones),
        no_resueltos: n(totales.no_resueltos),
        abandonados: n(totales.abandonados),
        abiertos: n(totales.abiertos),
        empresas: n(totales.empresas),
        autoresolucion: consultas ? resueltos / consultas : null,
        conformidad_positiva: n(totales.conformidad_positiva),
        conformidad_negativa: n(totales.conformidad_negativa),
        tasa_conformidad_positiva: respondidas ? n(totales.conformidad_positiva) / respondidas : null,
        fuera_de_alcance: n(alcance.fuera_de_alcance),
      },
      latencia: {
        primer_texto_p50_ms: latencia.p50 === null ? null : Math.round(latencia.p50),
        primer_texto_p95_ms: latencia.p95 === null ? null : Math.round(latencia.p95),
        respuestas: n(latencia.respuestas),
      },
      costo_usd: Number(latencia.costo_usd ?? 0),
      no_resueltos_por_motivo: porMotivo.map((m) => ({
        motivo: m.motivo,
        tickets: n(m.tickets),
        empresas: n(m.empresas),
      })),
      por_categoria: porCategoria.map((c) => ({
        categoria: c.categoria ?? 'sin_categoria',
        tickets: n(c.tickets),
        resueltos: n(c.resueltos),
      })),
      por_empresa: porEmpresa.map((e) => ({
        empresa_id: e.empresa_id,
        empresa: e.empresa,
        ruc: e.ruc,
        tickets: n(e.tickets),
        resueltos: n(e.resueltos),
        no_resueltos: n(e.no_resueltos),
        costo_usd: Number(e.costo_usd ?? 0),
      })),
      fallas_en_varias_empresas: await this.fallasEnVariasEmpresas(),
    };
  }

  /**
   * Un mismo motivo de NO_RESUELTO en 2 o más empresas en los últimos 7 días:
   * probablemente es una falla de la plataforma, no un problema de cada cliente
   * (plan §10, fase 2). Siempre mira la última semana, sin importar el rango.
   */
  async fallasEnVariasEmpresas() {
    const filas = await this.prisma.$queryRaw<
      Array<{ motivo: string; empresas: bigint; tickets: bigint; ultimo: Date; nombres: string[] }>
    >`
      SELECT motivo_no_resuelto::text AS motivo,
             count(DISTINCT empresa_id) AS empresas,
             count(*) AS tickets,
             max(created_at) AS ultimo,
             (array_agg(DISTINCT coalesce(empresa_razon_social, empresa_ruc, empresa_id::text)))[1:5] AS nombres
        FROM tickets
       WHERE resultado = 'NO_RESUELTO'
         AND motivo_no_resuelto IS NOT NULL
         AND motivo_no_resuelto <> 'cliente_disconforme'
         AND created_at >= now() - interval '7 days'
       GROUP BY 1
      HAVING count(DISTINCT empresa_id) >= 2
       ORDER BY 2 DESC, 3 DESC`;
    return filas.map((f) => ({
      motivo: f.motivo,
      empresas: Number(f.empresas),
      tickets: Number(f.tickets),
      ultimo: f.ultimo,
      ejemplos: f.nombres,
    }));
  }

  async tickets(filtros: {
    desde: Date;
    hasta: Date;
    resultado?: string;
    motivo?: string;
    empresaId?: string;
    pagina: number;
  }) {
    const tam = 30;
    const where: Prisma.ticketsWhereInput = {
      created_at: { gte: filtros.desde, lt: filtros.hasta },
      ...(filtros.resultado ? { resultado: filtros.resultado as Prisma.ticketsWhereInput['resultado'] } : {}),
      ...(filtros.motivo
        ? { motivo_no_resuelto: filtros.motivo as Prisma.ticketsWhereInput['motivo_no_resuelto'] }
        : {}),
      ...(filtros.empresaId ? { empresa_id: filtros.empresaId } : {}),
    };
    const [total, filas] = await Promise.all([
      this.prisma.tickets.count({ where }),
      this.prisma.tickets.findMany({
        where,
        orderBy: { created_at: 'desc' },
        skip: (filtros.pagina - 1) * tam,
        take: tam,
        select: {
          id: true,
          numero: true,
          titulo: true,
          empresa_id: true,
          empresa_razon_social: true,
          empresa_ruc: true,
          usuario_nombre: true,
          estado: true,
          resultado: true,
          motivo_no_resuelto: true,
          categoria: true,
          conformidad: true,
          nro_documento: true,
          created_at: true,
        },
      }),
    ]);
    return { total, pagina: filtros.pagina, tam, tickets: filas };
  }

  /** El hilo completo con las trazas: qué consultó el agente y qué vio en cada turno. */
  async detalle(id: string) {
    const t = await this.prisma.tickets.findUnique({
      where: { id },
      include: {
        mensajes: { orderBy: { orden: 'asc' }, include: { adjuntos: { select: { id: true, tipo: true } } } },
        trazas: { orderBy: { created_at: 'asc' } },
      },
    });
    if (!t) throw new NotFoundException('Ticket no encontrado');
    // La incidencia de YouTrack del problema de este ticket, si el motivo era accionable.
    const incidencia = motivoCargable(t.motivo_no_resuelto)
      ? await this.prisma.incidencias.findUnique({
          where: { firma: firmaIncidencia(t.motivo_no_resuelto, t.categoria) },
          select: { youtrack_id: true, estado: true, ocurrencias: true, prioridad: true, ultimo_error: true },
        })
      : null;
    return {
      ...t,
      incidencia,
      mensajes: t.mensajes.map((m) => ({ ...m, costo_usd: m.costo_usd === null ? null : Number(m.costo_usd) })),
    };
  }

  async adjunto(ticketId: string, adjuntoId: string) {
    const a = await this.prisma.adjuntos.findFirst({ where: { id: adjuntoId, ticket_id: ticketId } });
    if (!a) throw new NotFoundException('Captura no encontrada');
    return a;
  }

  /** Etiqueta de calidad de una respuesta del agente (golden set, plan §9.2). Nunca la ve el cliente. */
  async etiquetar(ticketId: string, orden: number, correcta: boolean | null) {
    const m = await this.prisma.mensajes.findUnique({ where: { ticket_id_orden: { ticket_id: ticketId, orden } } });
    if (!m || m.autor !== 'bot') throw new NotFoundException('Respuesta del agente no encontrada');
    await this.prisma.mensajes.update({
      where: { ticket_id_orden: { ticket_id: ticketId, orden } },
      data: { correcta },
    });
    return { ticket_id: ticketId, orden, correcta };
  }
}
