/*
 * priority-forge-hub — MCP tools
 *
 * Tool definitions + dispatch. The host (handler.ts) wires the integration
 * once via createMemoryHubIntegration({...}); this module looks up the
 * resulting handle via getCurrentIntegration() to access the reader and
 * trigger rederivation.
 */

import { DRYRUN_LOG_PATH } from './adapters/dryrun-adapter';
import {
  applyHubConfigureParams,
  collectHubStatusWarnings,
  isMemoryPersistenceActive,
  loadMemoryHubConfig,
  loadTeamHubConfig,
  TEAM_HUB_CONFIG_PATH,
  updateTeamHubConfig,
} from './config';
import { getMemoryHub, getOrchestrationHub, resetMemoryHubCache } from './factory';
import { getCurrentIntegration } from './integration';
import { MemoryHubBackend } from './types';

export interface McpTool {
  name: string;
  description: string;
  inputSchema: {
    type: 'object';
    properties: Record<string, unknown>;
    required?: string[];
  };
}

export const xmemTools: McpTool[] = [
  {
    name: 'hub_configure',
    description:
      'Configure team hub planes in ~/.priority-forge/team-hub.json: pulse (emit events), memory (backend), orchestration (shared tasks), salience (decay thresholds). Enabling memory with a real backend auto-enables pulse.emit.',
    inputSchema: {
      type: 'object',
      properties: {
        pulse: {
          type: 'object',
          description: '{ emit?, userId?, pushImmediately?, privacyRules? }',
        },
        memory: {
          type: 'object',
          description:
            '{ enabled?, backend?, teamId?, agentId?, syncIntervalMs?, xmem?, pgvector? }',
        },
        orchestration: {
          type: 'object',
          description: '{ enabled? } — requires memory backend pgvector or dryrun',
        },
        salience: {
          type: 'object',
          description:
            '{ decayTauDays?, interactionWindowDays?, minRetentionDays?, candidateEpsilon? }',
        },
      },
      required: [],
    },
  },
  {
    name: 'hub_status',
    description:
      'Report pulse, memory, orchestration planes, salience thresholds, config path, health, and warnings (noop edge cases).',
    inputSchema: { type: 'object', properties: {}, required: [] },
  },
  {
    name: 'xmem_enable_sync',
    description:
      'Enable the cross-team memory-hub integration. Backend=noop is the safe default; ' +
      'use "dryrun" to capture intended payloads to ~/.priority-forge/xmem-dryrun.jsonl ' +
      'without making network calls; use "xmem" to write to the live XMem API.',
    inputSchema: {
      type: 'object',
      properties: {
        backend: { type: 'string', enum: ['noop', 'dryrun', 'xmem', 'pgvector'], description: 'Adapter to use' },
        teamId: { type: 'string', description: 'Team scope (becomes user_id in XMem)' },
        agentId: { type: 'string', description: 'This forge owner identity (becomes agent_id in XMem)' },
        baseUrl: { type: 'string', description: '(xmem only) API base URL, e.g. https://api.xtrace.ai/v1' },
        apiKey: { type: 'string', description: '(xmem only) API key for the XMem deploy' },
        orgID: { type: 'string', description: '(xmem only) Organization ID — required alongside apiKey by the published XMem API' },
        environment: { type: 'string', enum: ['staging', 'production'], description: '(xmem only) Environment toggle; defaults to production' },
        connectionString: { type: 'string', description: '(pgvector only) Postgres connection string, e.g. postgres://user:pass@host:5432/db' },
        agentToken: { type: 'string', description: '(pgvector only) Per-agent secret token; verified against the stored SHA-256 hash before any write / claim / release / renewLease' },
        syncIntervalMs: { type: 'number', description: 'Polling interval for the read path (default 60000)' },
      },
      required: ['backend', 'teamId', 'agentId'],
    },
  },
  {
    name: 'xmem_disable_sync',
    description: 'Disable the memory-hub integration. Existing config is preserved; just sets enabled=false.',
    inputSchema: { type: 'object', properties: {}, required: [] },
  },
  {
    name: 'xmem_get_status',
    description: 'Report the active memory-hub backend, config path, dryrun log path, and a live health probe of the active adapter.',
    inputSchema: { type: 'object', properties: {}, required: [] },
  },
  {
    name: 'xmem_get_team_activity',
    description: 'Read recent cross-forge events from the active hub. Defaults to the configured teamId.',
    inputSchema: {
      type: 'object',
      properties: {
        teamId: { type: 'string', description: 'Team scope (default = configured teamId)' },
        sinceTs: { type: 'string', description: 'ISO timestamp; only events with timestamp >= this are returned' },
        agentIds: { type: 'array', items: { type: 'string' }, description: 'Filter to these agents only' },
        limit: { type: 'number', description: 'Maximum events to return (default 50)' },
      },
      required: [],
    },
  },
  {
    name: 'xmem_get_blocking_me',
    description: 'Return active cross-forge blockers where this forge owner is the BLOCKED agent.',
    inputSchema: {
      type: 'object',
      properties: {
        agentId: { type: 'string', description: 'Default = configured agentId' },
      },
      required: [],
    },
  },
  {
    name: 'xmem_get_i_am_blocking',
    description: 'Return active cross-forge blockers where this forge owner is the BLOCKING agent (work the team is waiting on).',
    inputSchema: {
      type: 'object',
      properties: {
        agentId: { type: 'string', description: 'Default = configured agentId' },
      },
      required: [],
    },
  },
  {
    name: 'xmem_search_team_memory',
    description: 'Semantic search across the team memory hub. With dryrun backend, falls back to substring scoring on the local jsonl.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Natural-language query' },
        teamId: { type: 'string', description: 'Default = configured teamId' },
        limit: { type: 'number', description: 'Default 20' },
      },
      required: ['query'],
    },
  },
  {
    name: 'xmem_force_rederive_blockers',
    description: 'Force an immediate re-derivation of cross-agent blocker edges. Useful for testing and after manual hub seeding.',
    inputSchema: { type: 'object', properties: {}, required: [] },
  },
  {
    name: 'xmem_reader_debug',
    description: 'Inspect the sync-reader state — running, last polled timestamp, cache + ownership sizes.',
    inputSchema: { type: 'object', properties: {}, required: [] },
  },
  {
    name: 'xmem_get_current_focus',
    description:
      'Snapshot of what each teammate is working on right now. Reduces task_selected / task_completed / context_switch events into a current-focus map. Returns { agents: { agentId: { currentTaskId, currentTaskTitle, project, startedAt, sharedTaskId? } | null } }.',
    inputSchema: {
      type: 'object',
      properties: {
        teamId: { type: 'string', description: 'Team scope (default = configured teamId)' },
        agentIds: { type: 'array', items: { type: 'string' }, description: 'Filter to these agents only (default = all)' },
      },
      required: [],
    },
  },
  {
    name: 'xmem_touch_memory',
    description: 'Refresh decay curve for a memory event (pgvector/dryrun). Records lastAccessedAt and increments accessCount.',
    inputSchema: {
      type: 'object',
      properties: {
        eventId: { type: 'string' },
        teamId: { type: 'string', description: 'Default = configured teamId' },
      },
      required: ['eventId'],
    },
  },
  {
    name: 'xmem_list_decay_candidates',
    description:
      'List memory events below salience candidateEpsilon (after minRetentionDays). Excludes pinned and tombstoned.',
    inputSchema: {
      type: 'object',
      properties: {
        teamId: { type: 'string' },
      },
      required: [],
    },
  },
  {
    name: 'xmem_set_memory_flags',
    description: 'Set metadata flags on a memory event (pinned, clear tombstone candidate). pgvector/dryrun only.',
    inputSchema: {
      type: 'object',
      properties: {
        eventId: { type: 'string' },
        teamId: { type: 'string' },
        pinned: { type: 'boolean' },
        clearTombstoneCandidate: { type: 'boolean' },
      },
      required: ['eventId'],
    },
  },
  {
    name: 'xmem_archive_memory',
    description:
      'Soft-delete (tombstone) a memory event. pgvector/dryrun implement; xmem returns unsupported_backend until XTrace delete API ships.',
    inputSchema: {
      type: 'object',
      properties: {
        eventId: { type: 'string' },
        teamId: { type: 'string' },
      },
      required: ['eventId'],
    },
  },
  {
    name: 'xmem_register_agent',
    description:
      'Bootstrap a per-agent token in the hardening table (pgvector backend only). After registration, the pgvector adapter verifies the configured agentToken against the stored SHA-256 hash before any write / claim / release / renewLease. Idempotent — re-registering rotates the token.',
    inputSchema: {
      type: 'object',
      properties: {
        teamId: { type: 'string', description: 'Team scope (default = configured teamId)' },
        agentId: { type: 'string', description: 'Agent identity to register' },
        token: { type: 'string', description: 'Secret token (recommend 32+ random bytes). Stored as SHA-256 hash.' },
      },
      required: ['agentId', 'token'],
    },
  },
];

