/**
 * Shared retrieve primitive for the agentic harness and readonly MCP.
 * Same object both consumers see: readOnly forward + cross-cube blend +
 * skipSynthesis cascade, plus matchQuality band and declined flag.
 *
 * Does NOT mutate dict / chat rows. Does NOT call the LLM.
 */

import { log } from "../logger.js";
import { semanticEmbed, isEmbedderReady } from "../embeddings/semantic-embedder.js";
import { getGeneralistEngine, getRoutingCubeIds } from "../multi-cube/cube-registry.js";
import { searchAcrossCubes } from "../multi-cube/hnsw-routing.js";

/** Same default as the synthesizer grounding gate (env-overridable). */
const DECLINE_FLOOR = Math.min(0.95, Math.max(0.5,
  parseFloat(process.env.GROUNDING_DECLINE_THRESHOLD ?? "0.750")));

export type RetrieveBand = "unknown" | "partial" | "match" | "strong";

export interface RetrieveHit {
  phrase: string;
  cube: string;
  sim: number;
  node: number;
  ce?: number;
  /** Provenance id (2026-09-13) — `doc-<hash>` / `crawl:<sha>` / `hf:<path>`. */
  sourceId?: string;
  /** Human source when the registry / manifest / macro chunk resolved it:
   *  the document title, or the thesis in quotes as a fallback. */
  source?: string;
}

export interface RetrieveResult {
  query: string;
  matchQuality: number;
  band: RetrieveBand;
  declined: boolean;
  winnerNode: number | null;
  contributingCubes: string[];
  matches: RetrieveHit[];
  /** Best cross-encoder score on the scored head; absent if rerank skipped. */
  topCe?: number;
  /** Smear-band CE-gate: querySim passed, CE said unrelated. */
  ceDeclined?: boolean;
  cubeFloor?: { cube: string; sim: number; floor: number; excess: number };
  cubeFloorWouldDecline?: boolean;
}

/** AUDIT summary of one retrieve call (chip 9, 2026-09-05) — handed to
 *  `opts.onAudit`, NEVER part of the JSON the reader sees (entryHash would
 *  cost tool-budget chars for nothing). */
export interface RetrieveAudit {
  tool?: string;
  query: string;
  topK: number;
  declined: boolean;
  band: RetrieveBand;
  matchQuality: number;
  ceDeclined?: boolean;
  hits: Array<{ cube: string; sim: number; node: number; entryHash: string | null; prefix: string }>;
}

export interface RetrieveOpts {
  onAudit?: (audit: RetrieveAudit) => void;
}

export function matchQualityBand(mq: number): RetrieveBand {
  if (mq < 0.74) return "unknown";
  if (mq < 0.78) return "partial";
  if (mq < 0.82) return "match";
  return "strong";
}

