/**
 * MCP Tool Server
 *
 * Standalone stdio MCP server that dynamically discovers and exposes
 * Officer marketplace tools to Claude Code. Reads tool directories from
 * PI_TOOLS_DIRS, parses TOOL.md frontmatter for schemas, and routes
 * tool calls to each tool's execute() function.
 *
 * Usage:
 *   PI_TOOLS_DIRS=/data/tools:/data/user/tools bun run src/servers/mcp-tool-server.ts
 */

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { readdirSync, existsSync, readFileSync, appendFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';

// ── Types ──

type ToolParamType = 'string' | 'number' | 'boolean' | 'enum' | 'object';

type ToolParam = {
  type: ToolParamType;
  description: string;
  values?: string[];
  optional?: boolean;
};

type ToolMeta = {
  name: string;
  label: string;
  description: string;
  inputs: Record<string, ToolParam>;
  targets?: string;
};

type DiscoveredTool = {
  entryFile: string;
  meta: ToolMeta;
};

// ── Frontmatter parsing (same logic as tool-loader-source.ts) ──

function parseFrontmatter(content: string): { meta: Partial<ToolMeta>; body: string } {
  const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
  if (!match) return { meta: {}, body: content };

  const yamlBlock = match[1]!;
  const body = match[2]!;
  const meta: Record<string, unknown> = {};

  const lines = yamlBlock.split('\n');
  let currentKey: string | null = null;
  let currentObj: Record<string, unknown> | null = null;
  let currentSubKey: string | null = null;
  let currentSubObj: Record<string, unknown> | null = null;

  for (const line of lines) {
    const topMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
    if (topMatch && !line.startsWith('  ')) {
      if (currentSubObj && currentSubKey && currentObj) {
        currentObj[currentSubKey] = currentSubObj;
        currentSubObj = null;
        currentSubKey = null;
      }
      if (currentObj && currentKey) {
        meta[currentKey] = currentObj;
        currentObj = null;
        currentKey = null;
      }
      const [, key, value] = topMatch;
      if (!value || value.trim() === '') {
        currentKey = key!;
        currentObj = {};
      } else {
        meta[key!] = value.trim();
      }
      continue;
    }

    const midMatch = line.match(/^  (\w[\w-]*):\s*(.*)$/);
    if (midMatch && currentObj !== null) {
      if (currentSubObj && currentSubKey) {
        currentObj[currentSubKey] = currentSubObj;
        currentSubObj = null;
        currentSubKey = null;
      }
      const [, key, value] = midMatch;
      if (!value || value.trim() === '') {
        currentSubKey = key!;
        currentSubObj = {};
      } else {
        currentObj[key!] = value.trim();
      }
      continue;
    }

    const deepMatch = line.match(/^    (\w[\w-]*):\s*(.*)$/);
    if (deepMatch && currentSubObj !== null) {
      const [, key, value] = deepMatch;
      currentSubObj[key!] = value!.trim();
      continue;
    }
  }

  if (currentSubObj && currentSubKey && currentObj) {
    currentObj[currentSubKey] = currentSubObj;
  }
  if (currentObj && currentKey) {
    meta[currentKey] = currentObj;
  }

  return { meta: meta as Partial<ToolMeta>, body };
}

// ── Tool discovery ──

function discoverTools(dirs: string[]): DiscoveredTool[] {
  const seen = new Set<string>();
  const tools: DiscoveredTool[] = [];

  for (const dir of dirs) {
    if (!existsSync(dir)) continue;

    for (const entry of readdirSync(dir, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue;

      const toolDir = join(dir, entry.name);
      const toolMdPath = join(toolDir, 'TOOL.md');
      if (!existsSync(toolMdPath)) continue;

      const indexTs = join(toolDir, 'index.ts');
      const indexJs = join(toolDir, 'index.js');
      const entryFile = existsSync(indexTs) ? indexTs : existsSync(indexJs) ? indexJs : null;
      if (!entryFile) continue;

      const content = readFileSync(toolMdPath, 'utf-8');
      const { meta } = parseFrontmatter(content);

      if (!meta.name || !meta.description) continue;
      if (((meta.targets as string) ?? 'all') === 'pi') continue;
      if (seen.has(meta.name)) continue;
      seen.add(meta.name);

      tools.push({ entryFile, meta: meta as ToolMeta });
    }
  }

  return tools;
}

// ── Schema building (frontmatter inputs → zod) ──

function buildZodSchema(inputs: Record<string, ToolParam>): z.ZodRawShape {
  // z.ZodRawShape is readonly in zod v4, so build it mutably and widen on return.
  const shape: Record<string, z.ZodTypeAny> = {};

  for (const [name, param] of Object.entries(inputs)) {
    let field: z.ZodTypeAny;

    switch (param.type) {
      case 'enum': {
        const values = Array.isArray(param.values)
          ? param.values
          : typeof param.values === 'string'
            ? (param.values as string).split(',').map((v) => v.trim())
            : [];
        if (values.length > 0) {
          field = z.enum(values as [string, ...string[]]).describe(param.description);
        } else {
          field = z.string().describe(param.description);
        }
        break;
      }
      case 'number':
        field = z.number().describe(param.description);
        break;
      case 'boolean':
        field = z.boolean().describe(param.description);
        break;
      case 'object':
        field = z.record(z.string(), z.unknown()).describe(param.description);
        break;
      default:
        field = z.string().describe(param.description);
    }

    shape[name] = param.optional ? field.optional() : field;
  }

  return shape;
}

// ── Logging ──

const LOG_FILE = process.env.MCP_TOOLS_LOG ?? '';

function logToolCall(toolName: string, durationMs: number, success: boolean, error?: string): void {
  const ts = new Date().toISOString();
  const status = success ? 'ok' : 'error';
  const line = error
    ? `${ts}\t${toolName}\t${status}\t${durationMs}ms\t${error}\n`
    : `${ts}\t${toolName}\t${status}\t${durationMs}ms\n`;

  // Always log to stderr for process-level visibility
  console.error(`[mcp-tools] ${toolName} ${status} (${durationMs}ms)${error ? ': ' + error : ''}`);

  // Write to log file if configured
  if (LOG_FILE) {
    try {
      mkdirSync(dirname(LOG_FILE), { recursive: true });
      appendFileSync(LOG_FILE, line);
    } catch {
      // Don't fail tool calls over logging
    }
  }
}

// ── Main ──

const rawDirs = process.env.PI_TOOLS_DIRS ?? '';
const toolDirs = rawDirs.split(':').filter(Boolean);

if (toolDirs.length === 0) {
  console.error('[mcp-tools] PI_TOOLS_DIRS not set — no tools to serve');
  process.exit(1);
}

const tools = discoverTools(toolDirs);

if (tools.length === 0) {
  console.error('[mcp-tools] No tools found in:', toolDirs.join(', '));
  process.exit(1);
}

const server = new McpServer({ name: 'officer-tools', version: '1.0.0' });

for (const { entryFile, meta } of tools) {
  const schema = buildZodSchema(meta.inputs ?? {});
  const capturedEntry = entryFile;
  const toolName = meta.name;

  server.registerTool(
    toolName,
    {
      title: meta.label ?? toolName,
      description: meta.description,
      inputSchema: z.object(schema),
    },
    async (params) => {
      const start = performance.now();
      let executeFn: Function | undefined;
      try {
        const mod = await import(capturedEntry);
        executeFn = mod.execute ?? mod.default?.execute;
      } catch (err) {
        const ms = Math.round(performance.now() - start);
        logToolCall(toolName, ms, false, `load failed: ${String(err)}`);
        return {
          content: [{ type: 'text' as const, text: `Failed to load ${toolName}: ${String(err)}` }],
          isError: true,
        };
      }

      if (typeof executeFn !== 'function') {
        const ms = Math.round(performance.now() - start);
        logToolCall(toolName, ms, false, 'no execute function');
        return {
          content: [{ type: 'text' as const, text: `${toolName}/index.ts must export an "execute" function` }],
          isError: true,
        };
      }

      try {
        const result = await executeFn('mcp', params, undefined, undefined);
        const ms = Math.round(performance.now() - start);
        logToolCall(toolName, ms, !result.isError, result.isError ? 'tool returned error' : undefined);
        return result;
      } catch (err) {
        const ms = Math.round(performance.now() - start);
        logToolCall(toolName, ms, false, String(err));
        return {
          content: [{ type: 'text' as const, text: `${toolName} threw: ${String(err)}` }],
          isError: true,
        };
      }
    },
  );

  console.error(`[mcp-tools] Registered: ${toolName} (${capturedEntry})`);
}

const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`[mcp-tools] Server running with ${tools.length} tools`);
