// @sarimarcus/content-sites-ui/webmcp (SLA-3084): read-only WebMCP tools (`document.modelContext`) for agents
// running in the visitor's browser. Nothing loads unless the browser exposes the API: the remote tool definitions
// and the Pagefind index are fetched lazily, and no request leaves the page until an agent calls a tool.

export interface WebMcpRemoteTool {
  name: string;
  title?: string;
  description: string;
  inputSchema?: object;
}

export interface WebMcpOptions {
  siteName: string;
  /** Registers `search_site` over the site's Pagefind index. */
  search?: boolean;
  /** MCP endpoint the remote tools are called on (stateless JSON-RPC `tools/call`); its CORS must allow the site. */
  remoteEndpoint?: string;
  /** A dynamic import of the tool definitions, so they never reach a browser without the API. */
  remoteTools?: () => Promise<{ default: WebMcpRemoteTool[] } | WebMcpRemoteTool[]>;
}

export interface WebMcpTool {
  name: string;
  title?: string;
  description: string;
  inputSchema?: object;
  annotations: { readOnlyHint: boolean };
  execute: (input: Record<string, unknown>, client?: { signal?: AbortSignal }) => Promise<unknown>;
}

interface PagefindResult {
  data(): Promise<{ url: string; meta?: Record<string, string>; excerpt?: string }>;
}

export interface PagefindLike {
  init?(): Promise<void>;
  search(query: string): Promise<{ results: PagefindResult[] } | null>;
}

export interface WebMcpDeps {
  fetch: typeof fetch;
  loadPagefind: () => Promise<PagefindLike>;
  origin: string;
}

const SEARCH_LIMIT_MAX = 20;

const stripTags = (html = '') => html.replace(/<[^>]*>/g, '');

export function searchTool(siteName: string, deps: WebMcpDeps): WebMcpTool {
  return {
    name: 'search_site',
    title: `Search ${siteName}`,
    description: `Full-text search of every page on ${siteName}. Returns each match's URL, title and a short excerpt, best match first.`,
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Words to search for.' },
        limit: { type: 'integer', minimum: 1, maximum: SEARCH_LIMIT_MAX, default: 8, description: 'Maximum number of results.' },
      },
      required: ['query'],
    },
    annotations: { readOnlyHint: true },
    async execute(input) {
      const query = String(input.query ?? '').trim();
      if (!query) throw new Error('search_site: query is required');
      const limit = Math.min(Math.max(Number(input.limit) || 8, 1), SEARCH_LIMIT_MAX);
      const pagefind = await deps.loadPagefind();
      const found = await pagefind.search(query);
      const data = await Promise.all((found?.results ?? []).slice(0, limit).map((r) => r.data()));
      return data.map((d) => ({
        url: new URL(d.url, deps.origin).href,
        title: d.meta?.title ?? '',
        excerpt: stripTags(d.excerpt),
      }));
    },
  };
}

/** Reads a JSON-RPC reply sent either as JSON or as a Streamable-HTTP SSE stream (last `data:` message wins). */
export async function readJsonRpc(res: Response): Promise<any> {
  if (!res.headers.get('content-type')?.includes('text/event-stream')) return res.json();
  const messages = (await res.text())
    .split(/\r?\n/)
    .filter((line) => line.startsWith('data:'))
    .map((line) => JSON.parse(line.slice(5)));
  if (!messages.length) throw new Error('empty event stream');
  return messages[messages.length - 1];
}

export async function callRemoteTool(
  endpoint: string,
  name: string,
  args: Record<string, unknown>,
  fetchImpl: typeof fetch,
  signal?: AbortSignal,
): Promise<unknown> {
  const res = await fetchImpl(endpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args ?? {} } }),
    signal,
  });
  if (!res.ok) throw new Error(`${name}: HTTP ${res.status}`);
  const body = await readJsonRpc(res);
  if (body.error) throw new Error(`${name}: ${body.error.message ?? 'JSON-RPC error'}`);
  const result = body.result ?? {};
  const text: string | undefined = result.content?.find((c: { type: string }) => c.type === 'text')?.text;
  if (result.isError) throw new Error(`${name}: ${text ?? 'tool error'}`);
  if (result.structuredContent !== undefined) return result.structuredContent;
  if (text === undefined) return null;
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}

export function remoteTool(def: WebMcpRemoteTool, endpoint: string, deps: WebMcpDeps): WebMcpTool {
  return {
    name: def.name,
    title: def.title,
    description: def.description,
    inputSchema: def.inputSchema,
    annotations: { readOnlyHint: true },
    execute: (input, client) => callRemoteTool(endpoint, def.name, input, deps.fetch, client?.signal),
  };
}

export async function buildWebMcpTools(options: WebMcpOptions, deps: WebMcpDeps): Promise<WebMcpTool[]> {
  const tools: WebMcpTool[] = [];
  if (options.search) tools.push(searchTool(options.siteName, deps));
  if (options.remoteEndpoint && options.remoteTools) {
    try {
      const mod = await options.remoteTools();
      const defs = Array.isArray(mod) ? mod : mod.default;
      for (const def of defs) tools.push(remoteTool(def, options.remoteEndpoint, deps));
    } catch {
      // search still registers when the definitions chunk fails to load
    }
  }
  return tools;
}

type ModelContext = { registerTool(tool: WebMcpTool): Promise<void> | void };

export function setupWebMcp(options: WebMcpOptions): void {
  const modelContext = (document as Document & { modelContext?: ModelContext }).modelContext;
  if (!modelContext) return;
  let pagefind: Promise<PagefindLike> | undefined;
  const deps: WebMcpDeps = {
    fetch: (...args) => fetch(...args),
    origin: location.origin,
    loadPagefind: () =>
      (pagefind ??= (async () => {
        // @ts-expect-error — emitted by the build, unresolvable at type-check time (as in searchPage.ts).
        const mod = (await import(/* @vite-ignore */ '/pagefind/pagefind.js')) as PagefindLike;
        await mod.init?.();
        return mod;
      })().catch((err) => {
        pagefind = undefined;
        throw err;
      })),
  };
  buildWebMcpTools(options, deps)
    .then((tools) => Promise.all(tools.map((tool) => Promise.resolve().then(() => modelContext.registerTool(tool)).catch(() => {}))))
    .catch(() => {});
}
