/**
 * @file tools/foundryWebSearchTool.js
 *
 * Azure AI Foundry web-search tool — wraps the backend's
 * /llm/web-search endpoint (Foundry Responses API + Grounding with
 * Bing Search).
 *
 * What this tool is FOR:
 *   - "Research X and cite your sources" — agent gets a synthesized
 *     answer + URL citations in one round-trip.
 *   - Time-sensitive lookups ("latest version of …", "what happened
 *     this week with …") where the model would otherwise guess from
 *     training data.
 *   - Anywhere you want CITATIONS in the response, not just raw URLs.
 *
 * What this tool is NOT for:
 *   - "Give me 10 raw search result links" — use the `web` tool
 *     (puppeteer-based) for that. The Foundry path doesn't expose
 *     raw URLs; it only returns what the model cited.
 *   - Sites that need login/interaction — Foundry's tool reads
 *     public web pages only. The `web` tool handles authenticated
 *     browsing.
 *
 * Architecture: model-centric. The agent decides locally to call this
 * tool; the tool fires ONE backend HTTP request; the backend fires
 * ONE Foundry HTTP request; Foundry runs the model+search+model loop
 * server-side and returns a single answer. No streaming, no agent
 * state on Foundry's side.
 */

import { BaseTool } from './baseTool.js';
import TagParser from '../utilities/tagParser.js';

class FoundryWebSearchTool extends BaseTool {
  constructor(config = {}, logger = null) {
    super(config, logger);
    this.id = 'foundry_web_search';
    this.name = 'Foundry Web Search (Grounded)';
    this.version = '1.0.0';
    this.requiresProject = false;
    this.isAsync = true;
    // Foundry round-trip is normally 2-5s but multi-search runs can
    // hit 10s+. Don't penalize the agent with a long enforced wait —
    // the aiService timeout is the real ceiling.
    this.builtinDelay = 500;

    // Reference to the AI service holding baseUrl + apiKeyManager.
    // Set by ToolsRegistry.registerTool via setAIService().
    this.aiService = null;
  }

  /**
   * Injected by the registry so we can reuse the auth + URL plumbing
   * the image / video tools already use. Without it, the tool throws
   * a clear "not initialised" error rather than half-running.
   */
  setAIService(aiService) {
    this.aiService = aiService;
  }

  getDescription() {
    return `
Foundry Web Search Tool: Grounded web search via Azure AI Foundry +
Bing. Returns a synthesized answer with citation URLs. Use this when
you need CURRENT information from the web AND want sources cited.

USAGE:

\`\`\`json
{
  "toolId": "foundry_web_search",
  "parameters": {
    "query": "What is the current Node.js LTS version this week?"
  }
}
\`\`\`

Optional parameters:
  - "instructions": override the default tool-use nudge. Use only when
    the model is ignoring the search; the default already says
    "always call the web_search_preview tool".
  - "model": chat model to orchestrate the search. Defaults to "gpt-4o"
    (must have Grounding with Bing Search bound in Foundry).

XML form (when JSON isn't convenient):

\`\`\`
<foundry_web_search>
  <query>Latest stable Postgres version</query>
</foundry_web_search>
\`\`\`

RESPONSE SHAPE:
  - answer    : string (model's synthesized answer)
  - citations : array of { url, title, start, end } — the URLs the
                model cited, with indices into \`answer\` so you can
                render inline links.
  - usage     : { inputTokens, outputTokens, webSearches } — number
                of Bing searches actually invoked. If \`webSearches:0\`
                the model didn't use the tool (it thought it already
                knew the answer); rephrase the query with explicit
                "search the web for ..." wording.

WHEN TO USE:
  ✓ "Research X and cite sources"
  ✓ "What's the latest …" (time-sensitive)
  ✓ "Compare these companies' recent earnings" (factual + current)

WHEN NOT TO USE:
  ✗ "Give me 10 raw search links" — use the \`web\` tool
  ✗ "Log in to … and …" — use the \`web\` tool
  ✗ Questions answerable from training data (waste of $)

COST:
  Per call: ~$0.007 (Bing) + chat-completion tokens ($0.01–0.05 typical).
  The model may invoke multiple searches per call — usage.webSearches
  shows how many fired. Each one bills.

LIMITATIONS:
  Only the URLs the model CITED come back. The raw Bing results list
  is hidden. For "show me all 10 results from a search", use the
  \`web\` tool.
    `.trim();
  }

