import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { inicioMesUTC, resolverHoldingId } from 'src/ai-usage/ai-common.util';

@Injectable()
export class AiHoldingService {
  constructor(private readonly prisma: PrismaService) {}

  /** Autorización: solo el holding raíz (empresa sin padre) administra IA de la red. */
  private async assertEsHolding(empresaId: string): Promise<void> {
    const e = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { parent_id: true },
    });
    if (e?.parent_id) {
      throw new ForbiddenException('Solo el holding puede administrar la IA de la red.');
    }
  }

  /** Red del holding: el holding + todos sus descendientes (por parent_id, BFS). */
  private async empresasDeLaRed(holdingId: string) {
    const select = { id: true, razon_social: true, nombre_fantasia: true, type: true } as const;
    const raiz = await this.prisma.empresas.findUnique({ where: { id: holdingId }, select });
    const red = raiz ? [raiz] : [];
    let frontera = [holdingId];
    for (let nivel = 0; nivel < 12 && frontera.length; nivel++) {
      const hijos = await this.prisma.empresas.findMany({ where: { parent_id: { in: frontera } }, select });
      if (!hijos.length) break;
      red.push(...hijos);
      frontera = hijos.map((h) => h.id);
    }
    return red;
  }

  /** Consumo de IA del mes por empresa de la red + su cuota/modo/plan. */
  async consumoRed(empresaIdActual: string) {
    await this.assertEsHolding(empresaIdActual);
    const holdingId = await resolverHoldingId(this.prisma, empresaIdActual);
    const empresas = await this.empresasDeLaRed(holdingId);
    const ids = empresas.map((e) => e.id);
    const inicio = inicioMesUTC();

    // Credencial de plataforma = config del holding (lo que usan las empresas en modo PLATAFORMA).
    const holdingCfg = await this.prisma.ai_empresa_config.findUnique({
      where: { empresa_id: holdingId },
      select: { proveedor: true, modelo: true, api_key_encrypted: true, activo: true },
    });
    // Tarifa del modelo de plataforma → para estimar el costo USD de una cuota de tokens.
    const tarifaPlat =
      holdingCfg?.proveedor && holdingCfg?.modelo
        ? await this.prisma.ai_tarifas.findUnique({
            where: { proveedor_modelo: { proveedor: holdingCfg.proveedor, modelo: holdingCfg.modelo } },
          })
        : null;

    const [consumo, configs, subs] = await Promise.all([
      this.prisma.ai_consumo.groupBy({
        by: ['empresa_id'],
        where: { empresa_id: { in: ids }, creado_en: { gte: inicio } },
        _sum: { tokens_total: true, costo_usd: true },
      }),
      this.prisma.ai_empresa_config.findMany({
        where: { empresa_id: { in: ids } },
        select: { empresa_id: true, limite_tokens_mensual: true, credencial_modo: true, origen_limite: true, activo: true },
      }),
      this.prisma.suscripciones.findMany({
        where: { empresa_id: { in: ids }, estado: { in: ['Activo', 'EnGracia'] } },
        select: { empresa_id: true, planes: { select: { descripcion: true, tokens_ia_mensual: true } } },
        orderBy: { fecha_inicio: 'desc' },
      }),
    ]);

    const consumoBy = new Map(consumo.map((c) => [c.empresa_id, c]));
    const cfgBy = new Map(configs.map((c) => [c.empresa_id, c]));
    const subBy = new Map<string, (typeof subs)[number]>();
    for (const s of subs) if (!subBy.has(s.empresa_id)) subBy.set(s.empresa_id, s);

    const filas = empresas.map((e) => {
      const cons = consumoBy.get(e.id);
      const cfg = cfgBy.get(e.id);
      const sub = subBy.get(e.id);
      const consumido = cons?._sum.tokens_total ?? 0;
      const limite = cfg?.limite_tokens_mensual ?? 0;
      return {
        empresa_id: e.id,
        nombre: e.nombre_fantasia || e.razon_social || '(sin nombre)',
        es_holding: e.id === holdingId,
        consumido_mes: consumido,
        costo_usd_mes: Number(cons?._sum.costo_usd ?? 0),
        limite_tokens_mensual: limite,
        ilimitado: !limite || limite <= 0,
        porcentaje_usado: limite > 0 ? Math.min(100, Math.round((consumido / limite) * 100)) : 0,
        credencial_modo: cfg?.credencial_modo ?? 'PROPIA',
        origen_limite: cfg?.origen_limite ?? 'MANUAL',
        ia_activa: cfg?.activo ?? false,
        plan: sub?.planes?.descripcion ?? null,
        cuota_plan: sub?.planes?.tokens_ia_mensual ?? 0,
      };
    });

    const totalTokens = filas.reduce((t, f) => t + f.consumido_mes, 0);
    const totalCosto = filas.reduce((t, f) => t + f.costo_usd_mes, 0);
    return {
      periodo_desde: inicio.toISOString().slice(0, 10),
      total_tokens_mes: totalTokens,
      total_costo_usd_mes: Math.round(totalCosto * 1e6) / 1e6,
      // Credencial que usan las empresas en modo PLATAFORMA (config del holding).
      plataforma: {
        proveedor: holdingCfg?.proveedor ?? null,
        modelo: holdingCfg?.modelo ?? null,
        tiene_key: !!holdingCfg?.api_key_encrypted,
        activa: holdingCfg?.activo ?? false,
        // Tarifa del modelo (USD/1M) para estimar el costo de una cuota de tokens.
        precio_in_por_1m: tarifaPlat ? Number(tarifaPlat.precio_in_por_1m) : null,
        precio_out_por_1m: tarifaPlat ? Number(tarifaPlat.precio_out_por_1m) : null,
        // Precio "mezcla" (promedio in/out) para una estimación rápida de tokens totales.
        precio_blend_por_1m: tarifaPlat
          ? Math.round(((Number(tarifaPlat.precio_in_por_1m) + Number(tarifaPlat.precio_out_por_1m)) / 2) * 10000) / 10000
          : null,
      },
      empresas: filas.sort((a, b) => b.consumido_mes - a.consumido_mes),
    };
  }

  // ── Tarifas de IA (nivel sistema; para el cálculo de costo USD) ──────────────
  /** Lista las tarifas + detecta modelos con consumo pero SIN tarifa cargada. */
  async listarTarifas(empresaIdActual: string) {
    await this.assertEsHolding(empresaIdActual);
    const [tarifas, consumidos] = await Promise.all([
      this.prisma.ai_tarifas.findMany({ orderBy: [{ proveedor: 'asc' }, { modelo: 'asc' }] }),
      this.prisma.ai_consumo.groupBy({ by: ['proveedor', 'modelo'], _sum: { tokens_total: true } }),
    ]);
    const key = (p: string, m: string) => `${(p || '').toLowerCase()}::${(m || '').toLowerCase()}`;
    const conTarifa = new Set(tarifas.map((t) => key(t.proveedor, t.modelo)));
    const sinTarifa = consumidos
      .filter((c) => !conTarifa.has(key(c.proveedor, c.modelo)))
      .map((c) => ({ proveedor: c.proveedor, modelo: c.modelo, tokens: c._sum.tokens_total ?? 0 }))
      .sort((a, b) => b.tokens - a.tokens);
    return {
      tarifas: tarifas.map((t) => ({
        id: t.id,
        proveedor: t.proveedor,
        modelo: t.modelo,
        precio_in_por_1m: Number(t.precio_in_por_1m),
        precio_out_por_1m: Number(t.precio_out_por_1m),
        activo: t.activo,
      })),
      sin_tarifa: sinTarifa,
    };
  }

  /** Crea o actualiza una tarifa por proveedor+modelo. Solo el holding raíz (tabla global). */
  async upsertTarifa(empresaIdActual: string, dto: { proveedor: string; modelo: string; precio_in_por_1m: number; precio_out_por_1m: number; activo?: boolean }) {
    await this.assertEsHolding(empresaIdActual);
    const data = {
      precio_in_por_1m: dto.precio_in_por_1m,
      precio_out_por_1m: dto.precio_out_por_1m,
      activo: dto.activo ?? true,
      updated_at: new Date(),
    };
    await this.prisma.ai_tarifas.upsert({
      where: { proveedor_modelo: { proveedor: dto.proveedor, modelo: dto.modelo } },
      update: data,
      create: { proveedor: dto.proveedor, modelo: dto.modelo, ...data },
    });
    return { ok: true };
  }

  async eliminarTarifa(empresaIdActual: string, id: string) {
    await this.assertEsHolding(empresaIdActual);
    await this.prisma.ai_tarifas.delete({ where: { id } });
    return { ok: true };
  }

  /**
   * Recalcula el costo_usd del consumo del MES en curso (red del holding) según las
   * tarifas actuales. El costo se sella al momento de la llamada; esto lo actualiza
   * tras cargar/cambiar tarifas. Rows sin tarifa activa → costo null.
   */
  async recalcularCostos(empresaIdActual: string) {
    await this.assertEsHolding(empresaIdActual);
    const holdingId = await resolverHoldingId(this.prisma, empresaIdActual);
    const red = await this.empresasDeLaRed(holdingId);
    const ids = red.map((e) => e.id);
    if (!ids.length) return { actualizados: 0 };
    const inicio = inicioMesUTC();

    // Con tarifa activa → recalcula; sin tarifa → deja el costo en null.
    const conTarifa = await this.prisma.$executeRaw(Prisma.sql`
      UPDATE ai_consumo c
      SET costo_usd = ROUND((c.tokens_in::numeric / 1000000 * t.precio_in_por_1m
                          + c.tokens_out::numeric / 1000000 * t.precio_out_por_1m)::numeric, 8)
      FROM ai_tarifas t
      WHERE lower(c.proveedor) = lower(t.proveedor)
        AND lower(c.modelo) = lower(t.modelo)
        AND t.activo = true
        AND c.empresa_id IN (${Prisma.join(ids)})
        AND c.creado_en >= ${inicio}
    `);

    await this.prisma.$executeRaw(Prisma.sql`
      UPDATE ai_consumo c SET costo_usd = NULL
      WHERE c.empresa_id IN (${Prisma.join(ids)})
        AND c.creado_en >= ${inicio}
        AND NOT EXISTS (
          SELECT 1 FROM ai_tarifas t
          WHERE lower(t.proveedor) = lower(c.proveedor) AND lower(t.modelo) = lower(c.modelo) AND t.activo = true
        )
    `);

    return { actualizados: Number(conTarifa) };
  }

  /** Ajusta la cuota/modo de una empresa de la red (override manual del holding). */
  async setCuota(
    empresaIdActual: string,
    targetEmpresaId: string,
    dto: { limite_tokens_mensual?: number; credencial_modo?: string },
  ) {
    await this.assertEsHolding(empresaIdActual);
    const holdingId = await resolverHoldingId(this.prisma, empresaIdActual);
    const red = await this.empresasDeLaRed(holdingId);
    if (!red.some((e) => e.id === targetEmpresaId)) {
      throw new BadRequestException('La empresa no pertenece a tu red.');
    }
    const data: Record<string, unknown> = { origen_limite: 'MANUAL', updated_at: new Date() };
    if (dto.limite_tokens_mensual !== undefined) {
      data.limite_tokens_mensual = Math.max(0, Math.round(dto.limite_tokens_mensual));
    }
    if (dto.credencial_modo && ['PROPIA', 'PLATAFORMA'].includes(dto.credencial_modo)) {
      data.credencial_modo = dto.credencial_modo;
    }
    await this.prisma.ai_empresa_config.upsert({
      where: { empresa_id: targetEmpresaId },
      update: data,
      create: {
        empresa_id: targetEmpresaId,
        limite_tokens_mensual: (data.limite_tokens_mensual as number) ?? 0,
        credencial_modo: (data.credencial_modo as string) ?? 'PLATAFORMA',
        origen_limite: 'MANUAL',
      },
    });
    return { ok: true };
  }
}