function asString(v: unknown, fallback = ''): string {
  return typeof v === 'string' ? v : fallback;
}

function asStringArray(v: unknown): string[] | undefined {
  if (!Array.isArray(v)) return undefined;
  const out = v.filter((x): x is string => typeof x === 'string');
  return out.length ? out : undefined;
}

function asNumber(v: unknown, fallback?: number): number | undefined {
  return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
}

function asBackend(v: unknown): MemoryHubBackend {
  if (v === 'noop' || v === 'dryrun' || v === 'xmem' || v === 'pgvector') return v;
  throw new Error(`invalid backend "${String(v)}" — expected noop | dryrun | xmem | pgvector`);
}

function restartHubReader(): void {
  resetMemoryHubCache();
  const integration = getCurrentIntegration();
  if (integration) {
    integration.reader.stop();
    if (isMemoryPersistenceActive()) integration.reader.start();
  }
}

export async function dispatchXmemTool(
  name: string,
  params: Record<string, unknown>,
): Promise<unknown> {
  switch (name) {
    case 'hub_configure': {
      const hub = applyHubConfigureParams(params);
      restartHubReader();
      const health = await getMemoryHub().health();
      const orch = getOrchestrationHub();
      return {
        ok: true,
        configPath: TEAM_HUB_CONFIG_PATH,
        pulse: hub.pulse,
        memory: {
          enabled: hub.memory.enabled,
          backend: hub.memory.backend,
          teamId: hub.memory.teamId,
          agentId: hub.memory.agentId,
        },
        orchestration: {
          enabled: hub.orchestration.enabled,
          supportsOrchestration: orch.supportsOrchestration,
        },
        salience: hub.salience,
        warnings: collectHubStatusWarnings(hub),
        health,
      };
    }
    case 'hub_status': {
      const hub = loadTeamHubConfig();
      const memHub = getMemoryHub();
      const orch = getOrchestrationHub();
      const health = await memHub.health();
      const cfg = hub.memory;
      const xmemApiKeySet = cfg.xmem.apiKey.length > 0;
      const xmemOrgIdSet = cfg.xmem.orgID.length > 0;
      const pgvectorConnectionStringSet = cfg.pgvector.connectionString.length > 0;
      const pgvectorAgentTokenSet = (cfg.pgvector.agentToken ?? '').length > 0;
      return {
        configPath: TEAM_HUB_CONFIG_PATH,
        pulse: hub.pulse,
        memory: {
          enabled: cfg.enabled,
          backend: memHub.backend,
          teamId: cfg.teamId,
          agentId: cfg.agentId,
        },
        orchestration: {
          enabled: hub.orchestration.enabled,
          supportsOrchestration: orch.supportsOrchestration,
        },
        salience: hub.salience,
        dryrunLogPath: DRYRUN_LOG_PATH,
        xmemCredsComplete: xmemApiKeySet && xmemOrgIdSet,
        pgvectorCredsComplete: pgvectorConnectionStringSet && pgvectorAgentTokenSet,
        forceNoopEnv: process.env.XMEM_FORCE_NOOP === '1',
        warnings: collectHubStatusWarnings(hub),
        health,
      };
    }
    case 'xmem_enable_sync': {
      const backend = asBackend(params.backend);
      const teamId = asString(params.teamId);
      const agentId = asString(params.agentId);
      if (!teamId || !agentId) throw new Error('teamId and agentId are required');
      const envRaw = asString(params.environment, '');
      const orchEnabled =
        params.orchestration === true ||
        backend === 'pgvector' ||
        backend === 'dryrun';
      const hub = applyHubConfigureParams({
        memory: {
          enabled: true,
          backend,
          teamId,
          agentId,
          syncIntervalMs: asNumber(params.syncIntervalMs, 60000),
          xmem: {
            baseUrl: asString(params.baseUrl, 'https://api.xtrace.ai/v1'),
            apiKey: asString(params.apiKey, ''),
            orgID: asString(params.orgID, ''),
            ...(envRaw === 'staging' || envRaw === 'production'
              ? { environment: envRaw }
              : {}),
          },
          pgvector: {
            connectionString: asString(params.connectionString, ''),
            ...(asString(params.agentToken, '')
              ? { agentToken: asString(params.agentToken) }
              : {}),
          },
        },
        pulse: { emit: true, userId: agentId },
        orchestration: { enabled: orchEnabled },
      });
      restartHubReader();
      const health = await getMemoryHub().health();
      return {
        ok: true,
        message: `memory-hub enabled (backend=${hub.memory.backend}, teamId=${hub.memory.teamId}, agentId=${hub.memory.agentId})`,
        configPath: TEAM_HUB_CONFIG_PATH,
        dryrunLogPath: DRYRUN_LOG_PATH,
        readerRunning: getCurrentIntegration()?.reader.isRunning() ?? false,
        warnings: collectHubStatusWarnings(hub),
        health,
      };
    }
    case 'xmem_disable_sync': {
      const hub = updateTeamHubConfig({
        memory: { ...loadTeamHubConfig().memory, enabled: false },
        orchestration: { enabled: false },
      });
      restartHubReader();
      getCurrentIntegration()?.reader.stop();
      return {
        ok: true,
        message: 'memory-hub disabled',
        backend: hub.memory.backend,
        teamId: hub.memory.teamId,
      };
    }
    case 'xmem_get_status': {
      return dispatchXmemTool('hub_status', params);
    }
    case 'xmem_get_team_activity': {
      const cfg = loadMemoryHubConfig();
      const hub = getMemoryHub();
      const teamId = asString(params.teamId, cfg.teamId);
      if (!teamId) throw new Error('teamId is required (configure via xmem_enable_sync first)');
      const events = await hub.read({
        teamId,
        sinceTs: typeof params.sinceTs === 'string' ? params.sinceTs : undefined,
        agentIds: asStringArray(params.agentIds),
        limit: asNumber(params.limit, 50),
      });
      return { teamId, count: events.length, events };
    }
    case 'xmem_get_blocking_me': {
      const cfg = loadMemoryHubConfig();
      const hub = getMemoryHub();
      const agentId = asString(params.agentId, cfg.agentId);
      if (!cfg.teamId || !agentId) throw new Error('teamId and agentId required');
      const blockers = await hub.blockersFor(cfg.teamId, agentId);
      return { teamId: cfg.teamId, agentId, count: blockers.length, blockers };
    }
    case 'xmem_get_i_am_blocking': {
      const cfg = loadMemoryHubConfig();
      const hub = getMemoryHub();
      const agentId = asString(params.agentId, cfg.agentId);
      if (!cfg.teamId || !agentId) throw new Error('teamId and agentId required');
      const events = await hub.read({ teamId: cfg.teamId, eventTypes: ['derived_blocker'] });
      const seen = new Map<string, unknown>();
      for (const ev of events) {
        const edge = ev.payload as {
          blockedTaskId?: string;
          blockingTaskId?: string;
          blockingAgentId?: string;
          status?: string;
        };
        if (
          typeof edge.blockedTaskId !== 'string' ||
          typeof edge.blockingTaskId !== 'string' ||
          edge.blockingAgentId !== agentId
        ) continue;
        const key = `${edge.blockedTaskId}::${edge.blockingTaskId}`;
        if (!seen.has(key)) seen.set(key, { ...edge, observedAt: ev.timestamp });
      }
      const active = [...seen.values()].filter((e) => (e as { status?: string }).status === 'active');
      return { teamId: cfg.teamId, agentId, count: active.length, blockers: active };
    }
    case 'xmem_search_team_memory': {
      const cfg = loadMemoryHubConfig();
      const hub = getMemoryHub();
      const teamId = asString(params.teamId, cfg.teamId);
      const query = asString(params.query);
      if (!teamId) throw new Error('teamId required');
      if (!query) throw new Error('query required');
      const results = await hub.search(teamId, query, asNumber(params.limit, 20));
      return { teamId, query, count: results.length, results };
    }
    case 'xmem_force_rederive_blockers': {
      const integration = getCurrentIntegration();
      if (!integration) {
        return { ok: false, message: 'no active integration — call createMemoryHubIntegration first' };
      }
      // Make sure the reader has the freshest cross-forge state before deriving.
      if (integration.reader.isRunning()) await integration.reader.poll();
      await integration.rederive();
      return { ok: true, message: 'rederivation complete' };
    }
    case 'xmem_reader_debug': {
      const integration = getCurrentIntegration();
      return (
        integration?.reader.getDebugSnapshot() ?? {
          running: false,
          teamId: null,
          agentId: null,
          lastSeenTs: null,
          cacheSize: 0,
          ownershipSize: 0,
          statusSize: 0,
        }
      );
    }
    case 'xmem_get_current_focus': {
      const cfg = loadMemoryHubConfig();
      const hub = getMemoryHub();
      const teamId = asString(params.teamId, cfg.teamId);
      if (!teamId) throw new Error('teamId is required (configure via xmem_enable_sync first)');
      const filterAgentIds = asStringArray(params.agentIds);
      // Pull lifecycle events. 500 is enough to cover ~1 day of normal team
      // activity; tune higher for very active teams.
      const events = await hub.read({
        teamId,
        eventTypes: ['task_selected', 'task_completed', 'context_switch'],
        agentIds: filterAgentIds,
        limit: 500,
      });
      // Reduce oldest → newest so the final entry per agent reflects their
      // current focus.
      const asc = [...events].reverse();
      interface FocusEntry {
        currentTaskId: string;
        currentTaskTitle: string;
        project: string;
        startedAt: string;
        sharedTaskId?: string;
      }
      const focusByAgent = new Map<string, FocusEntry | null>();
      for (const ev of asc) {
        const payload = ev.payload as {
          taskId?: string;
          task?: string;
          project?: string;
          startedAt?: string;
          sharedTaskId?: string;
          fromTaskId?: string;
        };
        if (ev.eventType === 'task_selected') {
          if (!payload.taskId) continue;
          focusByAgent.set(ev.agentId, {
            currentTaskId: payload.taskId,
            currentTaskTitle: payload.task ?? '',
            project: payload.project ?? '',
            startedAt: payload.startedAt ?? ev.timestamp,
            sharedTaskId: payload.sharedTaskId,
          });
        } else if (ev.eventType === 'task_completed') {
          const current = focusByAgent.get(ev.agentId);
          if (current && current.currentTaskId === payload.taskId) {
            focusByAgent.set(ev.agentId, null);
          }
        } else if (ev.eventType === 'context_switch') {
          const current = focusByAgent.get(ev.agentId);
          if (current && current.currentTaskId === payload.fromTaskId) {
            focusByAgent.set(ev.agentId, null);
          }
        }
      }
      // Only include agents that have a CURRENT focus. Agents whose
      // most-recent transition was a task_completed/context_switch fall
      // out of the snapshot — "who is doing what RIGHT NOW" should not
      // include "nobody, anymore." Idle agents are simply absent from
      // the map; their absence IS the signal. `idleAgents` is surfaced
      // separately for callers who want to know "team members the hub
      // has observed but who are idle" (vs "never seen at all").
      const out: Record<string, FocusEntry> = {};
      const idleAgents: string[] = [];
      for (const [agent, focus] of focusByAgent) {
        if (focus) {
          out[agent] = focus;
        } else {
          idleAgents.push(agent);
        }
      }
      return {
        teamId,
        agents: out,
        idleAgents,
        count: Object.keys(out).length,
      };
    }
    case 'xmem_touch_memory':
    case 'xmem_list_decay_candidates':
    case 'xmem_set_memory_flags':
    case 'xmem_archive_memory': {
      const cfg = loadMemoryHubConfig();
      if (!cfg.enabled) throw new Error('memory hub disabled — hub_configure first');
      const hub = getMemoryHub() as {
        touchMemory?: (e: string, t: string) => Promise<{ ok: boolean }>;
        listDecayCandidates?: (t: string) => Promise<import('./types').TeamMemoryEvent[]>;
        setMemoryFlags?: (
          e: string,
          t: string,
          f: { pinned?: boolean; clearTombstoneCandidate?: boolean },
        ) => Promise<{ ok: boolean }>;
        archiveMemory?: (e: string, t: string) => Promise<{ ok: boolean }>;
      };
      const teamId = asString(params.teamId, cfg.teamId);
      if (!teamId) throw new Error('teamId required');
      if (name === 'xmem_list_decay_candidates') {
        if (typeof hub.listDecayCandidates !== 'function') {
          throw new Error(
            `backend ${loadMemoryHubConfig().backend} does not support decay candidates`,
          );
        }
        const candidates = await hub.listDecayCandidates(teamId);
        return { teamId, count: candidates.length, candidates };
      }
      const eventId = asString(params.eventId);
      if (!eventId) throw new Error('eventId required');
      if (name === 'xmem_touch_memory') {
        if (typeof hub.touchMemory !== 'function') throw new Error('touch not supported on this backend');
        return { ...(await hub.touchMemory(eventId, teamId)), eventId, teamId };
      }
      if (name === 'xmem_set_memory_flags') {
        if (typeof hub.setMemoryFlags !== 'function') throw new Error('setMemoryFlags not supported');
        return {
          ...(await hub.setMemoryFlags(eventId, teamId, {
            pinned: params.pinned === true ? true : params.pinned === false ? false : undefined,
            clearTombstoneCandidate: params.clearTombstoneCandidate === true,
          })),
          eventId,
          teamId,
        };
      }
      if (name === 'xmem_archive_memory') {
        if (loadMemoryHubConfig().backend === 'xmem') {
          return { ok: false, reason: 'unsupported_backend', detail: 'XTrace delete API not integrated' };
        }
        if (typeof hub.archiveMemory !== 'function') throw new Error('archive not supported');
        return { ...(await hub.archiveMemory(eventId, teamId)), eventId, teamId };
      }
      throw new Error(`unhandled memory tool branch: ${name}`);
    }
    case 'xmem_register_agent': {
      const cfg = loadMemoryHubConfig();
      if (cfg.backend !== 'pgvector') {
        throw new Error(
          `xmem_register_agent requires backend=pgvector (current: ${cfg.backend}). Switch backends via xmem_enable_sync.`,
        );
      }
      const teamId = asString(params.teamId, cfg.teamId);
      const agentId = asString(params.agentId);
      const token = asString(params.token);
      if (!teamId) throw new Error('teamId is required (configure via xmem_enable_sync or pass explicitly)');
      if (!agentId || !token) throw new Error('agentId and token are required');
      // Duck-type cast: only the pgvector adapter implements registerAgent.
      // Other adapters never reach this branch (gated by backend check above).
      const hub = getMemoryHub() as unknown as {
        registerAgent?: (
          t: string,
          a: string,
          tk: string,
        ) => Promise<{ ok: boolean; reason?: string }>;
      };
      if (typeof hub.registerAgent !== 'function') {
        throw new Error('active backend does not expose registerAgent — refresh xmem_enable_sync');
      }
      const result = await hub.registerAgent(teamId, agentId, token);
      if (!result.ok) {
        throw new Error(`agent registration failed: ${result.reason ?? 'unknown error'}`);
      }
      return {
        ok: true,
        message: `agent "${agentId}" registered for team "${teamId}". Subsequent writes / claims by this agent must present the matching token via xmem_enable_sync(agentToken).`,
        teamId,
        agentId,
      };
    }
    default:
      throw new Error(`unknown xmem tool: ${name}`);
  }
}