  /**
   * Parse parameters from tool command content. Supports both
   * raw-XML form and JSON-in-content form (the latter is the
   * standard agentic invocation).
   */
  parseParameters(content) {
    try {
      // Prefer JSON: most agents emit JSON tool calls.
      try {
        const trimmed = (content || '').trim();
        if (trimmed.startsWith('{')) {
          const parsed = JSON.parse(trimmed);
          // Agents may pass either {query: ...} OR {parameters: {query: ...}}
          const inner = parsed.parameters || parsed;
          return {
            query: typeof inner.query === 'string' ? inner.query.trim() : null,
            instructions: typeof inner.instructions === 'string' ? inner.instructions : undefined,
            model: typeof inner.model === 'string' ? inner.model : undefined,
          };
        }
      } catch { /* fall through to XML */ }

      const queryMatches = TagParser.extractContent(content, 'query');
      const instructionsMatches = TagParser.extractContent(content, 'instructions');
      const modelMatches = TagParser.extractContent(content, 'model');
      return {
        query: queryMatches.length > 0 ? queryMatches[0].trim() : null,
        instructions: instructionsMatches.length > 0 ? instructionsMatches[0].trim() : undefined,
        model: modelMatches.length > 0 ? modelMatches[0].trim() : undefined,
      };
    } catch (error) {
      throw new Error(`Failed to parse foundry_web_search parameters: ${error.message}`);
    }
  }

  getSupportedActions() {
    return ['search'];
  }

  /**
   * Execute the search.
   */
  async execute(params, context) {
    if (!this.aiService) {
      return {
        success: false,
        error: 'foundry_web_search not initialised: aiService reference missing',
        output: 'Internal configuration issue. This tool needs the AI service injected at startup.',
      };
    }

    const { query, instructions, model } = params || {};
    if (!query) {
      return {
        success: false,
        error: 'query is required',
        output: 'Provide a `query` parameter with the search request.',
      };
    }

    try {
      // Idempotency key derived from agent + content hash; lets a
      // repeated identical query inside the same turn reuse the
      // prior billing row (the backend dedups on the same key).
      const idempotencyKey = this._buildIdempotencyKey(context, query);

      const result = await this.aiService.foundryWebSearch(query, {
        instructions,
        model,
        idempotencyKey,
        sessionId: context?.sessionId,
        platformProvided: context?.platformProvided !== false,
      });

      // Format for agent consumption. The answer goes in `output` so
      // the agent reads it as the primary result; structured fields
      // (citations, usage) live alongside for any downstream tool
      // that wants to act on URLs.
      const citationsBlock = (result.citations || []).length > 0
        ? '\n\nSources:\n' + result.citations.map((c, i) =>
            `  [${i + 1}] ${c.title || '(untitled)'} — ${c.url}`).join('\n')
        : '\n\n(No sources cited.)';

      const searchCount = result.usage?.webSearches || 0;
      const meta = ` [searches=${searchCount}, tokens=${result.usage?.inputTokens || 0}in/${result.usage?.outputTokens || 0}out]`;

      return {
        success: true,
        output: result.answer + citationsBlock + meta,
        // Structured fields downstream tools can inspect:
        answer: result.answer,
        citations: result.citations,
        usage: result.usage,
        model: result.model,
        // Hint to the agent: if webSearches=0, the model didn't
        // actually search. Surface this so the agent can decide to
        // retry with stronger wording.
        ...(searchCount === 0 ? { warning: 'Model did not invoke the web search tool. Try phrasing the query as "Search the web for: ..." to force tool use.' } : {}),
      };

    } catch (error) {
      // 503 = feature flag off / not provisioned. Distinct error path
      // so the agent can fall back to the `web` tool instead of
      // retrying.
      if (error.status === 503) {
        return {
          success: false,
          error: error.message,
          output: error.message + '\n\nFallback: use the `web` tool for puppeteer-based search instead.',
          notProvisioned: true,
        };
      }
      return {
        success: false,
        error: error.message,
        output: `Web search failed: ${error.message}`,
      };
    }
  }

  /**
   * Build an idempotency key per (agentId, query). Stable for repeated
   * identical queries inside the same turn — the backend's
   * Transaction.findOne dedup short-circuits the duplicate billing.
   *
   * Different turns or different agents → different keys → fresh
   * search + bill, which is correct (the world moves on between
   * turns, the user expects a fresh result).
   *
   * @private
   */
  _buildIdempotencyKey(context, query) {
    const agentId = context?.agentId || 'unknown';
    const turn = context?.turnId || context?.messageId || Date.now();
    // Cheap, deterministic, no-deps hash. Doesn't need to be
    // cryptographic — the keyspace is per-agent so collision risk
    // is minimal.
    let h = 0;
    const s = `${agentId}|${turn}|${query}`;
    for (let i = 0; i < s.length; i++) {
      h = ((h << 5) - h) + s.charCodeAt(i);
      h |= 0;
    }
    return `fws-${agentId}-${(h >>> 0).toString(36)}`;
  }
}

export default FoundryWebSearchTool;
