import { dirname, join } from "node:path";
import { mkdirSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { SmartRouter } from "./fetch/router.js";
import { MultiBrowserPool } from "./fetch/browser-pool.js";
import { closeDaemonBrowser } from "./fetch/playwright-tier.js";
import { httpFetch } from "./fetch/http-client.js";
import { initDatabase, closeDatabase } from "./cache/db.js";
import { handleFetch } from "./tools/fetch.js";
import { handleSearch } from "./tools/search.js";
import { buildSearchContentBlocks } from "./server/search-response.js";
import { handleCrawl } from "./tools/crawl.js";
import { handleCache } from "./tools/cache.js";
import { handleExtract } from "./tools/extract.js";
import { handleFindSimilar } from "./tools/find-similar.js";
import { handleResearch } from "./tools/research.js";
import { handleAgent } from "./tools/agent.js";
import { handleDiff } from "./tools/diff.js";
import { handleWatch } from "./tools/watch.js";
import { scheduleOverdueCheck } from "./watch/scheduler.js";
import { SearxngClient } from "./search/searxng.js";
import { DuckDuckGoEngine } from "./search/engines/duckduckgo.js";
import { BingEngine } from "./search/engines/bing.js";
import { StartpageEngine } from "./search/engines/startpage.js";
import { resolveSearchBackend, bootstrapNativeSearxng, getBootstrapState } from "./searxng/bootstrap.js";
import { SearxngProcess } from "./searxng/process.js";
import { DockerSearxng } from "./searxng/docker.js";
import { BackendStatus } from "./server/backend-status.js";
import { maybeEagerWarmup } from "./server/warmup-on-start.js";
import { getEmbeddingService, resetEmbeddingService } from "./embedding/embed.js";
import { getConfig } from "./config.js";
import { createLogger } from "./logger.js";
import {
  WIGOLO_INSTRUCTIONS,
  WIGOLO_INSTRUCTIONS_FULL,
  WIGOLO_DOCS_URI,
  TOOL_DESCRIPTIONS
} from "./instructions.js";
import {
  FETCH_TOOL_SCHEMA,
  SEARCH_TOOL_SCHEMA,
  CRAWL_TOOL_SCHEMA,
  CACHE_TOOL_SCHEMA,
  EXTRACT_TOOL_SCHEMA,
  FIND_SIMILAR_TOOL_SCHEMA,
  RESEARCH_TOOL_SCHEMA,
  AGENT_TOOL_SCHEMA,
  DIFF_TOOL_SCHEMA,
  WATCH_TOOL_SCHEMA
} from "./server/tool-schemas.js";
import { loadPlugins } from "./plugins/loader.js";
import { PluginRegistry } from "./plugins/registry.js";
import { registerExtractor } from "./extraction/pipeline.js";
const log = createLogger("server");
function readPackageVersion() {
  try {
    const here = dirname(fileURLToPath(import.meta.url));
    const pkgPath = join(here, "..", "package.json");
    const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
    return pkg.version ?? "0.0.0";
  } catch {
    return "0.0.0";
  }
}
const SERVER_VERSION = readPackageVersion();
async function initSubsystems() {
  const config = getConfig();
  mkdirSync(config.dataDir, { recursive: true });
  initDatabase(join(config.dataDir, "wigolo.db"));
  try {
    await getEmbeddingService().init();
  } catch (err) {
    log.warn("embedding service init failed, find_similar will run without embedding path", {
      error: String(err)
    });
  }
  const httpClient = {
    fetch: (url, options) => httpFetch(url, options)
  };
  const browserPool = new MultiBrowserPool({
    browserTypes: config.browserTypes,
    selectionStrategy: "round-robin"
  });
  const router = new SmartRouter(httpClient, browserPool);
  const backendStatus = new BackendStatus();
  const searchEngines = [
    new BingEngine(),
    new DuckDuckGoEngine(),
    new StartpageEngine()
  ];
  const pluginRegistry = new PluginRegistry();
  try {
    const pluginResult = await loadPlugins();
    for (const ext of pluginResult.extractors) {
      pluginRegistry.registerExtractor(ext, ext.name);
      registerExtractor(ext);
    }
    for (const eng of pluginResult.searchEngines) {
      pluginRegistry.registerSearchEngine(eng, eng.name);
      searchEngines.push(eng);
    }
    if (pluginResult.errors.length > 0) {
      log.warn("some plugins failed to load", {
        errors: pluginResult.errors.map((e) => `${e.pluginName}: ${e.message}`)
      });
    }
    if (pluginResult.loaded.length > 0) {
      log.info("plugins loaded", {
        count: pluginResult.loaded.length,
        names: pluginResult.loaded.map((p) => p.name)
      });
    }
  } catch (err) {
    log.error("plugin loading failed", { error: String(err) });
  }
  let searxngProcess = null;
  let dockerSearxng = null;
  let searxngBootstrap = null;
  async function bootstrapSearxng() {
    try {
      const initialState = getBootstrapState(config.dataDir);
      if (!config.searxngUrl && initialState?.status !== "ready") {
        backendStatus.markBootstrapping();
      }
      const backend = await resolveSearchBackend();
      if (backend.type === "external" && backend.url) {
        searchEngines.unshift(new SearxngClient(backend.url));
        backendStatus.markHealthy();
        log.info("using external search engine", { url: backend.url });
        return;
      }
      if (backend.type === "native" && backend.searxngPath) {
        const state = getBootstrapState(config.dataDir);
        if (state?.status !== "ready") {
          log.info("search engine not ready \u2014 bootstrapping in background; search uses fallback engines until ready");
          try {
            await bootstrapNativeSearxng(config.dataDir);
          } catch (err) {
            log.warn("search engine bootstrap failed, continuing with fallback scraping");
            backendStatus.markUnhealthy(`bootstrap exception: ${String(err)}`);
            return;
          }
        }
        const postBootstrapState = getBootstrapState(config.dataDir);
        if (postBootstrapState?.status === "ready") {
          searxngProcess = new SearxngProcess(backend.searxngPath, config.dataDir, {
            onUnhealthy: (reason) => {
              backendStatus.markUnhealthy(reason);
              const idx = searchEngines.findIndex((e) => e.name === "searxng");
              if (idx >= 0) searchEngines.splice(idx, 1);
              log.warn("search engine marked unhealthy", { reason });
            },
            onHealthy: () => {
              const url2 = searxngProcess?.getUrl();
              if (!url2) return;
              backendStatus.markHealthy();
              if (!searchEngines.some((e) => e.name === "searxng")) {
                searchEngines.unshift(new SearxngClient(url2));
              }
              log.info("search engine recovered");
            }
          });
          const url = await searxngProcess.start();
          if (url) {
            searchEngines.unshift(new SearxngClient(url));
            backendStatus.markHealthy();
            log.info("search engine ready", { url });
          } else {
            log.warn("search engine failed to start, using fallback scraping");
            backendStatus.markUnhealthy("search engine process failed to start");
          }
        }
        return;
      }
      if (backend.type === "docker") {
        dockerSearxng = new DockerSearxng();
        const url = await dockerSearxng.start();
        if (url) {
          searchEngines.unshift(new SearxngClient(url));
          backendStatus.markHealthy();
          log.info("search engine (docker) ready", { url });
        } else {
          log.warn("search engine (docker) failed to start, using fallback scraping");
          backendStatus.markUnhealthy("search engine (docker) failed to start");
        }
      }
      if (backend.type === "scraping") {
        const state = getBootstrapState(config.dataDir);
        const reason = state?.lastError?.message ?? state?.error ?? "no search engine backend available";
        backendStatus.markUnhealthy(reason);
      }
    } catch (err) {
      log.warn("background backend setup failed", { error: String(err) });
      backendStatus.markUnhealthy(`backend setup failed: ${String(err)}`);
    }
  }
  async function shutdown() {
    log.info("Shutting down");
    if (searxngBootstrap) {
      await Promise.race([
        searxngBootstrap.catch(() => {
        }),
        new Promise((r) => setTimeout(r, 2e3))
      ]);
    }
    if (searxngProcess) await searxngProcess.stop();
    if (dockerSearxng) await dockerSearxng.stop();
    await browserPool.shutdown();
    await closeDaemonBrowser().catch((e) => log.debug("closeDaemonBrowser failed", { error: e instanceof Error ? e.message : String(e) }));
    resetEmbeddingService();
    closeDatabase();
  }
  return {
    searchEngines,
    browserPool,
    router,
    backendStatus,
    pluginRegistry,
    shutdown,
    bootstrapSearxng: () => {
      searxngBootstrap = bootstrapSearxng();
      return searxngBootstrap;
    }
  };
}
function createMcpServer(subsystems) {
  const { searchEngines, router, backendStatus } = subsystems;
  const server = new Server(
    { name: "wigolo", version: SERVER_VERSION },
    {
      capabilities: { tools: {}, resources: {} },
      instructions: WIGOLO_INSTRUCTIONS
    }
  );
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
    resources: [
      {
        uri: WIGOLO_DOCS_URI,
        name: "Wigolo usage guide",
        description: "Routing tables, performance budgets, auth flows, and other detail trimmed from the per-session instructions.",
        mimeType: "text/markdown"
      }
    ]
  }));
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    if (request.params.uri !== WIGOLO_DOCS_URI) {
      throw new Error(`Unknown resource: ${request.params.uri}`);
    }
    return {
      contents: [
        {
          uri: WIGOLO_DOCS_URI,
          mimeType: "text/markdown",
          text: WIGOLO_INSTRUCTIONS_FULL
        }
      ]
    };
  });
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools: [
      {
        name: "fetch",
        description: TOOL_DESCRIPTIONS.fetch,
        inputSchema: FETCH_TOOL_SCHEMA
      },
      {
        name: "search",
        description: TOOL_DESCRIPTIONS.search,
        inputSchema: SEARCH_TOOL_SCHEMA
      },
      {
        name: "crawl",
        description: TOOL_DESCRIPTIONS.crawl,
        inputSchema: CRAWL_TOOL_SCHEMA
      },
      {
        name: "cache",
        description: TOOL_DESCRIPTIONS.cache,
        inputSchema: CACHE_TOOL_SCHEMA
      },
      {
        name: "extract",
        description: TOOL_DESCRIPTIONS.extract,
        inputSchema: EXTRACT_TOOL_SCHEMA
      },
      {
        name: "find_similar",
        description: TOOL_DESCRIPTIONS.find_similar,
        inputSchema: FIND_SIMILAR_TOOL_SCHEMA
      },
      {
        name: "research",
        description: TOOL_DESCRIPTIONS.research,
        inputSchema: RESEARCH_TOOL_SCHEMA
      },
      {
        name: "agent",
        description: TOOL_DESCRIPTIONS.agent,
        inputSchema: AGENT_TOOL_SCHEMA
      },
      {
        name: "diff",
        description: TOOL_DESCRIPTIONS.diff,
        inputSchema: DIFF_TOOL_SCHEMA
      },
      {
        name: "watch",
        description: TOOL_DESCRIPTIONS.watch,
        inputSchema: WATCH_TOOL_SCHEMA
      }
    ]
  }));
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
    const { name, arguments: args } = request.params;
    if (name !== "watch") {
      scheduleOverdueCheck(router);
    }
    const meta = request.params._meta;
    const progressToken = meta?.progressToken;
    const onProgress = progressToken !== void 0 && extra && typeof extra.sendNotification === "function" ? async (update) => {
      try {
        await extra.sendNotification({
          method: "notifications/progress",
          params: {
            progressToken,
            progress: update.progress,
            total: update.total,
            message: update.message
          }
        });
      } catch (err) {
        log.debug("sendNotification failed", { error: String(err) });
      }
    } : void 0;
    if (name === "fetch") {
      const input = args ?? {};
      const r = await handleFetch(input, router);
      if (!r.ok) {
        return {
          content: [{ type: "text", text: JSON.stringify({ error: r.error, error_reason: r.error_reason, stage: r.stage, ...r.hint ? { hint: r.hint } : {} }, null, 2) }],
          isError: true
        };
      }
      return {
        content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }],
        isError: false
      };
    }
    if (name === "search") {
      const input = args ?? {};
      const samplingServer = server;
      const r = await handleSearch(input, searchEngines, router, backendStatus, samplingServer, onProgress);
      if (!r.ok) {
        return {
          content: [{ type: "text", text: JSON.stringify({ error: r.error, error_reason: r.error_reason, stage: r.stage, ...r.hint ? { hint: r.hint } : {} }, null, 2) }],
          isError: true
        };
      }
      const blocks = buildSearchContentBlocks(input, r.data);
      return {
        content: blocks,
        isError: !!r.data.error
      };
    }
    if (name === "crawl") {
      const input = args ?? {};
      const result = await handleCrawl(input, router);
      return {
        content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
        isError: !!result.error
      };
    }
    if (name === "cache") {
      const input = args ?? {};
      const result = await handleCache(input, router);
      return {
        content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
        isError: !!result.error
      };
    }
    if (name === "extract") {
      const input = args ?? {};
      const r = await handleExtract(input, router);
      if (!r.ok) {
        return {
          content: [{ type: "text", text: JSON.stringify({ error: r.error, error_reason: r.error_reason, stage: r.stage, ...r.hint ? { hint: r.hint } : {} }, null, 2) }],
          isError: true
        };
      }
      return {
        content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }],
        isError: false
      };
    }
    if (name === "find_similar") {
      const input = args ?? {};
      const r = await handleFindSimilar(input, searchEngines, router, backendStatus);
      if (!r.ok) {
        return {
          content: [{ type: "text", text: JSON.stringify({ error: r.error, error_reason: r.error_reason, stage: r.stage, ...r.hint ? { hint: r.hint } : {} }, null, 2) }],
          isError: true
        };
      }
      return {
        content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }],
        isError: false
      };
    }
    if (name === "research") {
      const input = args ?? {};
      const samplingServer = server;
      const r = await handleResearch(input, searchEngines, router, backendStatus, samplingServer);
      if (!r.ok) {
        return {
          content: [{ type: "text", text: JSON.stringify({ error: r.error, error_reason: r.error_reason, stage: r.stage, ...r.hint ? { hint: r.hint } : {} }, null, 2) }],
          isError: true
        };
      }
      return {
        content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }],
        isError: false
      };
    }
    if (name === "agent") {
      const input = args ?? {};
      const samplingServer = server;
      const r = await handleAgent(input, searchEngines, router, backendStatus, samplingServer);
      if (!r.ok) {
        return {
          content: [{ type: "text", text: JSON.stringify({ error: r.error, error_reason: r.error_reason, stage: r.stage, ...r.hint ? { hint: r.hint } : {} }, null, 2) }],
          isError: true
        };
      }
      return {
        content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }],
        isError: false
      };
    }
    if (name === "diff") {
      const input = args ?? {};
      const r = await handleDiff(input);
      return {
        content: [{ type: "text", text: JSON.stringify(r.ok ? r.data : { error: r.error, error_reason: r.error_reason, stage: r.stage }, null, 2) }],
        isError: !r.ok
      };
    }
    if (name === "watch") {
      const input = args ?? {};
      const r = await handleWatch(input, router);
      return {
        content: [{ type: "text", text: JSON.stringify(r.ok ? r.data : { error: r.error, error_reason: r.error_reason, stage: r.stage, ...r.hint ? { hint: r.hint } : {} }, null, 2) }],
        isError: !r.ok
      };
    }
    return {
      content: [{ type: "text", text: `Unknown tool: ${name}` }],
      isError: true
    };
  });
  return server;
}
async function startServer() {
  const subs = await initSubsystems();
  const server = createMcpServer(subs);
  const transport = new StdioServerTransport();
  await server.connect(transport);
  log.info("MCP server started");
  maybeEagerWarmup();
  subs.bootstrapSearxng().catch((err) => {
    log.warn("search engine bootstrap failed", { error: String(err) });
  });
  const shutdown = async () => {
    await subs.shutdown();
    await server.close();
    process.exit(0);
  };
  process.on("SIGINT", shutdown);
  process.on("SIGTERM", shutdown);
}
export {
  createMcpServer,
  initSubsystems,
  startServer
};
//# sourceMappingURL=server.js.map