/**
 * gno query command implementation.
 * Hybrid search with expansion, fusion, and reranking.
 *
 * @module src/cli/commands/query
 */

import type {
  EmbeddingPort,
  GenerationPort,
  RerankPort,
} from "../../llm/types";
import type { HybridSearchOptions, SearchResults } from "../../pipeline/types";

import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
import { resolveDownloadPolicy } from "../../llm/policy";
import { resolveModelUri } from "../../llm/registry";
import { type HybridSearchDeps, searchHybrid } from "../../pipeline/hybrid";
import {
  createVectorIndexPort,
  type VectorIndexPort,
} from "../../store/vector";
import { getGlobals } from "../program";
import {
  createProgressRenderer,
  createThrottledProgressRenderer,
} from "../progress";
import { decorateSearchResultsForIndex, initStore } from "./shared";

// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────

export type QueryCommandOptions = HybridSearchOptions & {
  /** Override config path */
  configPath?: string;
  /** Index name */
  indexName?: string;
  /** Override embedding model */
  embedModel?: string;
  /** Override expansion model */
  expandModel?: string;
  /** Deprecated alias for expansion model */
  genModel?: string;
  /** Override rerank model */
  rerankModel?: string;
  /** Output as JSON */
  json?: boolean;
  /** Output as Markdown */
  md?: boolean;
  /** Output as CSV */
  csv?: boolean;
  /** Output as XML */
  xml?: boolean;
  /** Output files only */
  files?: boolean;
};

export interface QueryFormatOptions {
  format: "terminal" | "json" | "files" | "csv" | "md" | "xml";
  full?: boolean;
  lineNumbers?: boolean;
  terminalLinks?: import("../format/search-results").FormatOptions["terminalLinks"];
}

export type QueryResult =
  | { success: true; data: SearchResults }
  | { success: false; error: string };

// ─────────────────────────────────────────────────────────────────────────────
// Command Implementation
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Execute gno query command.
 */
// oxlint-disable-next-line max-lines-per-function -- CLI orchestration with multiple output formats
export async function query(
  queryText: string,
  options: QueryCommandOptions = {}
): Promise<QueryResult> {
  const isStructured =
    options.json || options.files || options.csv || options.xml;
  const limit = options.limit ?? (isStructured ? 20 : 5);

  const initResult = await initStore({
    configPath: options.configPath,
    indexName: options.indexName,
    collection: options.collection,
    syncConfig: false,
  });

  if (!initResult.ok) {
    return { success: false, error: initResult.error };
  }

  const { store, config } = initResult;

  let embedPort: EmbeddingPort | null = null;
  let expandPort: GenerationPort | null = null;
  let rerankPort: RerankPort | null = null;

  try {
    const llm = new LlmAdapter(config);

    // Resolve download policy from env/flags
    const globals = getGlobals();
    const policy = resolveDownloadPolicy(process.env, {
      offline: globals.offline,
    });

    // Create progress renderer for model downloads (throttled)
    const showProgress = !options.json && process.stderr.isTTY;
    const downloadProgress = showProgress
      ? createThrottledProgressRenderer(createProgressRenderer())
      : undefined;

    // Create embedding port (for vector search)
    const embedUri = resolveModelUri(
      config,
      "embed",
      options.embedModel,
      options.collection
    );
    const embedResult = await llm.createEmbeddingPort(embedUri, {
      policy,
      onProgress: downloadProgress
        ? (progress) => downloadProgress("embed", progress)
        : undefined,
    });
    if (embedResult.ok) {
      embedPort = embedResult.value;
    }

    // Create expansion port - optional.
    // Skip when structured query modes are provided.
    if (!options.noExpand && !options.queryModes?.length) {
      const expandUri = resolveModelUri(
        config,
        "expand",
        options.expandModel ?? options.genModel,
        options.collection
      );
      const genResult = await llm.createExpansionPort(expandUri, {
        policy,
        onProgress: downloadProgress
          ? (progress) => downloadProgress("expand", progress)
          : undefined,
      });
      if (genResult.ok) {
        expandPort = genResult.value;
      }
    }

    // Create rerank port - optional
    if (!options.noRerank) {
      const rerankUri = resolveModelUri(
        config,
        "rerank",
        options.rerankModel,
        options.collection
      );
      const rerankResult = await llm.createRerankPort(rerankUri, {
        policy,
        onProgress: downloadProgress
          ? (progress) => downloadProgress("rerank", progress)
          : undefined,
      });
      if (rerankResult.ok) {
        rerankPort = rerankResult.value;
      }
    }

    // Clear progress line if shown
    if (showProgress && downloadProgress) {
      process.stderr.write("\n");
    }

    // Create vector index (optional)
    let vectorIndex: VectorIndexPort | null = null;
    if (embedPort) {
      const embedInitResult = await embedPort.init();
      if (embedInitResult.ok) {
        const dimensions = embedPort.dimensions();
        const db = store.getRawDb();
        const vectorResult = await createVectorIndexPort(db, {
          model: embedUri,
          dimensions,
        });
        if (vectorResult.ok) {
          vectorIndex = vectorResult.value;
        }
      }
    }

    const deps: HybridSearchDeps = {
      store,
      config,
      vectorIndex,
      embedPort,
      expandPort,
      rerankPort,
    };

    const result = await searchHybrid(deps, queryText, {
      ...options,
      limit,
    });

    if (!result.ok) {
      return { success: false, error: result.error.message };
    }

    return {
      success: true,
      data: decorateSearchResultsForIndex(result.value, options.indexName),
    };
  } finally {
    if (embedPort) {
      await embedPort.dispose();
    }
    if (expandPort) {
      await expandPort.dispose();
    }
    if (rerankPort) {
      await rerankPort.dispose();
    }
    await store.close();
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Formatters
// ─────────────────────────────────────────────────────────────────────────────

// Import shared formatters dynamically to keep module loading fast
// and avoid circular dependencies

/**
 * Output explain data to stderr using pipeline formatters.
 */
function outputExplainToStderr(data: SearchResults): void {
  const explain = data.meta.explain;
  if (!explain) {
    return;
  }

  // Import pipeline formatters synchronously (they're lightweight)
  const {
    formatExplain,
    formatResultExplain,
  } = require("../../pipeline/explain");
  process.stderr.write(`${formatExplain(explain.lines)}\n`);
  process.stderr.write(`${formatResultExplain(explain.results)}\n`);
}

/**
 * Format query result for output.
 * Uses shared formatSearchResults for consistent output across search commands.
 */
export function formatQuery(
  result: QueryResult,
  options: QueryFormatOptions
): string {
  if (!result.success) {
    return options.format === "json"
      ? JSON.stringify({
          error: { code: "QUERY_FAILED", message: result.error },
        })
      : `Error: ${result.error}`;
  }

  // Output explain to stderr if present (async but best-effort)
  outputExplainToStderr(result.data);

  // Use shared formatter for consistent output
  // Dynamic import to keep module loading fast
  const { formatSearchResults } = require("../format/search-results");
  return formatSearchResults(result.data, {
    format: options.format,
    full: options.full,
    lineNumbers: options.lineNumbers,
    terminalLinks: options.terminalLinks,
  });
}
