import { type Application, type Request, type Response, type RequestHandler } from 'express';
import type { ToolDef } from '../llm/openai-compat.js';
import { getSshSubsystem } from '../engine/tools/ssh.js';

/**
 * Tool catalog entry exposed by GET /api/tools.
 *
 * The catalog is built at request-time from the same module set the agent loop
 * uses, plus per-caller MCP context. UI consumers (Piece allowed_tools editor)
 * should rely on this rather than the previous hand-maintained static list.
 *
 * See docs/superpowers/specs/2026-05-21-settings-ui-and-config-restructure-design.md
 * step 4 for the design rationale.
 */
export interface ToolCatalogEntry {
  name: string;
  source: 'builtin' | 'meta' | 'mcp';
  /**
   * Coarse grouping for UI. Values are stable strings derived from the source
   * module file name (e.g. 'core', 'web', 'office'). MCP tools use
   * `mcp:<serverId>` so the UI can group them by server.
   */
  category: string;
  /** MCP server id (source: 'mcp' only). */
  serverId?: string;
  /** Whether the tool can be invoked at this moment. */
  available: boolean;
  /** Human-readable explanation when available=false. */
  reason?: string;
  /**
   * Where the tool is "switched on":
   * - 'global' → always injected (meta tools like ReadToolDoc)
   * - 'piece'  → must appear in a piece's `allowed_tools`
   * - 'user'   → per-user resource (MCP)
   */
  scope: 'global' | 'piece' | 'user';
}

export interface ToolCatalogResponse {
  tools: ToolCatalogEntry[];
}

/**
 * Meta tools are auto-injected by `agent-loop.buildSystemPrompt()` regardless of
 * a piece's `allowed_tools`. Keep this list in sync with `META_TOOLS` in
 * `src/engine/tools/index.ts`.
 */
const META_TOOLS = new Set<string>([
  'ReadToolDoc',
  'CreateChecklist',
  'CheckItem',
  'GetChecklist',
  'MissionUpdate',
  'ListUserAssets',
  'RunUserScript',
  'UpdateUserMemory',
  'ReadUserMemory',
  'WriteUserScript',
  'Brainstorm',
  'ReadAppDoc',
  'ListAppDocs',
  'GetMyOrchestratorState',
  'ReadSkill',
  'ListSkills',
  'InstallSkill',
]);

/**
 * Modules to load. The key becomes the `category` field for builtin tools.
 * `core` is loaded separately because its export name is `ALL_TOOL_DEFS`.
 *
 * Categories that map to "user-scoped" assets (per-user MCP servers, SSH) get
 * scope='user' below — see categoryScope().
 */
const MODULE_SPECS: Array<{ category: string; specifier: string }> = [
  { category: 'web', specifier: '../engine/tools/web.js' },
  { category: 'image', specifier: '../engine/tools/image.js' },
  { category: 'data', specifier: '../engine/tools/data.js' },
  { category: 'office', specifier: '../engine/tools/office.js' },
  { category: 'review', specifier: '../engine/tools/review.js' },
  { category: 'x', specifier: '../engine/tools/x.js' },
  { category: 'orchestration', specifier: '../engine/tools/orchestration.js' },
  { category: 'browser', specifier: '../engine/tools/browser.js' },
  { category: 'maps', specifier: '../engine/tools/maps.js' },
  { category: 'youtube', specifier: '../engine/tools/youtube.js' },
  { category: 'pieces', specifier: '../engine/tools/pieces.js' },
  { category: 'amazon', specifier: '../engine/tools/amazon.js' },
  { category: 'speech', specifier: '../engine/tools/speech.js' },
  { category: 'checklist', specifier: '../engine/tools/checklist.js' },
  { category: 'knowledge', specifier: '../engine/tools/knowledge.js' },
  { category: 'ms-learn', specifier: '../engine/tools/ms-learn.js' },
  { category: 'slide', specifier: '../engine/tools/slide.js' },
  { category: 'docs', specifier: '../engine/tools/docs.js' },
  { category: 'mission', specifier: '../engine/tools/mission.js' },
  { category: 'user-folder', specifier: '../engine/tools/user-folder.js' },
  { category: 'brainstorm', specifier: '../engine/tools/brainstorm.js' },
  { category: 'app-docs', specifier: '../engine/tools/app-docs.js' },
  { category: 'ssh', specifier: '../engine/tools/ssh.js' },
  { category: 'ssh', specifier: '../engine/tools/ssh-console.js' },
  { category: 'notes', specifier: '../engine/tools/notes.js' },
  { category: 'dashboard', specifier: '../engine/tools/dashboard.js' },
  { category: 'skills', specifier: '../engine/tools/skills.js' },
];

