/**
 * RAG Context Query Helper
 *
 * Shared helper for querying the RAG service to get relevant context.
 * Used by chat agent to automatically retrieve context before responding.
 *
 * IMPROVEMENTS (P1):
 * - Context-aware query expansion: includes recent conversation context
 *   to resolve pronouns and maintain topic continuity
 */

import { fetchJson } from '../../../lib/utils/type-cast-helpers';
import { createDebugLog } from '../../lib/debug_log';
import {
  UpstreamHttpError,
  isUpstreamHttpError,
} from '../../lib/errors/upstream_http_error';
import { getRagConfig, ragFetch } from '../../lib/helpers/rag_config';
import {
  buildRagSearchFilters,
  type RagMetadataFilters,
} from '../../lib/helpers/rag_metadata_filters';
import {
  extractCitationsFromSearchResults,
  formatSearchResults,
  type ContextCitation,
  type SearchResponse,
} from './format_search_results';

const debugLog = createDebugLog('DEBUG_RAG_QUERY', '[RAGQuery]');
const DEFAULT_TOP_K = 10;
const DEFAULT_SIMILARITY_THRESHOLD = 0.51;
const RAG_REQUEST_TIMEOUT_MS = 10000; // 10 seconds

// Query expansion constants
const MAX_CONTEXT_MESSAGES = 3; // Number of recent messages to include for context
const MAX_CONTEXT_CHARS = 500; // Max chars per context message

/**
 * Recent conversation message for context expansion.
 */
export interface RecentMessage {
  role: 'user' | 'assistant';
  content: string;
}

/**
 * Check if the query likely contains unresolved references.
 * This helps determine if we need context expansion.
 */
function hasUnresolvedReferences(query: string): boolean {
  const lowered = query.toLowerCase();

  // Pronouns and demonstratives that suggest references to prior context
  const referencePatterns = [
    /\b(it|this|that|these|those|they|them|its|their)\b/i,
    /\b(the same|the one|the other|above|previous|mentioned|said)\b/i,
    /\b(他|她|它|这个|那个|这些|那些|上面|之前|刚才)\b/, // Chinese references
    /\b(그것|이것|저것|그|이|저)\b/, // Korean references
    /\b(それ|これ|あれ|その|この|あの)\b/, // Japanese references
  ];

  return referencePatterns.some((pattern) => pattern.test(lowered));
}

/**
 * Build an expanded query that includes conversation context.
 * This helps resolve pronouns and maintain topic continuity.
 */
function buildExpandedQuery(
  currentQuery: string,
  recentMessages?: RecentMessage[],
): string {
  // If no context or query is already long, use original
  if (!recentMessages || recentMessages.length === 0) {
    return currentQuery;
  }

  // If query doesn't seem to have unresolved references, skip expansion
  // (unless the query is very short, which might be ambiguous)
  if (!hasUnresolvedReferences(currentQuery) && currentQuery.length > 20) {
    return currentQuery;
  }

  // Build context from recent messages (most recent first, then reverse for chronological)
  const contextParts: string[] = [];
  let totalChars = 0;

  // Take up to MAX_CONTEXT_MESSAGES, preferring user messages for topic context
  const relevantMessages = recentMessages
    .slice(-MAX_CONTEXT_MESSAGES * 2) // Look at more messages
    .filter((m) => m.content && m.content.trim().length > 0);

  for (const msg of relevantMessages) {
    if (totalChars >= MAX_CONTEXT_CHARS * MAX_CONTEXT_MESSAGES) break;

    const truncated =
      msg.content.length > MAX_CONTEXT_CHARS
        ? msg.content.slice(0, MAX_CONTEXT_CHARS) + '...'
        : msg.content;

    contextParts.push(`${msg.role}: ${truncated}`);
    totalChars += truncated.length;
  }

  if (contextParts.length === 0) {
    return currentQuery;
  }

  // Format: provide context then the current query
  // This helps embedding models understand the full context
  const expandedQuery = `Previous conversation:
${contextParts.join('\n')}

Current question: ${currentQuery}`;

  debugLog('Query expanded with conversation context', {
    originalLength: currentQuery.length,
    expandedLength: expandedQuery.length,
    contextMessagesUsed: contextParts.length,
  });

  return expandedQuery;
}

/**
 * Result from a RAG context query, containing both the formatted
 * text for injection and structured citation metadata.
 */
export interface RagContextResult {
  text: string;
  citations: ContextCitation[];
}

/**
 * Options for RAG context queries.
 */
export interface RagContextOptions {
  /** File storage IDs to scope the search to */
  fileIds?: string[];
  /**
   * Optional pre-retrieval narrowing filters forwarded as the `/search`
   * `filters` object: hierarchical folder prefix + flat metadata
   * equality/IN filters. Narrowing-only — `fileIds` stays the
   * authorization boundary.
   */
  filters?: {
    folderPath?: string;
    metadata?: RagMetadataFilters;
  };
  /**
   * Org slug for the X-Tale-Org header. Required by the RAG service's
   * `/api/v1/search` endpoint (it picks the org's provider catalog to
   * embed the query). Empty / missing yields HTTP 400 from RAG and is
   * surfaced (not silently swallowed) so the caller sees the bug.
   */
  orgSlug: string;
}

/**
 * Query the RAG service for relevant context based on the user's message.
 * Returns formatted context string that can be injected into the agent's context.
 *
 * @param userMessage - The user's message to search for relevant context
 * @param topK - Number of results to return (default: 5)
 * @param similarityThreshold - Minimum similarity score (default: 0.51)
 * @param signal - Optional AbortSignal for timeout control
 * @param recentMessages - Optional recent conversation messages for context expansion
 * @param options - Optional multi-tenant options (userId, datasets)
 * @returns Formatted context string or undefined if no relevant results
 */
