import {
  BadRequestException,
  Injectable,
  InternalServerErrorException,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
import { PrismaService } from 'src/prisma/prisma.service';
import { resolverHoldingId } from 'src/ai-usage/ai-common.util';
import { UpsertAiConfigDto } from './dto/upsert-ai-config.dto';
import { envs } from 'src/config';
import { esErrorParametroTokens, mensajeAmigableIA } from '../shared/ai-error.util';

const ALGORITHM = 'aes-256-cbc';
const IV_LENGTH = 16;

function getEncryptionKey(): Buffer {
  const raw = envs.aiEncryptionKey ?? '';
  if (raw.length !== 32) {
    throw new InternalServerErrorException('AI_ENCRYPTION_KEY debe tener exactamente 32 caracteres');
  }
  return Buffer.from(raw, 'utf8');
}

function encrypt(text: string): string {
  const key = getEncryptionKey();
  const iv = randomBytes(IV_LENGTH);
  const cipher = createCipheriv(ALGORITHM, key, iv);
  const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
  return `${iv.toString('hex')}:${encrypted.toString('hex')}`;
}

function decrypt(encrypted: string): string {
  const key = getEncryptionKey();
  const [ivHex, dataHex] = encrypted.split(':');
  const iv = Buffer.from(ivHex, 'hex');
  const data = Buffer.from(dataHex, 'hex');
  const decipher = createDecipheriv(ALGORITHM, key, iv);
  const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
  return decrypted.toString('utf8');
}

@Injectable()
export class AiConfigService {
  private readonly logger = new Logger(AiConfigService.name);

  constructor(private readonly prisma: PrismaService) {}

  async upsertConfig(empresaId: string, dto: UpsertAiConfigDto) {
    try {
      const existing = await this.prisma.ai_empresa_config.findUnique({
        where: { empresa_id: empresaId },
      });

      let apiKeyEncrypted = existing?.api_key_encrypted ?? null;
      if (dto.api_key) {
        apiKeyEncrypted = encrypt(dto.api_key);
      }

      const data = {
        proveedor: dto.proveedor,
        modelo: dto.modelo,
        api_key_encrypted: apiKeyEncrypted,
        // Aceptar ambos nombres: cron_expression (frontend) o frecuencia_cron (legacy)
        frecuencia_cron: dto.cron_expression ?? dto.frecuencia_cron ?? existing?.frecuencia_cron ?? '0 2 * * *',
        recalculo_automatico: dto.recalculo_automatico ?? existing?.recalculo_automatico ?? true,
        // Umbrales — aceptar nombres nuevos y legacy
        umbral_alerta_mora: dto.umbral_mora_pct ?? dto.umbral_alerta_mora ?? existing?.umbral_alerta_mora ?? 30,
        umbral_stock_bajo: dto.umbral_stock_bajo ?? existing?.umbral_stock_bajo ?? 10,
        umbral_venta_alta: dto.umbral_saldo_critico ?? dto.umbral_venta_alta ?? existing?.umbral_venta_alta ?? 5000000,
        limite_tokens_mensual: dto.limite_tokens_mensual ?? existing?.limite_tokens_mensual ?? 0,
        credencial_modo: dto.credencial_modo ?? existing?.credencial_modo ?? 'PROPIA',
        activo: dto.activo ?? !!apiKeyEncrypted,
        updated_at: new Date(),
      };

      const config = await this.prisma.ai_empresa_config.upsert({
        where: { empresa_id: empresaId },
        update: data,
        create: { ...data, empresa_id: empresaId },
      });

      return this._sanitize(config);
    } catch (error) {
      this.logger.error('Error en upsertConfig', error);
      throw error;
    }
  }

  async getConfig(empresaId: string) {
    const config = await this.prisma.ai_empresa_config.findUnique({
      where: { empresa_id: empresaId },
    });
    if (!config) return null;
    const sanitizada = this._sanitize(config);
    // Con "Tokens de la plataforma" la key vive en el holding, no en la
    // empresa: `tiene_api_key` tiene que decir si hay credencial USABLE. Si no,
    // el dashboard pide configurar una API key que la empresa nunca va a tener.
    if (config.credencial_modo === 'PLATAFORMA') {
      const efectiva = await this.getConfigEfectiva(empresaId);
      return { ...sanitizada, tiene_api_key: !!efectiva.api_key, modelo_efectivo: efectiva.modelo };
    }
    return sanitizada;
  }

  /**
   * Configuración EFECTIVA para hacer una llamada IA, resolviendo el modo de credencial:
   * - PROPIA: usa proveedor/modelo/key de la empresa (paga la empresa).
   * - PLATAFORMA: usa proveedor/modelo/key del HOLDING (costo del holding, cuota del plan).
   * Nunca lanza: devuelve api_key null si no hay credencial resoluble (el caller lo maneja).
   */
  async getConfigEfectiva(empresaId: string): Promise<{
    proveedor: string;
    modelo: string;
    api_key: string | null;
    credencial_modo: string;
    credencial_propia: boolean;
  }> {
    const propia = await this.prisma.ai_empresa_config.findUnique({ where: { empresa_id: empresaId } });
    const modo = propia?.credencial_modo ?? 'PROPIA';

    if (modo === 'PLATAFORMA') {
      // Holding (raíz): subir por parent_id hasta la empresa sin padre.
      const holdingId = await resolverHoldingId(this.prisma, empresaId);
      const holdingCfg = await this.prisma.ai_empresa_config.findUnique({ where: { empresa_id: holdingId } });
      return {
        proveedor: holdingCfg?.proveedor ?? propia?.proveedor ?? 'anthropic',
        modelo: holdingCfg?.modelo ?? propia?.modelo ?? '',
        api_key: holdingCfg?.api_key_encrypted ? decrypt(holdingCfg.api_key_encrypted) : null,
        credencial_modo: 'PLATAFORMA',
        credencial_propia: false,
      };
    }

    return {
      proveedor: propia?.proveedor ?? 'anthropic',
      modelo: propia?.modelo ?? '',
      api_key: propia?.api_key_encrypted ? decrypt(propia.api_key_encrypted) : null,
      credencial_modo: 'PROPIA',
      credencial_propia: true,
    };
  }

  /**
   * Aplica la cuota de tokens IA que incluye el plan a la config de la empresa.
   * - Setea `limite_tokens_mensual` = cuota del plan y `origen_limite = 'PLAN'`.
   * - Si la empresa NO tenía config, la crea con modo PLATAFORMA (si la cuota > 0).
   * - Si ya existía, NO pisa `credencial_modo` (respeta que la empresa haya elegido usar su propia key).
   * Nunca lanza: un fallo acá no debe romper el alta/cambio de suscripción.
   */
  async aplicarCuotaDesdePlan(empresaId: string, tokensIaMensual: number): Promise<void> {
    try {
      const cuota = Math.max(0, Math.round(tokensIaMensual || 0));
      const existente = await this.prisma.ai_empresa_config.findUnique({ where: { empresa_id: empresaId } });
      if (existente) {
        await this.prisma.ai_empresa_config.update({
          where: { empresa_id: empresaId },
          data: { limite_tokens_mensual: cuota, origen_limite: 'PLAN', updated_at: new Date() },
        });
      } else {
        await this.prisma.ai_empresa_config.create({
          data: {
            empresa_id: empresaId,
            limite_tokens_mensual: cuota,
            origen_limite: 'PLAN',
            credencial_modo: cuota > 0 ? 'PLATAFORMA' : 'PROPIA',
          },
        });
      }
    } catch (err) {
      this.logger.error(
        `No se pudo aplicar cuota IA del plan a empresa ${empresaId}: ${err instanceof Error ? err.message : 'unknown'}`,
      );
    }
  }

  async getConfigConKey(empresaId: string) {
    const config = await this.prisma.ai_empresa_config.findUnique({
      where: { empresa_id: empresaId },
    });
    if (!config) throw new NotFoundException('Configuración IA no encontrada');
    const apiKey = config.api_key_encrypted ? decrypt(config.api_key_encrypted) : null;
    return { ...config, api_key: apiKey };
  }

  async listarModelos(empresaId: string, override?: { proveedor?: string; api_key?: string }) {
    let savedConfig: any = null;
    try {
      savedConfig = await this.getConfigConKey(empresaId);
    } catch (_) {}

    const proveedor = override?.proveedor || savedConfig?.proveedor || 'anthropic';
    const apiKey = override?.api_key || savedConfig?.api_key;

    if (!apiKey) {
      // Sin API key: devolver lista estática por proveedor
      return { modelos: this._modelosPorDefecto(proveedor), fuente: 'estatica' };
    }

    try {
      if (proveedor === 'anthropic') {
        const res = await fetch('https://api.anthropic.com/v1/models', {
          headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
        });
        if (!res.ok) return { modelos: this._modelosPorDefecto(proveedor), fuente: 'estatica' };
        const json = (await res.json());
        const modelos = (json.data || [])
          .filter((m: any) => m.id && m.id.startsWith('claude'))
          .sort((a: any, b: any) => (b.created_at || 0) - (a.created_at || 0))
          .map((m: any) => ({ id: m.id, nombre: m.display_name || m.id }));
        return {
          modelos: modelos.length ? modelos : this._modelosPorDefecto(proveedor),
          fuente: modelos.length ? 'api' : 'estatica',
        };
      }
      if (proveedor === 'openai') {
        const client = new OpenAI({ apiKey });
        const list = await client.models.list();
        const modelos = list.data
          .filter((m) => m.id.startsWith('gpt'))
          .sort((a, b) => b.created - a.created)
          .slice(0, 10)
          .map((m) => ({ id: m.id, nombre: m.id }));
        return {
          modelos: modelos.length ? modelos : this._modelosPorDefecto(proveedor),
          fuente: modelos.length ? 'api' : 'estatica',
        };
      }
      if (proveedor === 'deepseek') {
        // DeepSeek expone /models compatible con OpenAI.
        const client = new OpenAI({ apiKey, baseURL: 'https://api.deepseek.com' });
        const list = await client.models.list();
        const modelos = list.data.map((m) => ({ id: m.id, nombre: m.id }));
        return {
          modelos: modelos.length ? modelos : this._modelosPorDefecto(proveedor),
          fuente: modelos.length ? 'api' : 'estatica',
        };
      }
    } catch (err: any) {
      this.logger.warn(`listarModelos fallido [${proveedor}]: ${err.message}`);
    }

    return { modelos: this._modelosPorDefecto(proveedor), fuente: 'estatica' };
  }

  private _modelosPorDefecto(proveedor: string) {
    if (proveedor === 'anthropic')
      return [
        { id: 'claude-3-haiku-20240307', nombre: 'Claude 3 Haiku (rápido y económico)' },
        { id: 'claude-3-sonnet-20240229', nombre: 'Claude 3 Sonnet (equilibrado)' },
        { id: 'claude-3-opus-20240229', nombre: 'Claude 3 Opus (más capaz)' },
        { id: 'claude-3-5-haiku-20241022', nombre: 'Claude 3.5 Haiku' },
        { id: 'claude-3-5-sonnet-20241022', nombre: 'Claude 3.5 Sonnet' },
      ];
    if (proveedor === 'openai')
      return [
        { id: 'gpt-4o-mini', nombre: 'GPT-4o Mini (económico)' },
        { id: 'gpt-4o', nombre: 'GPT-4o (capaz)' },
        { id: 'gpt-4-turbo', nombre: 'GPT-4 Turbo' },
      ];
    if (proveedor === 'deepseek')
      return [
        { id: 'deepseek-chat', nombre: 'DeepSeek Chat (alias al último)' },
        { id: 'deepseek-reasoner', nombre: 'DeepSeek Reasoner (alias razonamiento)' },
        { id: 'deepseek-v4-flash', nombre: 'DeepSeek V4 Flash (económico)' },
        { id: 'deepseek-v4-pro', nombre: 'DeepSeek V4 Pro (más capaz)' },
      ];
    return [];
  }

  async testConexion(empresaId: string, override?: { proveedor?: string; modelo?: string; api_key?: string }) {
    // Intentar obtener config guardada, pero no fallar si no existe
    let savedConfig: any = null;
    try {
      savedConfig = await this.getConfigConKey(empresaId);
    } catch (_) {
      /* sin config guardada aún */
    }

    const proveedor = override?.proveedor || savedConfig?.proveedor || 'anthropic';
    const modelo = override?.modelo || savedConfig?.modelo || 'claude-3-5-haiku-20241022';
    const apiKey = override?.api_key || savedConfig?.api_key;

    if (!apiKey) {
      throw new BadRequestException('Ingresá una API key para probar la conexión');
    }

    try {
      if (proveedor === 'anthropic') {
        // Test directo via HTTP para ver el error real sin que el SDK lo envuelva
        const res = await fetch('https://api.anthropic.com/v1/messages', {
          method: 'POST',
          headers: {
            'x-api-key': apiKey,
            'anthropic-version': '2023-06-01',
            'content-type': 'application/json',
          },
          body: JSON.stringify({
            model: modelo,
            max_tokens: 10,
            messages: [{ role: 'user', content: 'ping' }],
          }),
        });
        const json = await res.json();
        if (!res.ok) {
          const errMsg = json?.error?.message || JSON.stringify(json);
          this.logger.warn(`Anthropic HTTP ${res.status}: ${errMsg}`);
          throw new Error(errMsg);
        }
      } else if (proveedor === 'openai' || proveedor === 'deepseek') {
        // DeepSeek es compatible con la API de OpenAI (base URL propia).
        const client = new OpenAI(
          proveedor === 'deepseek' ? { apiKey, baseURL: 'https://api.deepseek.com' } : { apiKey },
        );
        const messages = [{ role: 'user' as const, content: 'ping' }];
        // Modelos nuevos exigen `max_completion_tokens`; fallback a `max_tokens` para los viejos.
        try {
          await client.chat.completions.create({ model: modelo, max_completion_tokens: 10, messages });
        } catch (err) {
          if (esErrorParametroTokens(err)) {
            await client.chat.completions.create({ model: modelo, max_tokens: 10, messages });
          } else {
            throw err;
          }
        }
      } else {
        throw new BadRequestException(`Proveedor desconocido: ${proveedor}`);
      }
      return { ok: true, mensaje: `Conexión exitosa con ${proveedor} (${modelo})` };
    } catch (err: any) {
      // Extraer el mensaje real del error (SDKs anidan el mensaje de distintas formas)
      const rawMessage: string =
        err?.error?.error?.message || // Anthropic SDK v2
        err?.error?.message || // OpenAI SDK
        err?.message ||
        JSON.stringify(err);
      this.logger.warn(`Test IA fallido [${proveedor}/${modelo}]: ${rawMessage}`);
      const amigable = mensajeAmigableIA(rawMessage, proveedor);
      throw new BadRequestException({ mensaje: amigable, detalle: rawMessage });
    }
  }

  private _sanitize(config: any) {
    const { api_key_encrypted, ...rest } = config;
    return { ...rest, tiene_api_key: !!api_key_encrypted };
  }

  // ── Consumo IA (analytics desde ai_chat_audit) ────────────────────
  async getConsumo(empresaId: string) {
    const ahora = new Date();
    const inicioHoy = new Date(ahora); inicioHoy.setHours(0, 0, 0, 0);
    const inicioSemana = new Date(ahora); inicioSemana.setDate(ahora.getDate() - 7); inicioSemana.setHours(0, 0, 0, 0);
    const inicioMes = new Date(ahora); inicioMes.setDate(ahora.getDate() - 30); inicioMes.setHours(0, 0, 0, 0);

    const [agregHoy, agregSemana, agregMes, porResultado, porDia] = await Promise.all([
      this._agregar(empresaId, inicioHoy),
      this._agregar(empresaId, inicioSemana),
      this._agregar(empresaId, inicioMes),
      this.prisma.ai_chat_audit.groupBy({
        by: ['resultado'],
        where: { empresa_id: empresaId, created_at: { gte: inicioMes } },
        _count: { _all: true },
        _sum: { tokens_usados: true },
      }),
      // Timeline diario últimos 30 días
      this.prisma.$queryRawUnsafe<{ fecha: Date; tokens: bigint; consultas: bigint }[]>(
        `SELECT DATE(created_at) AS fecha,
                COALESCE(SUM(tokens_usados),0)::BIGINT AS tokens,
                COUNT(*)::BIGINT AS consultas
         FROM ai_chat_audit
         WHERE empresa_id = $1::uuid AND created_at >= $2
         GROUP BY DATE(created_at)
         ORDER BY DATE(created_at) ASC`,
        empresaId,
        inicioMes,
      ),
    ]);

    return {
      hoy: agregHoy,
      semana: agregSemana,
      mes: agregMes,
      por_resultado: porResultado.map((r) => ({
        resultado: r.resultado,
        consultas: r._count._all,
        tokens: Number(r._sum.tokens_usados ?? 0),
      })),
      por_dia: porDia.map((d) => ({
        fecha: d.fecha instanceof Date ? d.fecha.toISOString().slice(0, 10) : String(d.fecha).slice(0, 10),
        tokens: Number(d.tokens),
        consultas: Number(d.consultas),
      })),
    };
  }

  private async _agregar(empresaId: string, desde: Date) {
    const r = await this.prisma.ai_chat_audit.aggregate({
      where: { empresa_id: empresaId, created_at: { gte: desde } },
      _count: { _all: true },
      _sum: { tokens_usados: true, filas_devueltas: true, ms_ejecucion: true },
      _avg: { ms_ejecucion: true },
    });
    const ok = await this.prisma.ai_chat_audit.count({
      where: { empresa_id: empresaId, created_at: { gte: desde }, resultado: 'ok' },
    });
    const total = r._count._all ?? 0;
    return {
      consultas: total,
      consultas_ok: ok,
      tokens: Number(r._sum.tokens_usados ?? 0),
      filas: Number(r._sum.filas_devueltas ?? 0),
      ms_promedio: Math.round(Number(r._avg.ms_ejecucion ?? 0)),
      tasa_exito: total > 0 ? Math.round((ok / total) * 100) : null,
    };
  }
}
