/**
 * AgentIndexer — keeps a GLOBAL vector index of all agents so other
 * agents (and the scheduler / UI) can answer "which agent should
 * handle this task?" by meaning instead of guessing from a flat list.
 *
 * The discovery problem this solves:
 *   - Today's `agentcommunication.get-available-agents` returns a flat
 *     list of N agents. With 10+ agents on a team, a delegating agent
 *     has no good way to pick — it has to read every name + description
 *     and reason about them.
 *   - After this indexer is in place: `find-agent({query: "good at SQL
 *     migrations"})` returns the top 5 candidates ranked by semantic
 *     fit, including their system prompts so the caller can sanity-check.
 *
 * Why GLOBAL scope:
 *   - The agent roster is the same for every requestor (filtering
 *     for "active", "not self", etc. is the caller's job, not the
 *     index's).
 *   - One global file avoids N² storage as teams grow.
 *
 * Re-indexing policy:
 *   - The agent record changes infrequently (only on create / update /
 *     delete). Rather than indexing one-at-a-time from every CRUD path
 *     (multiple entry points: platformcontrol, agentPool, UI), the tool
 *     exposes a `backfill-agents-index` action that walks the live
 *     listActiveAgents() snapshot. Run it after meaningful agent
 *     changes (the agent CRUD code paths can call it explicitly, or
 *     the user can trigger it from the UI).
 *   - indexOne / deleteOne stay available for callers that DO have a
 *     clean hook point.
 *
 * Failure policy: same best-effort contract as the other indexers.
 */

/**
 * Pack an agent record into the text the embedding model sees.
 *
 * Pack order:
 *   1. role/type tag    → most concise signal
 *   2. name             → human-readable identifier
 *   3. systemPrompt     → most informative, truncated for sane embedding
 *   4. capabilities     → tool affordances (e.g. "filesystem, terminal, web")
 *   5. skills           → declared expertise
 *
 * systemPrompt is capped at ~2000 chars: agent role/intent typically
 * lives in the first paragraph, and beyond that we get task-specific
 * instructions that wash out the embedding's role signal.
 */
export function agentToText(agent, { maxPromptChars = 2000 } = {}) {
  if (!agent) return '';
  const parts = [];
  if (agent.type) parts.push(`[${agent.type}]`);
  if (agent.name) parts.push(String(agent.name));
  const prompt = String(agent.systemPrompt || agent.originalSystemPrompt || '');
  if (prompt) {
    parts.push(prompt.length > maxPromptChars ? prompt.slice(0, maxPromptChars) : prompt);
  }
  if (Array.isArray(agent.capabilities) && agent.capabilities.length > 0) {
    parts.push(`capabilities: ${agent.capabilities.join(', ')}`);
  }
  if (Array.isArray(agent.skills) && agent.skills.length > 0) {
    parts.push(`skills: ${agent.skills.join(', ')}`);
  }
  return parts.join('\n');
}

export class AgentIndexer {
  /**
   * @param {object} opts
   * @param {import('../embeddingService.js').EmbeddingService} opts.embeddingService
   * @param {object} [opts.logger]
   * @param {number} [opts.maxPromptChars]
   */
  constructor({ embeddingService, logger = null, maxPromptChars = 2000 }) {
    this._svc = embeddingService;
    this._logger = logger;
    this._maxPromptChars = maxPromptChars;
  }

  get isEnabled() {
    return !!(this._svc && this._svc.isEnabled);
  }

  /**
   * Index a single agent. Best-effort.
   * @returns {Promise<{indexed: boolean, reason?: string, error?: string}>}
   */
  async indexOne(agent) {
    if (!this.isEnabled) return { indexed: false, reason: 'disabled' };
    if (!agent?.id) return { indexed: false, reason: 'invalid-input' };
    const text = agentToText(agent, { maxPromptChars: this._maxPromptChars });
    if (!text) return { indexed: false, reason: 'empty-text' };

    try {
      const [vec] = await this._svc.embedDocuments([text], 'document');
      const store = await this._svc.getStore({ surface: 'agents' });
      await store.upsert(agent.id, vec, this._buildMetadata(agent, text));
      return { indexed: true };
    } catch (err) {
      this._logger?.warn?.('[agentIndexer] indexOne failed', { agentId: agent?.id, error: err?.message });
      return { indexed: false, reason: 'error', error: err?.message };
    }
  }

  /**
   * Remove an agent from the index. Best-effort.
   */
  async deleteOne(agentId) {
    if (!this.isEnabled) return { deleted: false, reason: 'disabled' };
    if (!agentId) return { deleted: false, reason: 'invalid-input' };
    try {
      const store = await this._svc.getStore({ surface: 'agents' });
      await store.delete(agentId);
      return { deleted: true };
    } catch (err) {
      this._logger?.warn?.('[agentIndexer] deleteOne failed', { agentId, error: err?.message });
      return { deleted: false, reason: 'error', error: err?.message };
    }
  }

