import { spawnSync, spawn } from "node:child_process";
import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, mkdtempSync, rmdirSync } from "node:fs";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { resolvePythonExe } from "../python-env.js";
import { chromium, firefox, webkit } from "playwright";
import { getBootstrapState } from "../searxng/bootstrap.js";
import { isProcessAlive } from "../searxng/process.js";
import { getConfig } from "../config.js";
import { getRerankProvider } from "../providers/rerank-provider.js";
import { getEmbedProvider } from "../providers/embed-provider.js";
import { initDatabase, closeDatabase } from "../cache/db.js";
import { getCacheStats } from "../cache/store.js";
import { getBackgroundIndexQueue } from "../embedding/background-queue.js";
import { loadFeedConfig } from "../search/core/rss/feed-config.js";
import { getEngineHealthSummary } from "../search/core/engine-health.js";
import { isTelemetryEnabled } from "./telemetry.js";
import { allProviders, providerEnvVar, selectProvider } from "../integrations/cloud/llm/select.js";
import { resolveModel, providerDefaultModel, providerModelEnvVar } from "../integrations/cloud/llm/model-select.js";
import { readKey } from "../security/key-store.js";
import { setLogSuppression } from "../logger.js";
function out(line = "") {
  process.stderr.write(`${line}
`);
}
function maskApiKey(value) {
  if (!value) return "";
  if (value.length <= 4) return "*".repeat(value.length);
  const show = Math.min(8, Math.ceil(value.length * 0.25));
  return value.slice(0, show) + "*".repeat(Math.max(4, value.length - show));
}
function formatProviderDoctorLines(provider, location, keyValue) {
  const masked = maskApiKey(keyValue);
  return [
    `  provider:    ${provider} (key in ${location})`,
    `  key (masked): ${masked}`
  ];
}
function checkPython() {
  const python = resolvePythonExe();
  const r = spawnSync(python, ["--version"], { encoding: "utf-8" });
  if (r.status !== 0 || r.error) return { ok: false };
  const match = (r.stdout || r.stderr || "").match(/Python (\d+\.\d+\.\d+)/);
  return { ok: true, version: match?.[1] };
}
function checkDocker() {
  const r = spawnSync("docker", ["--version"], { encoding: "utf-8" });
  if (r.status !== 0 || r.error) return { ok: false };
  return { ok: true, version: (r.stdout || "").trim() };
}
function checkPlaywright() {
  let installed = false;
  let version;
  try {
    const r = spawnSync("npx", ["playwright", "--version"], { encoding: "utf-8", timeout: 5e3 });
    if (r.status === 0) {
      installed = true;
      const m = r.stdout.match(/(\d+\.\d+\.\d+)/);
      version = m?.[1];
    }
  } catch {
  }
  const probeBrowser = (api) => {
    try {
      const exec = api.executablePath();
      return { ok: !!exec && existsSync(exec), path: exec };
    } catch {
      return { ok: false };
    }
  };
  const chromiumProbe = probeBrowser(chromium);
  const firefoxProbe = probeBrowser(firefox);
  const webkitProbe = probeBrowser(webkit);
  return {
    installed,
    version,
    browsers: {
      chromium: chromiumProbe.ok,
      chromiumHeadlessShell: chromiumProbe.ok,
      firefox: firefoxProbe.ok,
      webkit: webkitProbe.ok
    },
    chromiumPath: chromiumProbe.path
  };
}
async function checkReranker() {
  try {
    const provider = await getRerankProvider();
    const docs = [
      "React Server Components render on the server.",
      "Next.js App Router uses RSC by default.",
      "Bananas are a popular fruit.",
      "TypeScript adds static types to JavaScript.",
      "The capital of France is Paris."
    ].map((text, i) => ({ id: String(i), text }));
    const t0 = Date.now();
    await provider.rerank("react server components", docs);
    const rerankMs = Date.now() - t0;
    return { installed: true, modelId: provider.modelId, rerankMs };
  } catch (err) {
    return { installed: false, reason: err instanceof Error ? err.message : "rerank failed" };
  }
}
function checkFastembedCache(dataDir) {
  const cacheDir = join(dataDir, "fastembed");
  if (!existsSync(cacheDir)) {
    return { installed: false, reason: "cache dir missing \u2014 run `wigolo warmup --embeddings`" };
  }
  try {
    const entries = readdirSync(cacheDir);
    if (entries.length === 0) {
      return { installed: false, reason: "cache empty \u2014 run `wigolo warmup --embeddings`" };
    }
    return { installed: true };
  } catch (err) {
    return { installed: false, reason: err instanceof Error ? err.message : "unknown error" };
  }
}
function probeWreqJsAvailable() {
  try {
    const req = createRequire(import.meta.url);
    req.resolve("wreq-js");
    return true;
  } catch {
    return false;
  }
}
function formatEngineHealthLines(entries) {
  if (entries.length === 0) {
    return ["  (no engines configured)"];
  }
  const sorted = [...entries].sort((a, b) => {
    if (a.vertical !== b.vertical) return a.vertical.localeCompare(b.vertical);
    return a.name.localeCompare(b.name);
  });
  const lines = [];
  for (const e of sorted) {
    const name = e.name.padEnd(15);
    const vertical = e.vertical.padEnd(10);
    const suffix = e.status !== "ok" && e.hint ? `${e.status} (${e.hint})` : e.status;
    lines.push(`  ${name} ${vertical} ${suffix}`);
  }
  return lines;
}
function formatTlsTierLine(mode, browser, wreqAvailable) {
  if (mode === "off") return "off (default)";
  if (!wreqAvailable) return `${mode} (wreq-js missing \u2014 fallback only)`;
  return `${mode} (${browser}, wreq-js \u2713)`;
}
function humanRetry(nextRetryAt) {
  if (!nextRetryAt) return "not scheduled";
  const when = new Date(nextRetryAt);
  const mins = Math.round((when.getTime() - Date.now()) / 6e4);
  if (mins < 0) return `${nextRetryAt} (ready now)`;
  if (mins < 60) return `${nextRetryAt} (in ${mins} minutes)`;
  const hrs = Math.round(mins / 60);
  return `${nextRetryAt} (in ${hrs} hours)`;
}
async function runDoctor(dataDir) {
  setLogSuppression("warn");
  try {
    return await runDoctorInner(dataDir);
  } finally {
    setLogSuppression(null);
  }
}
async function runDoctorInner(dataDir) {
  let degraded = false;
  out(`[wigolo doctor] Data dir:        ${dataDir}`);
  out("");
  const py = checkPython();
  const dk = checkDocker();
  out("[wigolo doctor] Runtime:");
  out(`  Python 3:      ${py.ok ? `available (${py.version ?? "unknown"})` : "not available"}`);
  out(`  Docker:        ${dk.ok ? `available (${dk.version})` : "not available"}`);
  if (!py.ok && !dk.ok) degraded = true;
  out("");
  const pw = checkPlaywright();
  out("[wigolo doctor] Browser engine:");
  out(`  Installation:  ${pw.installed ? `installed${pw.version ? ` (v${pw.version})` : ""}` : "not installed"}`);
  out(`  Browsers:      chromium ${pw.browsers.chromium ? "OK" : "missing"}  firefox ${pw.browsers.firefox ? "OK" : "missing"}  webkit ${pw.browsers.webkit ? "OK" : "missing"}`);
  if (pw.chromiumPath) {
    out(`  Chromium path: ${pw.chromiumPath}${pw.browsers.chromium ? "" : " (missing on disk)"}`);
  }
  if (!pw.browsers.chromium) {
    out("  Hint:          run 'npx playwright install chromium' \u2014 JS-rendered pages will fail without it");
    degraded = true;
  }
  out("");
  out("[wigolo doctor] Fetch tiers:");
  const tlsCfg = getConfig();
  const wreqAvailable = probeWreqJsAvailable();
  out(`  tls_tier:      ${formatTlsTierLine(tlsCfg.tlsTier, tlsCfg.tlsBrowser, wreqAvailable)}`);
  out("");
  const reranker = await checkReranker();
  const embeddings = checkFastembedCache(dataDir);
  out("[wigolo doctor] Optional components:");
  if (reranker.installed) {
    const timing = reranker.rerankMs !== void 0 ? ` \u2014 5-doc rerank ${reranker.rerankMs}ms` : "";
    out(`  ML reranker:        installed (${reranker.modelId})${timing}`);
  } else {
    out(`  ML reranker:        not installed${reranker.reason ? ` (${reranker.reason})` : ""}`);
  }
  if (embeddings.installed) {
    out(`  Embeddings model:   installed (fastembed BGE-small-en-v1.5)`);
  } else {
    out(`  Embeddings model:   not installed${embeddings.reason ? ` (${embeddings.reason})` : ""}`);
  }
  out("");
  out("[wigolo doctor] LLM (extract / research / agent):");
  const cfg = getConfig();
  const active = selectProvider(process.env);
  for (const p of allProviders()) {
    const envVar = providerEnvVar(p);
    const envSet = !!process.env[envVar];
    let keyLocation = "none";
    let maskedKey;
    try {
      const ksResult = await readKey(p, { dataDir: cfg.dataDir });
      if (ksResult) {
        keyLocation = ksResult.location;
        maskedKey = maskApiKey(ksResult.value);
      } else if (envSet) {
        keyLocation = "env";
        maskedKey = maskApiKey(process.env[envVar] ?? "");
      }
    } catch {
      if (envSet) {
        keyLocation = "env";
        maskedKey = maskApiKey(process.env[envVar] ?? "");
      }
    }
    const configured = keyLocation !== "none";
    const activeMark = p === active ? " <- active" : "";
    out(
      `  ${p.padEnd(10)} ${configured ? `configured (${keyLocation})` : "no key"} (${envVar}${envSet ? "" : " unset"})${activeMark}`
    );
    if (configured && maskedKey) {
      out(`    key (masked): ${maskedKey}`);
    }
    if (configured) {
      const model = resolveModel(p, void 0, process.env);
      const modelEnv = providerModelEnvVar(p);
      const usingDefault = model === providerDefaultModel(p) && !process.env[modelEnv] && !process.env.WIGOLO_LLM_MODEL;
      out(`    model:     ${model}${usingDefault ? " (default)" : ""}`);
      if (p === "gemini" && /pro/i.test(model)) {
        out(`    warning:   gemini pro models hit the free-tier 0/day quota \u2014 switch WIGOLO_LLM_MODEL_GEMINI to gemini-2.5-flash or gemini-2.5-flash-lite`);
      }
    }
  }
  if (cfg.llmProvider) {
    if (cfg.llmProvider.startsWith("http://") || cfg.llmProvider.startsWith("https://")) {
      out(`  override:    custom URL (${cfg.llmProvider})`);
    } else {
      out(`  override:    WIGOLO_LLM_PROVIDER=${cfg.llmProvider}`);
    }
  }
  if (process.env.WIGOLO_LLM_MODEL) {
    out(`  WIGOLO_LLM_MODEL: ${process.env.WIGOLO_LLM_MODEL} (universal override)`);
  }
  out(`  cache TTL:   ${cfg.llmCacheTtlDays} days`);
  out(`  per-request: ${cfg.llmMaxCallsPerRequest} call(s) max`);
  out("");
  out("[wigolo doctor] Search backend:");
  const rawBackend = process.env.WIGOLO_SEARCH;
  const aliased = rawBackend === "v1" ? "core (alias from v1, deprecated)" : null;
  const normalized = rawBackend === void 0 || rawBackend === "" || rawBackend === "v1" ? "core" : rawBackend;
  const valid = ["core", "searxng", "hybrid"].includes(normalized);
  const renderBackend = (name) => name === normalized ? `* ${name}` : `  ${name}`;
  out(`  Backend:       ${aliased ?? normalized}${valid ? "" : " (unknown \u2014 falling back to core)"} (default: core)`);
  out(`  Modes:         ${renderBackend("core")}   ${renderBackend("searxng")}   ${renderBackend("hybrid")}`);
  if (normalized === "hybrid") {
    const { SIGNAL_NAMES } = await import("../search/hybrid/signals.js");
    out(`  hybrid mode:   core runs first; falls back to searxng + RRF merge when any signal fires`);
    out(`  signals:       ${SIGNAL_NAMES.join(", ")}`);
  }
  out("");
  out("[wigolo doctor] Search engines:");
  try {
    const engineHealth = getEngineHealthSummary();
    for (const line of formatEngineHealthLines(engineHealth)) {
      out(line);
    }
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    out(`  (engine health summary failed: ${msg.slice(0, 80)})`);
  }
  out("");
  const state = getBootstrapState(dataDir);
  out("[wigolo doctor] Search engine:");
  if (!state) {
    out("  status:        not bootstrapped \u2014 run `npx @staticn0va/wigolo warmup`");
    degraded = true;
  } else if (state.status === "ready") {
    out(`  status:        ready`);
    out(`  path:          ${state.searxngPath ?? "unknown"}`);
  } else {
    out(`  status:        ${state.status}`);
    if (state.attempts !== void 0) out(`  attempts:      ${state.attempts} / 3`);
    if (state.lastAttemptAt) out(`  lastAttemptAt: ${state.lastAttemptAt}`);
    if (state.nextRetryAt || state.status === "failed") out(`  nextRetryAt:   ${humanRetry(state.nextRetryAt)}`);
    if (state.lastError?.command) out(`  command:       ${state.lastError.command}`);
    if (state.lastError?.exitCode !== void 0) out(`  exit code:     ${state.lastError.exitCode}`);
    if (state.lastError?.message) out(`  message:       ${state.lastError.message}`);
    if (state.lastError?.stderr) {
      out("  stderr:");
      for (const line of state.lastError.stderr.split("\n").slice(0, 20)) out(`    ${line}`);
    }
    degraded = true;
  }
  out("");
  const lockPath = join(dataDir, "searxng.lock");
  if (existsSync(lockPath)) {
    try {
      const lock = JSON.parse(readFileSync(lockPath, "utf-8"));
      if (lock.pid && isProcessAlive(lock.pid)) {
        out(`[wigolo doctor] Search engine process:  running (pid ${lock.pid}, port ${lock.port ?? "?"})`);
      } else {
        out("[wigolo doctor] Search engine process:  stale lock (process exited) \u2014 will be cleaned on next start");
      }
    } catch {
      out("[wigolo doctor] Search engine process:  lock file unparseable \u2014 will be cleaned on next start");
    }
  } else {
    out("[wigolo doctor] Search engine process:  not running (starts on-demand with MCP server)");
  }
  if (state?.status === "failed") {
    out("");
    out("[wigolo doctor] Recovery:");
    if (state.nextRetryAt) out(`  - Wait until next auto-retry (${humanRetry(state.nextRetryAt)}), or`);
    out(`  - Force retry now: npx @staticn0va/wigolo warmup --force`);
  }
  await checkCoreEmbeddings();
  await checkSqliteVec(dataDir);
  checkCacheStats(dataDir);
  checkBackgroundQueue(dataDir);
  checkRssFeeds(dataDir);
  checkTelemetryStatus();
  checkTuiEnv();
  if (normalized === "searxng" || normalized === "hybrid") {
    out("");
    out(`[wigolo doctor] Mode '${normalized}' note:`);
    if (state?.status === "ready") {
      out("  search engine: ready (will be used for this backend)");
    } else {
      out(`  search engine: not ready \u2014 ${normalized === "hybrid" ? "hybrid will degrade to core-only" : "searxng calls will fail"}`);
    }
  }
  out("");
  out(`[wigolo doctor] Overall: ${degraded ? "DEGRADED" : "OK"}`);
  return degraded ? 1 : 0;
}
async function checkCoreEmbeddings() {
  out("");
  out("[wigolo doctor] Core embeddings:");
  try {
    const provider = await getEmbedProvider();
    out(`  provider:      ready (fastembed ${provider.modelId}, dim=${provider.dim})`);
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    out(`  provider:      not ready (${msg.slice(0, 80)})`);
  }
}
async function checkSqliteVec(dataDir) {
  out("");
  out("[wigolo doctor] Core sqlite-vec:");
  let opened = false;
  try {
    const db = initDatabase(join(dataDir, "wigolo.db"));
    opened = true;
    try {
      const row = db.prepare("SELECT vec_version() AS v").get();
      const v = row?.v ?? "unknown";
      out(`  extension:     loaded (vec_version ${v})`);
    } catch {
      out("  extension:     not loaded (run warmup to load on next start)");
    }
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    out(`  extension:     (check failed: ${msg.slice(0, 80)})`);
  } finally {
    if (opened) {
      try {
        closeDatabase();
      } catch {
      }
    }
  }
}
function checkCacheStats(dataDir) {
  out("");
  out("[wigolo doctor] Local cache:");
  let opened = false;
  try {
    initDatabase(join(dataDir, "wigolo.db"));
    opened = true;
    const stats = getCacheStats();
    if (stats.total_urls === 0) {
      out("  urls:          0 (cache empty \u2014 populate via fetch/crawl)");
    } else {
      out(`  urls:          ${stats.total_urls}`);
      out(`  size:          ${stats.total_size_mb.toFixed(2)} MB`);
      if (stats.oldest) out(`  oldest:        ${stats.oldest}`);
      if (stats.newest) out(`  newest:        ${stats.newest}`);
    }
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    out(`  (check failed: ${msg.slice(0, 80)})`);
  } finally {
    if (opened) {
      try {
        closeDatabase();
      } catch {
      }
    }
  }
}
function checkBackgroundQueue(dataDir) {
  out("");
  out("[wigolo doctor] Background embedding queue:");
  let opened = false;
  try {
    initDatabase(join(dataDir, "wigolo.db"));
    opened = true;
    const queue = getBackgroundIndexQueue();
    const pending = queue.pendingSize();
    if (pending === 0) {
      out("  pending:       0 (idle)");
    } else {
      out(`  pending:       ${pending} job(s) \u2014 draining on next worker tick`);
    }
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    out(`  (check failed: ${msg.slice(0, 80)})`);
  } finally {
    if (opened) {
      try {
        closeDatabase();
      } catch {
      }
    }
  }
}
function checkRssFeeds(dataDir) {
  out("");
  out("[wigolo doctor] RSS feeds:");
  try {
    const { feeds } = loadFeedConfig({ dataDir });
    if (feeds.length === 0) {
      out("  feeds:         none configured (set WIGOLO_RSS_FEEDS to opt in)");
      return;
    }
    let db = null;
    try {
      db = initDatabase(join(dataDir, "wigolo.db"));
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      out(`  feeds:         ${feeds.length} configured (db unreadable: ${msg.slice(0, 60)})`);
      return;
    }
    let stmt = null;
    try {
      stmt = db.prepare(
        "SELECT COUNT(*) AS n, MAX(fetched_at) AS last_at FROM feed_items WHERE feed_url = ?"
      );
    } catch {
    }
    try {
      const now = Date.now();
      for (const feed of feeds) {
        let line;
        if (!stmt) {
          line = `  ${feed.url}  0 items [never polled]`;
        } else {
          try {
            const row = stmt.get(feed.url);
            const n = row?.n ?? 0;
            const lastAt = row?.last_at ?? null;
            if (!lastAt) {
              line = `  ${feed.url}  ${n} items [never polled]`;
            } else {
              const ageMs = now - new Date(lastAt).getTime();
              const ageHr = ageMs / 36e5;
              const fresh = ageHr <= 24 ? "fresh" : "stale";
              const ageLabel = ageHr < 1 ? `${Math.max(0, Math.round(ageMs / 6e4))}m ago` : `${Math.round(ageHr)}h ago`;
              const day = lastAt.slice(0, 10);
              line = `  ${feed.url}  ${n} items, last fetched ${day} (${ageLabel}) [${fresh}]`;
            }
          } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            line = `  ${feed.url}  (check failed: ${msg.slice(0, 60)})`;
          }
        }
        out(line);
      }
    } finally {
      try {
        closeDatabase();
      } catch {
      }
    }
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    out(`  (check failed: ${msg.slice(0, 80)})`);
  }
}
function checkTelemetryStatus() {
  out("");
  const state = isTelemetryEnabled() ? "enabled" : "disabled";
  out(`[wigolo doctor] Telemetry: opt-in ${state} (WIGOLO_TELEMETRY=1 to opt in)`);
}
function checkTuiEnv() {
  out("");
  out("[wigolo doctor] TUI env:");
  const reducedMotion = process.env.WIGOLO_TUI_REDUCED_MOTION;
  const reducedMotionActive = reducedMotion === "1" || process.env.CI === "true" || process.env.CI === "1" || process.stdout.isTTY === false;
  out(
    `  WIGOLO_TUI_REDUCED_MOTION: ${reducedMotion ?? "(unset)"} \u2014 ${reducedMotionActive ? "reduced motion active" : "animations enabled"} (set to 1 to disable spinners/gradient/transitions in the settings TUI)`
  );
}
const DOCTOR_CHILD_ENV = "WIGOLO_DOCTOR_CHILD";
function isPostExitNativeNoise(line) {
  return line.startsWith("libc++abi:") || line.includes("mutex lock failed: Invalid argument") || line.includes("terminating due to uncaught exception");
}
async function runDoctorIsolated(dataDir) {
  const sentinel = process.env[DOCTOR_CHILD_ENV];
  if (sentinel) {
    const code = await runDoctor(dataDir);
    try {
      writeFileSync(sentinel, String(code), "utf-8");
    } catch {
    }
    return code;
  }
  return runDoctorAsChild(dataDir);
}
async function runDoctorAsChild(dataDir) {
  if (process.env.WIGOLO_DOCTOR_INPROC === "1") {
    return runDoctor(dataDir);
  }
  const sentinelDir = mkdtempSync(join(tmpdir(), "wigolo-doctor-"));
  const sentinelPath = join(sentinelDir, "exit-code");
  const env = { ...process.env, [DOCTOR_CHILD_ENV]: sentinelPath };
  const entry = process.argv[1];
  if (!entry) {
    return runDoctor(dataDir);
  }
  const code = await new Promise((resolve) => {
    const child = spawn(process.execPath, [entry, "doctor"], {
      stdio: ["inherit", "inherit", "pipe"],
      env
    });
    if (child.stderr) {
      let buf = "";
      child.stderr.setEncoding("utf-8");
      child.stderr.on("data", (chunk) => {
        buf += chunk;
        let nl;
        while ((nl = buf.indexOf("\n")) !== -1) {
          const line = buf.slice(0, nl);
          buf = buf.slice(nl + 1);
          if (!isPostExitNativeNoise(line)) {
            process.stderr.write(`${line}
`);
          }
        }
      });
      child.stderr.on("end", () => {
        if (buf && !isPostExitNativeNoise(buf)) {
          process.stderr.write(buf);
        }
      });
    }
    child.on("exit", (exitCode, signal) => {
      try {
        if (existsSync(sentinelPath)) {
          const raw = readFileSync(sentinelPath, "utf-8").trim();
          const n = Number(raw);
          if (Number.isInteger(n) && n >= 0 && n <= 255) {
            resolve(n);
            return;
          }
        }
      } catch {
      }
      if (typeof exitCode === "number") {
        resolve(exitCode);
        return;
      }
      if (signal) {
        resolve(1);
        return;
      }
      resolve(1);
    });
    child.on("error", () => resolve(1));
  });
  try {
    unlinkSync(sentinelPath);
  } catch {
  }
  try {
    rmdirSync(sentinelDir);
  } catch {
  }
  return code;
}
export {
  formatEngineHealthLines,
  formatProviderDoctorLines,
  formatTlsTierLine,
  isPostExitNativeNoise,
  maskApiKey,
  runDoctor,
  runDoctorAsChild,
  runDoctorIsolated
};
//# sourceMappingURL=doctor.js.map