interface ToolModule {
  TOOL_DEFS?: Record<string, ToolDef>;
  ALL_TOOL_DEFS?: Record<string, ToolDef>;
}

/**
 * Deps the catalog needs to enumerate per-user MCP tools. Optional — if
 * absent, MCP tools are omitted entirely (e.g. when MCP_ENCRYPTION_KEY is
 * not configured and the aggregator was never set up).
 */
export interface McpCatalogDeps {
  registry: {
    listEnabledForUser(userId: string): Array<{ id: string; name: string; enabled: boolean }>;
  };
  tokenManager: {
    hasToken(userId: string, serverId: string): boolean;
  };
  toolCache: {
    getAllForServers(serverIds: string[]): Array<{ serverId: string; toolName: string }>;
  };
}

export interface MountToolsApiOptions {
  /** When true, /api/tools is gated behind requireAuth. */
  authActive?: boolean;
  /** requireAuth middleware (only consulted when authActive is true). */
  requireAuth?: RequestHandler;
  /** Subsystems used to enumerate per-user MCP tools. */
  mcp?: McpCatalogDeps | null;
}

// ──────────────────────────────────────────────────────────────────────
// Module / category caches
// ──────────────────────────────────────────────────────────────────────

let _cachedBuiltinEntries: Array<Omit<ToolCatalogEntry, 'available' | 'reason'>> | null = null;

async function loadBuiltinEntries(): Promise<Array<Omit<ToolCatalogEntry, 'available' | 'reason'>>> {
  if (_cachedBuiltinEntries) return _cachedBuiltinEntries;

  // name → category info. First-write-wins so a later module specifying the
  // same tool name keeps the original category — matches runtime tools/index.ts.
  const seen = new Map<string, { category: string; source: 'builtin' | 'meta' }>();

  // Core tools (Read/Write/Edit/Bash/Glob/Grep) — always categorised 'core'.
  try {
    const coreMod = (await import('../engine/tools/core.js')) as ToolModule;
    const defs = coreMod.ALL_TOOL_DEFS ?? {};
    for (const name of Object.keys(defs)) {
      if (!seen.has(name)) {
        seen.set(name, { category: 'core', source: META_TOOLS.has(name) ? 'meta' : 'builtin' });
      }
    }
  } catch {
    // core should always load; if not, we have bigger problems
  }

  for (const { category, specifier } of MODULE_SPECS) {
    try {
      const mod = (await import(specifier)) as ToolModule;
      const defs = mod.TOOL_DEFS ?? {};
      for (const name of Object.keys(defs)) {
        if (!seen.has(name)) {
          seen.set(name, {
            category,
            source: META_TOOLS.has(name) ? 'meta' : 'builtin',
          });
        }
      }
    } catch {
      // module not available — skip
    }
  }

  const entries: Array<Omit<ToolCatalogEntry, 'available' | 'reason'>> = [];
  for (const [name, info] of seen) {
    entries.push({
      name,
      source: info.source,
      category: info.category,
      scope: info.source === 'meta' ? 'global' : 'piece',
    });
  }
  entries.sort((a, b) => a.name.localeCompare(b.name));
  _cachedBuiltinEntries = entries;
  return entries;
}