  /**
   * Bulk index. Full-rebuild semantics: drops the prior store first.
   */
  async backfill(agents) {
    if (!this.isEnabled) return { ok: false, reason: 'disabled', indexed: 0, skipped: 0, failed: 0 };
    if (!Array.isArray(agents)) {
      return { ok: false, reason: 'invalid-input', indexed: 0, skipped: 0, failed: 0 };
    }

    let skipped = 0;
    const valid = [];
    for (const a of agents) {
      if (!a?.id) { skipped++; continue; }
      const text = agentToText(a, { maxPromptChars: this._maxPromptChars });
      if (!text) { skipped++; continue; }
      valid.push({ agent: a, text });
    }

    const store = await this._svc.getStore({ surface: 'agents' });
    try { await store.drop(); }
    catch (err) {
      this._logger?.warn?.('[agentIndexer] backfill drop failed', { error: err?.message });
    }

    if (valid.length === 0) {
      return { ok: true, indexed: 0, skipped, failed: 0 };
    }

    let vectors;
    try {
      vectors = await this._svc.embedDocuments(valid.map(v => v.text), 'document');
    } catch (err) {
      this._logger?.warn?.('[agentIndexer] backfill embed failed', {
        count: valid.length, error: err?.message,
      });
      return { ok: false, indexed: 0, skipped, failed: valid.length, error: err?.message };
    }

    const rows = valid.map((v, i) => ({
      id: v.agent.id,
      vector: vectors[i],
      metadata: this._buildMetadata(v.agent, v.text),
    }));

    try {
      await store.upsertBatch(rows);
      return { ok: true, indexed: rows.length, skipped, failed: 0 };
    } catch (err) {
      this._logger?.warn?.('[agentIndexer] backfill upsert failed', {
        count: rows.length, error: err?.message,
      });
      return { ok: false, indexed: 0, skipped, failed: rows.length, error: err?.message };
    }
  }

  /**
   * Search agents by query. Filters out paused / specific IDs (e.g. self)
   * via the optional `excludeIds` set.
   *
   * @param {string} query
   * @param {object} [opts]
   * @param {number} [opts.topK=5]
   * @param {boolean} [opts.hybrid=true]
   * @param {string[]} [opts.excludeIds] - Agent IDs to filter out (e.g. requester's own).
   * @returns {Promise<{enabled: boolean, hits: Array, error?: string}>}
   */
  async search(query, { topK = 5, hybrid = true, excludeIds = [] } = {}) {
    if (!this.isEnabled) return { enabled: false, hits: [] };
    if (typeof query !== 'string' || !query.trim()) {
      return { enabled: true, hits: [] };
    }
    try {
      const store = await this._svc.getStore({ surface: 'agents' });
      const queryVec = await this._svc.embedQuery(query);
      const excludeSet = new Set(excludeIds);
      const filter = excludeSet.size > 0
        ? (m) => !excludeSet.has(m?.agentId)
        : null;
      const raw = hybrid
        ? await store.hybridQuery(queryVec, query, { topK, filter })
        : await store.query(queryVec, { topK, filter });

      const hits = raw.map(r => ({
        id: r.id,
        score: r.score,
        agentId: r.metadata?.agentId || r.id,
        name: r.metadata?.name || '',
        type: r.metadata?.type || '',
        capabilities: r.metadata?.capabilities || [],
        skills: r.metadata?.skills || [],
        promptSnippet: r.metadata?.promptSnippet || '',
        metadata: r.metadata,
      }));
      return { enabled: true, hits };
    } catch (err) {
      this._logger?.warn?.('[agentIndexer] search failed', { error: err?.message });
      return { enabled: true, hits: [], error: err?.message };
    }
  }

  /** Read-only stats. */
  async stats() {
    if (!this.isEnabled) return { count: 0, enabled: false };
    try {
      const store = await this._svc.getStore({ surface: 'agents' });
      const s = await store.stats();
      return { ...s, enabled: true };
    } catch (err) {
      return { count: 0, enabled: true, error: err?.message };
    }
  }

  /** @private */
  _buildMetadata(agent, text) {
    return {
      agentId: agent.id,
      name: agent.name || '',
      type: agent.type || '',
      capabilities: Array.isArray(agent.capabilities) ? agent.capabilities : [],
      skills: Array.isArray(agent.skills) ? agent.skills : [],
      // First ~500 chars of the (already-truncated-for-embedding) prompt
      // — purely for hit display, not used for ranking.
      promptSnippet: text.length > 500 ? text.slice(0, 500) + '…' : text,
      // Full embed text for the hybrid substring path.
      text,
      indexedAt: new Date().toISOString(),
    };
  }
}
