import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Parser } from 'node-sql-parser';
import { PrismaService } from 'src/prisma/prisma.service';
import { AiConfigService } from '../ai-config/ai-config.service';
import { AiProviderService } from '../ai-provider/ai-provider.service';
import { RrhhContextService } from '../shared/rrhh-context.service';
import { ModulesContextService } from '../shared/modules-context.service';
import { AiAuditService } from './ai-audit.service';
import { AiReadonlyDbService } from './ai-readonly.service';
import { buildSchemaContextFor } from './schema-context';
import { extraerMensajeError, mensajeAmigableIA } from '../shared/ai-error.util';

interface ChatRespuestaIA {
  sql: string;
  titulo: string;
  tipo_grafico: string;
  explicacion: string;
}

export interface EnviarMensajeContext {
  ip?: string;
  userAgent?: string;
}

// Blacklist reducida: defensa adicional por si el parser falla abriendo una brecha.
// El enforcement real lo hace (1) el parser AST y (2) el usuario PG read-only.
const SQL_PELIGROSAS = [
  'DROP', 'DELETE', 'UPDATE', 'INSERT', 'ALTER', 'TRUNCATE',
  'EXEC', 'EXECUTE', 'CREATE', 'GRANT', 'REVOKE', 'COPY',
  'pg_sleep', 'pg_read_file', 'pg_ls_dir', 'lo_import', 'lo_export',
];
const QUERY_TIMEOUT_MS = 10_000;
const MAX_FILAS = 500;
const sqlParser = new Parser();

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly readonlyDb: AiReadonlyDbService,
    private readonly aiProvider: AiProviderService,
    private readonly aiAudit: AiAuditService,
    private readonly aiConfig: AiConfigService,
    private readonly rrhhContext: RrhhContextService,
    private readonly modulesContext: ModulesContextService,
  ) {}

  // ── Sesiones ──────────────────────────────────────────────────

  async crearSesion(empresaId: string, usuarioId: string) {
    return this.prisma.ai_chat_sesiones.create({
      data: { empresa_id: empresaId, usuario_id: usuarioId },
    });
  }

  async listarSesiones(empresaId: string, usuarioId: string) {
    return this.prisma.ai_chat_sesiones.findMany({
      where: { empresa_id: empresaId, usuario_id: usuarioId },
      orderBy: { updated_at: 'desc' },
      take: 50,
      select: {
        id: true,
        titulo: true,
        created_at: true,
        updated_at: true,
        _count: { select: { mensajes: true } },
      },
    });
  }

  async getSesionConMensajes(sesionId: string, empresaId: string, usuarioId: string) {
    const sesion = await this.prisma.ai_chat_sesiones.findFirst({
      where: { id: sesionId, empresa_id: empresaId, usuario_id: usuarioId },
      include: {
        mensajes: {
          orderBy: { created_at: 'asc' },
          select: {
            id: true,
            sesion_id: true,
            rol: true,
            contenido: true,
            // sql_generado: OMITIDO — nunca se expone al frontend
            datos_json: true,
            tipo_grafico: true,
            tokens_usados: true,
            created_at: true,
          },
        },
      },
    });
    if (!sesion) throw new NotFoundException('Sesión no encontrada');
    return sesion;
  }

  // ── Mensajes / consultas ──────────────────────────────────────

  async enviarMensaje(
    sesionId: string,
    empresaId: string,
    usuarioId: string,
    pregunta: string,
    ctx: EnviarMensajeContext = {},
  ) {
    const auditBase = {
      empresa_id: empresaId,
      usuario_id: usuarioId,
      sesion_id: sesionId,
      pregunta,
      ip_address: ctx.ip ?? null,
      user_agent: ctx.userAgent ?? null,
    };

    // Verificar que la sesión pertenece a este usuario/empresa
    const sesion = await this.prisma.ai_chat_sesiones.findFirst({
      where: { id: sesionId, empresa_id: empresaId, usuario_id: usuarioId },
    });
    if (!sesion) throw new NotFoundException('Sesión no encontrada');

    // Guardar mensaje del usuario
    await this.prisma.ai_chat_mensajes.create({
      data: { sesion_id: sesionId, rol: 'user', contenido: pregunta },
    });

    // Cargar contexto acumulado del negocio en paralelo
    const [historial, ejemplosExitosos, favoritos] = await Promise.all([
      this.prisma.ai_chat_mensajes.findMany({
        where: { sesion_id: sesionId },
        orderBy: { created_at: 'desc' },
        take: 6,
      }),
      this.prisma.ai_chat_mensajes.findMany({
        where: {
          sesion: { empresa_id: empresaId },
          rol: 'assistant',
          sql_generado: { not: null },
          datos_json: { not: null },
        },
        orderBy: { created_at: 'desc' },
        take: 10,
        include: {
          sesion: {
            include: {
              mensajes: {
                where: { rol: 'user' },
                orderBy: { created_at: 'asc' },
                take: 1,
                select: { contenido: true },
              },
            },
          },
        },
      }),
      this.prisma.ai_chat_favoritos.findMany({
        where: { empresa_id: empresaId },
        orderBy: { created_at: 'desc' },
        take: 5,
        select: { pregunta: true, sql_guardado: true },
      }),
    ]);

    // Construir bloque de memoria acumulada (re-validando cada SQL de referencia
    // para evitar prompt injection vía registros viejos o maliciosos)
    const memoriaNegocio = this._construirMemoriaNegocio(favoritos, ejemplosExitosos);

    const historialTexto = historial
      .reverse()
      .map((m) => `${m.rol === 'user' ? 'Usuario' : 'Asistente'}: ${m.contenido}`)
      .join('\n');

    const partes: string[] = [];
    if (memoriaNegocio) partes.push(memoriaNegocio);
    if (historialTexto) partes.push(`Historial reciente de esta conversación:\n${historialTexto}`);
    partes.push(`Nueva pregunta: ${pregunta}`);
    const promptCompleto = partes.join('\n\n');

    // Llamar al LLM
    let iaResponse: ChatRespuestaIA;
    let tokensUsados = 0;
    let modeloLlm = '';
    let proveedorLlm = '';

    try {
      // Capturar el modelo desde la config antes de llamar al proveedor (más confiable)
      try {
        const aiCfg = await this.aiConfig.getConfigConKey(empresaId);
        modeloLlm = aiCfg.modelo ?? '';
        proveedorLlm = aiCfg.proveedor ?? '';
      } catch (_) {}

      // Resolver permisos RRHH + dominios AI activos una sola vez por consulta (cache interno 60s).
      const [verRRHH, verSalarios, activeDomains] = await Promise.all([
        this.rrhhContext.puedeVerRRHH(usuarioId, empresaId),
        this.rrhhContext.puedeVerSalarios(usuarioId, empresaId),
        this.modulesContext.getActiveDomains(empresaId),
      ]);
      const schemaContextDinamico = buildSchemaContextFor({
        includeRRHH: verRRHH,
        includeRRHHSalaries: verSalarios,
        activeDomains,
      });
      const result = await this.aiProvider.completar(empresaId, promptCompleto, schemaContextDinamico, {
        feature: 'CHAT',
        usuarioId,
        referenciaId: sesionId,
      });
      tokensUsados = result.tokensUsados;
      if (result.modelo) modeloLlm = result.modelo;
      iaResponse = this._parsearRespuestaIA(result.content);
    } catch (e) {
      const rawError = extraerMensajeError(e);
      const mensajeError = `Lo siento, no pude procesar esa consulta. ${mensajeAmigableIA(rawError, proveedorLlm)}`;
      await this.prisma.ai_chat_mensajes.create({
        data: { sesion_id: sesionId, rol: 'assistant', contenido: mensajeError },
      });
      await this.aiAudit.log({
        ...auditBase,
        modelo_llm: modeloLlm || null,
        resultado: 'error_llm',
        error_mensaje: rawError,
        tokens_usados: tokensUsados || null,
      });
      return { error: mensajeError };
    }

    // Si el LLM indicó sql vacío (funcionalidad no disponible)
    if (!iaResponse.sql || iaResponse.sql.trim() === '') {
      const mensajeAsistente = await this.prisma.ai_chat_mensajes.create({
        data: {
          sesion_id: sesionId,
          rol: 'assistant',
          contenido: iaResponse.explicacion,
          tipo_grafico: 'none',
          tokens_usados: tokensUsados,
        },
      });
      await this.prisma.ai_chat_sesiones.update({
        where: { id: sesionId },
        data: { titulo: sesion.titulo ?? pregunta.substring(0, 100), updated_at: new Date() },
      });
      await this.aiAudit.log({
        ...auditBase,
        modelo_llm: modeloLlm || null,
        mensaje_id: mensajeAsistente.id,
        resultado: 'ok',
        tokens_usados: tokensUsados,
        filas_devueltas: 0,
      });
      return {
        mensaje: this._sanitizeMensaje(mensajeAsistente),
        datos: [],
        titulo: iaResponse.titulo,
        tipo_grafico: 'none',
        explicacion: iaResponse.explicacion,
        tokens_usados: tokensUsados,
      };
    }

    // Validar SQL (parser AST + blacklist defensiva)
    try {
      this._validarSQL(iaResponse.sql);
    } catch (e) {
      const motivo = e instanceof Error ? e.message : String(e);
      this.logger.warn(`SQL bloqueado por validación: ${motivo}\nSQL: ${iaResponse.sql}`);
      const mensajeError = `La consulta generada no es segura: ${motivo}`;
      await this.prisma.ai_chat_mensajes.create({
        data: {
          sesion_id: sesionId,
          rol: 'assistant',
          contenido: mensajeError,
          sql_generado: iaResponse.sql,
        },
      });
      await this.aiAudit.log({
        ...auditBase,
        modelo_llm: modeloLlm || null,
        sql_generado: iaResponse.sql,
        resultado: motivo.startsWith('[parser]') ? 'blocked_parser' : 'blocked_validation',
        motivo_bloqueo: motivo,
        tokens_usados: tokensUsados,
      });
      return { error: mensajeError };
    }

    // Ejecutar SQL con timeout + read-only DB
    let datos: any[] = [];
    let errorEjecucion: string | null = null;
    const t0 = Date.now();

    try {
      datos = await this._ejecutarConTimeout(iaResponse.sql, empresaId);
    } catch (e) {
      errorEjecucion = e.message;
      this.logger.error(`Error ejecutando SQL: ${e.message}\nSQL: ${iaResponse.sql}`);
    }
    const msEjecucion = Date.now() - t0;

    const contenidoRespuesta = errorEjecucion
      ? `${iaResponse.explicacion}\n\n⚠️ Error al ejecutar: ${errorEjecucion}`
      : iaResponse.explicacion;

    const mensajeAsistente = await this.prisma.ai_chat_mensajes.create({
      data: {
        sesion_id: sesionId,
        rol: 'assistant',
        contenido: contenidoRespuesta,
        sql_generado: iaResponse.sql,
        datos_json: datos.length > 0 ? datos : null,
        tipo_grafico: iaResponse.tipo_grafico,
        tokens_usados: tokensUsados,
      },
    });

    if (!sesion.titulo) {
      await this.prisma.ai_chat_sesiones.update({
        where: { id: sesionId },
        data: { titulo: pregunta.substring(0, 100), updated_at: new Date() },
      });
    } else {
      await this.prisma.ai_chat_sesiones.update({
        where: { id: sesionId },
        data: { updated_at: new Date() },
      });
    }

    await this.aiAudit.log({
      ...auditBase,
      mensaje_id: mensajeAsistente.id,
      sql_generado: iaResponse.sql,
      resultado: errorEjecucion ? 'error_execution' : 'ok',
      error_mensaje: errorEjecucion,
      ms_ejecucion: msEjecucion,
      filas_devueltas: datos.length,
      tokens_usados: tokensUsados,
      modelo_llm: modeloLlm || null,
    });

    return {
      mensaje: this._sanitizeMensaje(mensajeAsistente),
      datos,
      titulo: iaResponse.titulo,
      tipo_grafico: iaResponse.tipo_grafico,
      explicacion: iaResponse.explicacion,
      tokens_usados: tokensUsados,
    };
  }

  // ── Favoritos ─────────────────────────────────────────────────

  async guardarFavorito(empresaId: string, usuarioId: string, mensajeId: string, titulo?: string) {
    const mensaje = await this.prisma.ai_chat_mensajes.findFirst({
      where: {
        id: mensajeId,
        rol: 'assistant',
        sesion: { empresa_id: empresaId, usuario_id: usuarioId },
      },
      select: {
        id: true,
        contenido: true,
        sql_generado: true,
        tipo_grafico: true,
        sesion: { select: { titulo: true } },
      },
    });
    if (!mensaje) throw new NotFoundException('Mensaje no encontrado o no autorizado');
    if (!mensaje.sql_generado) throw new BadRequestException('Este mensaje no tiene una consulta guardable');

    // Re-validar: el SQL puede ser viejo y el guardado debe ser seguro
    try {
      this._validarSQL(mensaje.sql_generado);
    } catch (e) {
      throw new BadRequestException(
        `No se puede guardar como favorito: ${e instanceof Error ? e.message : 'SQL inválido'}`,
      );
    }

    const tituloFinal = titulo || mensaje.sesion?.titulo || 'Consulta favorita';

    return this.prisma.ai_chat_favoritos.create({
      data: {
        empresa_id: empresaId,
        usuario_id: usuarioId,
        titulo: tituloFinal.substring(0, 200),
        pregunta: tituloFinal,
        sql_guardado: mensaje.sql_generado,
        tipo_grafico: mensaje.tipo_grafico ?? null,
      },
    });
  }

  async listarFavoritos(empresaId: string, usuarioId: string) {
    return this.prisma.ai_chat_favoritos.findMany({
      where: { empresa_id: empresaId, usuario_id: usuarioId },
      orderBy: { created_at: 'desc' },
      select: {
        id: true,
        titulo: true,
        pregunta: true,
        tipo_grafico: true,
        created_at: true,
        // sql_guardado: OMITIDO
      },
    });
  }

  async eliminarFavorito(id: string, empresaId: string, usuarioId: string) {
    const fav = await this.prisma.ai_chat_favoritos.findFirst({
      where: { id, empresa_id: empresaId, usuario_id: usuarioId },
    });
    if (!fav) throw new NotFoundException('Favorito no encontrado');
    return this.prisma.ai_chat_favoritos.delete({ where: { id } });
  }

  async ejecutarFavorito(id: string, empresaId: string, usuarioId: string) {
    const fav = await this.prisma.ai_chat_favoritos.findFirst({
      where: { id, empresa_id: empresaId, usuario_id: usuarioId },
    });
    if (!fav) throw new NotFoundException('Favorito no encontrado');

    this._validarSQL(fav.sql_guardado);
    const datos = await this._ejecutarConTimeout(fav.sql_guardado, empresaId);

    return {
      titulo: fav.titulo,
      tipo_grafico: fav.tipo_grafico,
      datos,
    };
  }

  // ── Helpers privados ──────────────────────────────────────────

  private _parsearRespuestaIA(content: string): ChatRespuestaIA {
    const jsonStr = extractBalancedJson(content);
    if (!jsonStr) {
      throw new BadRequestException('La IA no devolvió un JSON válido');
    }
    try {
      const parsed = JSON.parse(jsonStr);
      if (!parsed.titulo) {
        throw new Error('JSON incompleto: falta titulo');
      }
      return {
        sql: parsed.sql ?? '',
        titulo: parsed.titulo,
        tipo_grafico: parsed.tipo_grafico ?? 'table',
        explicacion: parsed.explicacion ?? '',
      };
    } catch {
      throw new BadRequestException(
        `No se pudo parsear la respuesta de la IA: ${content.substring(0, 200)}`,
      );
    }
  }

  /**
   * Validación de SQL en 3 capas:
   *  1. Blacklist rápida (detecta palabras reservadas peligrosas antes de parsear)
   *  2. Parser AST (node-sql-parser) — garantiza UN SOLO statement SELECT
   *  3. Requiere filtro empresa_id
   *
   * La defensa final es el usuario PG read-only (ver AiReadonlyDbService).
   */
  private _validarSQL(sql: string): void {
    if (!sql || typeof sql !== 'string') {
      throw new BadRequestException('SQL vacío');
    }
    const trimmed = sql.trim();
    if (trimmed.length === 0) {
      throw new BadRequestException('SQL vacío');
    }
    if (trimmed.length > 10_000) {
      throw new BadRequestException('SQL excede longitud máxima permitida');
    }

    // Capa 1: blacklist defensiva (case-insensitive con boundaries)
    const upper = trimmed.toUpperCase();
    for (const palabra of SQL_PELIGROSAS) {
      const regex = new RegExp(`\\b${palabra.toUpperCase()}\\b`);
      if (regex.test(upper)) {
        throw new BadRequestException(`Operación no permitida en la consulta: ${palabra}`);
      }
    }

    // Capa 2: AST — sustituir $1 por un placeholder literal parseable
    const sqlParseable = trimmed.replace(/\$1(?:::uuid)?/gi, "'00000000-0000-0000-0000-000000000000'");
    let ast: any;
    try {
      ast = sqlParser.astify(sqlParseable, { database: 'postgresql' });
    } catch (e) {
      throw new BadRequestException(
        `[parser] SQL no pudo ser parseado: ${e instanceof Error ? e.message : 'error desconocido'}`,
      );
    }

    const statements = Array.isArray(ast) ? ast : [ast];
    if (statements.length !== 1) {
      throw new BadRequestException('[parser] Solo se permite un único statement por consulta');
    }
    const tipo = String(statements[0]?.type ?? '').toLowerCase();
    if (tipo !== 'select') {
      throw new BadRequestException(`[parser] Solo se permiten consultas SELECT (detectado: ${tipo})`);
    }

    // Capa 3: debe filtrar por empresa_id
    if (!upper.includes('EMPRESA_ID')) {
      throw new BadRequestException('La consulta debe filtrar por empresa_id');
    }
  }

  /** Elimina campos sensibles antes de enviar un mensaje al cliente */
  private _sanitizeMensaje(msg: any) {
    const { sql_generado, ...safe } = msg;
    return safe;
  }

  private _construirMemoriaNegocio(
    favoritos: { pregunta: string; sql_guardado: string }[],
    exitosos: { sql_generado: string | null; sesion: { mensajes: { contenido: string }[] } }[],
  ): string {
    const partes: string[] = [];

    // Favoritos validados — re-validar por si se guardó algo inseguro antes
    const favValidos = favoritos.filter((f) => f.sql_guardado && this._esSqlValidoParaMemoria(f.sql_guardado));
    if (favValidos.length > 0) {
      const lines = favValidos
        .map((f) => `-- Pregunta: ${f.pregunta}\n${f.sql_guardado}`)
        .join('\n\n');
      partes.push(
        `CONSULTAS FAVORITAS DEL NEGOCIO (validadas por el usuario, usá como referencia de columnas y patrones correctos):\n${lines}`,
      );
    }

    // Consultas exitosas recientes — re-validar también
    const exitososFiltrados = exitosos
      .filter(
        (e) =>
          e.sql_generado &&
          e.sesion?.mensajes?.[0]?.contenido &&
          this._esSqlValidoParaMemoria(e.sql_generado),
      )
      .slice(0, 6);

    if (exitososFiltrados.length > 0) {
      const lines = exitososFiltrados
        .map((e) => `-- Pregunta: ${e.sesion.mensajes[0].contenido}\n${e.sql_generado}`)
        .join('\n\n');
      partes.push(
        `CONSULTAS SQL RECIENTES EXITOSAS (referencia de columnas y joins que funcionan en este negocio):\n${lines}`,
      );
    }

    if (partes.length === 0) return '';

    return (
      `=== MEMORIA DEL NEGOCIO ===\n` +
      `Estas consultas ya funcionaron para este negocio. Usalas como referencia de nombres de columnas, joins y patrones. ` +
      `NO las copies literalmente — adaptá según la pregunta actual.\n\n` +
      partes.join('\n\n') +
      `\n=== FIN MEMORIA ===`
    );
  }

  private _esSqlValidoParaMemoria(sql: string | null): boolean {
    if (!sql) return false;
    try {
      this._validarSQL(sql);
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Ejecuta SQL con:
   *  - Usuario PG read-only (defensa de motor)
   *  - SET LOCAL statement_timeout — cancela la query real en Postgres
   *  - Wrapper LIMIT para forzar límite en DB (no sólo en memoria)
   *  - Promise.race como defensa adicional (si la cancelación falla)
   */
  private async _ejecutarConTimeout(sql: string, empresaId: string): Promise<any[]> {
    // Cast UUID al parámetro $1 si el LLM lo omitió
    const withCast = sql.replace(/\$1(?!::uuid)/g, '$1::uuid');
    // Quitar punto y coma final (interfiere con el wrapper)
    const sanitized = withCast.trim().replace(/;+\s*$/g, '');
    // Wrapper LIMIT — fuerza límite a nivel Postgres (no sólo slice en memoria)
    const wrapped = `SELECT * FROM (${sanitized}) AS __ai_wrap LIMIT ${MAX_FILAS}`;

    // El rol PG read-only ya tiene statement_timeout = 10s configurado.
    // Promise.race es la defensa adicional de capa de aplicación.
    // runTenantScoped setea el GUC app.empresa_id → las policies RLS filtran por
    // empresa aunque el SQL del LLM no incluya el WHERE (aislamiento a nivel DB).
    const execute = this.readonlyDb.runTenantScoped<any[]>(empresaId, wrapped, empresaId);

    const timeout = new Promise<never>((_, reject) =>
      setTimeout(
        () => reject(new Error('La consulta superó el límite de 10 segundos')),
        QUERY_TIMEOUT_MS,
      ),
    );

    const resultado = await Promise.race([execute, timeout]);
    return Array.isArray(resultado) ? resultado : [];
  }
}

/** Extrae el primer objeto JSON balanceado del texto (soporta anidación y strings). */
function extractBalancedJson(content: string): string {
  const start = content.indexOf('{');
  if (start === -1) return '';
  let depth = 0;
  let inString = false;
  let escape = false;
  for (let i = start; i < content.length; i++) {
    const ch = content[i];
    if (escape) {
      escape = false;
      continue;
    }
    if (ch === '\\') {
      escape = true;
      continue;
    }
    if (ch === '"') {
      inString = !inString;
      continue;
    }
    if (inString) continue;
    if (ch === '{') depth++;
    else if (ch === '}') {
      depth--;
      if (depth === 0) return content.substring(start, i + 1);
    }
  }
  return '';
}