/** Test-only: reset the module-load cache. */
export function _resetToolCatalogCacheForTests(): void {
  _cachedBuiltinEntries = null;
}

// ──────────────────────────────────────────────────────────────────────
// SSH availability
// ──────────────────────────────────────────────────────────────────────

function annotateSshAvailability(entry: Omit<ToolCatalogEntry, 'available' | 'reason'>): ToolCatalogEntry {
  if (entry.category !== 'ssh') {
    return { ...entry, available: true };
  }
  const sub = getSshSubsystem();
  if (sub) {
    return { ...entry, available: true };
  }
  return {
    ...entry,
    available: false,
    reason: 'SSH subsystem not initialised',
  };
}

// ──────────────────────────────────────────────────────────────────────
// MCP catalog
// ──────────────────────────────────────────────────────────────────────

function buildMcpEntries(userId: string, mcp: McpCatalogDeps): ToolCatalogEntry[] {
  const servers = mcp.registry.listEnabledForUser(userId);
  if (servers.length === 0) return [];

  const cache = mcp.toolCache.getAllForServers(servers.map((s) => s.id));
  const entries: ToolCatalogEntry[] = [];

  for (const server of servers) {
    const connected = mcp.tokenManager.hasToken(userId, server.id);
    const serverTools = cache.filter((t) => t.serverId === server.id);

    if (serverTools.length === 0) {
      // No cached tools yet — surface the server as a single placeholder so
      // the UI can still show it (e.g. "<server> — not yet connected").
      entries.push({
        name: `mcp__${server.id}__`,
        source: 'mcp',
        category: `mcp:${server.id}`,
        serverId: server.id,
        available: false,
        reason: connected
          ? `mcp server ${server.name} has no cached tools`
          : `mcp server ${server.name} offline`,
        scope: 'user',
      });
      continue;
    }

    for (const t of serverTools) {
      entries.push({
        name: `mcp__${server.id}__${t.toolName}`,
        source: 'mcp',
        category: `mcp:${server.id}`,
        serverId: server.id,
        available: connected,
        reason: connected ? undefined : `mcp server ${server.name} offline`,
        scope: 'user',
      });
    }
  }

  entries.sort((a, b) => a.name.localeCompare(b.name));
  return entries;
}

// ──────────────────────────────────────────────────────────────────────
// Handler
// ──────────────────────────────────────────────────────────────────────

export function mountToolsApi(app: Application, options: MountToolsApiOptions = {}): void {
  const handler = async (req: Request, res: Response): Promise<void> => {
    const builtinBase = await loadBuiltinEntries();
    const builtin = builtinBase.map(annotateSshAvailability);

    // MCP entries require an authenticated caller. When auth is disabled the
    // request still carries no user; we treat that case as "no MCP catalog"
    // because MCP is inherently per-user.
    const user = (req.user as { id?: string } | undefined) ?? null;
    let mcpEntries: ToolCatalogEntry[] = [];
    if (options.mcp && user?.id) {
      try {
        mcpEntries = buildMcpEntries(user.id, options.mcp);
      } catch {
        // Defensive: never let MCP enumeration crash the whole catalog.
        mcpEntries = [];
      }
    }

    const all: ToolCatalogEntry[] = [...builtin, ...mcpEntries];

    // Legacy shape: flat array of names. Maintained so the existing
    // ui/src/api.ts fetchTools() (and any external consumer treating
    // response.tools as string[]) keeps working until step 5.
    if (req.query.legacy === '1') {
      res.json({ tools: all.map((t) => t.name) });
      return;
    }

    const payload: ToolCatalogResponse = { tools: all };
    res.json(payload);
  };

  const guards: RequestHandler[] = [];
  if (options.authActive && options.requireAuth) {
    guards.push(options.requireAuth);
  }
  app.get('/api/tools', ...guards, handler);
}