export async function queryRagContext(
  userMessage: string,
  topK: number = DEFAULT_TOP_K,
  similarityThreshold: number = DEFAULT_SIMILARITY_THRESHOLD,
  signal?: AbortSignal,
  recentMessages?: RecentMessage[],
  // The type says orgSlug is required, but TS forces this parameter to
  // be optional because all preceding params have defaults. The runtime
  // assertion below (outside the outer try/catch) enforces it loudly.
  options: RagContextOptions = { orgSlug: '' },
): Promise<RagContextResult | undefined> {
  // Validate orgSlug up front, OUTSIDE the outer try/catch so the bug
  // surfaces as a real throw instead of being silently swallowed by the
  // graceful-degrade catch at the bottom of this function. A missing /
  // blank slug is a caller misconfiguration, not a runtime RAG outage.
  // (Round-3 P2 R7-P2-a — previously the empty-slug case hit ragFetch's
  // throw, fell into the catch, and returned undefined as if the search
  // had failed.)
  if (
    !options ||
    typeof options.orgSlug !== 'string' ||
    !options.orgSlug.trim()
  ) {
    throw new Error(
      'queryRagContext: options.orgSlug is required and must be non-empty',
    );
  }
  try {
    const ragServiceUrl = getRagConfig().serviceUrl;

    // Build expanded query with conversation context
    const expandedQuery = buildExpandedQuery(userMessage, recentMessages);

    debugLog('Querying RAG service for context', {
      userMessage: userMessage.slice(0, 100),
      expandedQueryLength: expandedQuery.length,
      hasContextExpansion: expandedQuery !== userMessage,
      topK,
      ragServiceUrl,
    });

    // Create abort controller for timeout if no signal provided
    const controller = new AbortController();
    const timeoutId = setTimeout(
      () => controller.abort(),
      RAG_REQUEST_TIMEOUT_MS,
    );
    const fetchSignal = signal || controller.signal;

    try {
      const requestPayload: Record<string, unknown> = {
        query: expandedQuery,
        top_k: topK,
        similarity_threshold: similarityThreshold,
        include_metadata: true,
      };

      if (!options.fileIds || options.fileIds.length === 0) {
        debugLog('No file IDs provided, skipping RAG query');
        // Without this, the controller fires `controller.abort()`
        // RAG_REQUEST_TIMEOUT_MS later against no in-flight fetch — a
        // resource leak and an unhandled-rejection source under stricter
        // test runners.
        clearTimeout(timeoutId);
        return undefined;
      }
      requestPayload.file_ids = options.fileIds;

      if (options.filters) {
        const searchFilters = buildRagSearchFilters({
          folderPath: options.filters.folderPath,
          metadata: options.filters.metadata,
        });
        if (searchFilters) {
          requestPayload.filters = searchFilters;
        }
      }

      const response = await ragFetch('/api/v1/search', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(requestPayload),
        orgSlug: options.orgSlug,
        signal: fetchSignal,
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        const errorText = await response.text().catch(() => '');
        // 4xx is a caller/config bug: missing or bad X-Tale-Org,
        // schema-rejected query, etc. Surfacing as a thrown
        // UpstreamHttpError gives the agent runtime a clear signal
        // (and prevents the agent from treating "auth misconfigured"
        // as "knowledge base is empty"). 5xx remains silent fallback
        // — RAG outage shouldn't break chat completely.
        if (response.status >= 400 && response.status < 500) {
          throw UpstreamHttpError.fromResponse(
            'rag',
            response,
            errorText,
            '/api/v1/search',
          );
        }
        console.error('[rag_query] RAG service unavailable', {
          status: response.status,
          // errorText logged engineer-side only; caller gets a graceful
          // empty-context return so chat continues without RAG.
          error: errorText,
        });
        return undefined;
      }

      const result = await fetchJson<SearchResponse>(response);

      if (!result.success || result.total_results === 0) {
        debugLog('No relevant RAG context found', {
          success: result.success,
          total_results: result.total_results,
        });
        return undefined;
      }

      const ragContext = formatSearchResults(result.results);
      if (!ragContext) return undefined;

      const citations = extractCitationsFromSearchResults(result.results);

      debugLog('RAG context retrieved', {
        resultCount: result.total_results,
        contextLength: ragContext.length,
        citationCount: citations.length,
        processingTimeMs: result.processing_time_ms,
      });

      return { text: ragContext, citations };
    } catch (fetchError) {
      clearTimeout(timeoutId);

      // Caller/config bugs (4xx → `UpstreamHttpError`) MUST propagate
      // past this graceful-degrade layer. Otherwise the explicit throw
      // for missing `X-Tale-Org` / bad query in the `!response.ok`
      // branch above is silently swallowed here and the agent treats
      // "auth misconfigured" as "knowledge base is empty" — the very
      // failure mode the upstream throw was added to prevent.
      if (isUpstreamHttpError(fetchError)) {
        throw fetchError;
      }

      // Handle timeout specifically
      if (fetchError instanceof Error && fetchError.name === 'AbortError') {
        console.error('[rag_query] RAG service request timeout', {
          timeoutMs: RAG_REQUEST_TIMEOUT_MS,
        });
      } else {
        console.error('[rag_query] RAG service fetch error', {
          error:
            fetchError instanceof Error
              ? fetchError.message
              : String(fetchError),
        });
      }
      return undefined; // Gracefully degrade on fetch error
    }
  } catch (error) {
    console.error('[rag_query] Failed to query RAG service', {
      error: error instanceof Error ? error.message : String(error),
    });
    return undefined; // Gracefully degrade on error
  }
}