export async function retrieveKnowledge(query: string, topN = 8, opts?: RetrieveOpts): Promise<RetrieveResult> {
  const q = query.trim();
  const n = Math.max(1, Math.min(12, Math.trunc(topN) || 8));
  if (!q) {
    return {
      query: q,
      matchQuality: 0,
      band: "unknown",
      declined: true,
      winnerNode: null,
      contributingCubes: [],
      matches: [],
    };
  }
  if (!isEmbedderReady()) {
    throw new Error("embedder not ready");
  }

  const engine = getGeneralistEngine();
  const emb = await semanticEmbed(q, "search_query");
  const vec: number[] = new Array(emb.length);
  for (let i = 0; i < emb.length; i++) vec[i] = Math.fround(emb[i]);

  const readyCubeIds = getRoutingCubeIds();
  const [forwardResult, crossCubeMatches] = await Promise.all([
    engine.forwardAsync(q, { readOnly: true }),
    readyCubeIds.length > 0 ? searchAcrossCubes(vec, readyCubeIds, 30) : Promise.resolve([]),
  ]);
  if (!forwardResult) {
    return {
      query: q,
      matchQuality: 0,
      band: "unknown",
      declined: true,
      winnerNode: null,
      contributingCubes: [],
      matches: [],
    };
  }

  // Dynamic import: synthesizer → large-harness → chat-tools → this module.
  const { synthesizeResponse } = await import("../synthesis/response-synthesizer.js");
  const synth = await synthesizeResponse(q, engine, forwardResult, {
    readOnly: true,
    skipSynthesis: true,
    promptTopN: n,
    geometryTopN: n,
    crossCubeMatches,
  });

  const matches: RetrieveHit[] = (synth.cascade ?? []).slice(0, n).map(m => ({
    phrase: String(m.phrase ?? "").slice(0, 400),
    cube: String(m.cubeId ?? "generalist"),
    sim: Number((m.querySim ?? 0).toFixed(3)),
    node: Number(m.nodeAffinity ?? -1),
    ...(((m as { ceScore?: number }).ceScore != null)
      ? { ce: Number((m as { ceScore?: number }).ceScore!.toFixed(3)) }
      : {}),
    ...(m.sourceId ? { sourceId: m.sourceId } : {}),
  }));
  // Provenance titles — after ranking, only for the ids being shown. Scores
  // are untouched; this is display. Best-effort: a resolver failure leaves
  // the bare ids in place.
  try {
    const { resolveSources, sourceLabel } = await import("../ingestion/source-resolver.js");
    const resolved = await resolveSources(matches.map(m => m.sourceId));
    for (const m of matches) {
      const r = m.sourceId ? resolved.get(m.sourceId) : undefined;
      if (r && r.resolvedBy !== "unresolved") m.source = sourceLabel(r);
    }
  } catch (err: any) {
    log(`[retrieve] source resolution skipped: ${err?.message ?? err}`, "server");
  }
  // Gate on max organic querySim (same as synthConfidence), not forward-pass matchQuality.
  const mq = matches.reduce((mx, m) => Math.max(mx, m.sim), 0);
  const topCe = typeof synth.topCe === "number" ? Number(synth.topCe.toFixed(3)) : undefined;
  const { shouldCeDecline } = await import("../synthesis/reranker.js");
  const ceDeclined = synth.ceDeclined === true || shouldCeDecline({
    bestQuerySim: mq,
    topCe,
    querySimDeclineFloor: DECLINE_FLOOR,
  });
  const { evaluateCubeFloors, cubeFloorGateEnabled } = await import("./cube-floors.js");
  const floorEval = evaluateCubeFloors(matches.map(m => ({ cube: m.cube, sim: m.sim })));
  if (floorEval.best && floorEval.wouldDecline) {
    log(
      `[cube-floor] ${cubeFloorGateEnabled() ? "decline" : "SHADOW-decline"} cube=${floorEval.best.cube} sim=${floorEval.best.sim.toFixed(3)} floor=${floorEval.best.floor.toFixed(3)} excess=${floorEval.best.excess.toFixed(3)} gate=${cubeFloorGateEnabled() ? "on" : "off"}`,
      "server",
    );
  }
  const floorDeclined = cubeFloorGateEnabled() && floorEval.wouldDecline;
  const declined = mq < DECLINE_FLOOR || ceDeclined || floorDeclined;

  if (opts?.onAudit) {
    try {
      const cascade = synth.cascade ?? [];
      opts.onAudit({
        query: q,
        topK: n,
        declined,
        band: declined ? "unknown" : matchQualityBand(mq),
        matchQuality: Number(mq.toFixed(3)),
        ...(ceDeclined ? { ceDeclined: true } : {}),
        hits: matches.map((m, i) => ({
          cube: m.cube,
          sim: m.sim,
          node: m.node,
          entryHash: (cascade[i] as { entryHash?: string } | undefined)?.entryHash ?? null,
          prefix: m.phrase.replace(/\s+/g, " ").trim().slice(0, 160),
        })),
      });
    } catch { /* audit is best-effort */ }
  }

  return {
    declined,
    band: declined ? "unknown" : matchQualityBand(mq),
    matchQuality: Number(mq.toFixed(3)),
    query: q,
    winnerNode: typeof (forwardResult as any).winnerNode === "number" ? (forwardResult as any).winnerNode : null,
    contributingCubes: synth.contributingCubes ?? [],
    matches,
    ...(topCe != null ? { topCe } : {}),
    ...(ceDeclined ? { ceDeclined: true } : {}),
    ...(floorEval.best ? { cubeFloor: floorEval.best, cubeFloorWouldDecline: floorEval.wouldDecline } : {}),
  };
}
