/** Pipeline tools — list/run/inspect workflow definitions and runs, and create a
 *  new workflow from a high-level step list (binding each step to a CLI agent).
 *
 *  Vocabulary: a *workflow* is a YAML DAG definition; a *pipeline* is a run of one.
 *  Both definitions live in <dataDir>/flows/*.yaml. lib/flows.ts is a separate
 *  legacy fan-out system and is intentionally NOT used here. */

import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import YAML from 'yaml';
import {
  listWorkflows, getWorkflow, startPipeline, getPipeline, listPipelinesSummary, parseWorkflow, cancelPipeline,
} from '@/lib/pipeline';
import { getDataDir } from '@/lib/dirs';
import { ensureInitialized } from '@/lib/init';
import { getProjectInfo } from '@/lib/projects';
import { listAgents } from '@/lib/agents';
import { text, fail, guard } from './_shared';

export function registerPipelineTools(server: McpServer): void {
  server.tool(
    'forge_list_workflows',
    'List the available pipeline workflow definitions (reusable DAG templates). Use forge_run_pipeline to start one, or forge_help topic="pipelines" to learn what a pipeline is.',
    {},
    () => guard(() => {
      const wfs = listWorkflows();
      if (wfs.length === 0) return text('No workflows defined. Create one with forge_create_pipeline, or install from the marketplace.');
      const lines = wfs.map((w) => {
        const n = Object.keys(w.nodes || {}).length;
        return `• ${w.name}${w.type && w.type !== 'dag' ? ` (${w.type})` : ''} — ${n} node(s)${w.description ? `: ${w.description}` : ''}`;
      });
      return text(`${wfs.length} workflow(s):\n${lines.join('\n')}`);
    }),
  );

  server.tool(
    'forge_run_pipeline',
    'Start a pipeline from a workflow definition. Provide any input variables the workflow declares.',
    {
      workflow: z.string().describe('Workflow name (from forge_list_workflows)'),
      input: z.record(z.string(), z.string()).optional().describe('Input variables, e.g. { "project": "my-app" }'),
    },
    (params) => guard(() => {
      if (!getWorkflow(params.workflow)) return fail(`Workflow not found: ${params.workflow}. Use forge_list_workflows.`);
      ensureInitialized();
      const pipeline = startPipeline(params.workflow, params.input || {});
      return text(`Started pipeline ${pipeline.id} (workflow: ${params.workflow}, status: ${pipeline.status}).\nTrack it with forge_get_pipeline id=${pipeline.id}.`);
    }),
  );

  server.tool(
    'forge_get_pipeline',
    'Show the status and per-node results of a pipeline run.',
    { id: z.string().describe('Pipeline run id') },
    (params) => guard(() => {
      const p = getPipeline(params.id);
      if (!p) return fail(`Pipeline not found: ${params.id}`);
      const nodes = Object.entries(p.nodes).map(([id, n]) => {
        let line = `  ${id}: ${n.status}`;
        if (n.error) line += ` — ${n.error}`;
        return line;
      });
      return text(`Pipeline ${p.id} [${p.status}] (workflow: ${p.workflowName})\n${nodes.join('\n')}`);
    }),
  );

  server.tool(
    'forge_cancel_pipeline',
    'Cancel a running pipeline by id — stops its in-flight nodes. No-op if it already finished.',
    { id: z.string().describe('Pipeline run id (from forge_run_pipeline / forge_list_pipeline_runs)') },
    (params) => guard(() => {
      const ok = cancelPipeline(params.id);
      return text(ok ? `Cancelled pipeline ${params.id}.` : `Could not cancel ${params.id} (not found, or already finished).`);
    }),
  );

  server.tool(
    'forge_list_pipeline_runs',
    'List recent pipeline runs (newest first) with their status.',
    { limit: z.number().int().positive().max(100).optional().describe('Max runs to show (default 20)') },
    (params) => guard(() => {
      const runs = listPipelinesSummary({ limit: params.limit ?? 20 });
      if (runs.length === 0) return text('No pipeline runs yet.');
      const lines = runs.map((r) => `• ${r.id}  [${r.status}]  ${r.workflowName}  (${r.createdAt})`);
      return text(`${runs.length} run(s):\n${lines.join('\n')}`);
    }),
  );

  server.tool(
    'forge_create_pipeline',
    'Create a new pipeline workflow from an ordered list of steps. Each step runs an agent in a project; steps run in sequence (each depends on the previous). Bind a step to a specific CLI agent via its "agent" field (defaults to "claude"). Saves a reusable workflow you can then forge_run_pipeline.',
    {
      name: z.string().describe('Workflow name (used as the file name)'),
      description: z.string().optional().describe('What the pipeline does'),
      steps: z.array(z.object({
        project: z.string().describe('Project name or {{vars}} template the step runs in'),
        prompt: z.string().describe('What the agent should do in this step'),
        agent: z.string().optional().describe('CLI agent id for this step (default "claude")'),
      })).min(1).describe('Ordered steps; each runs after the previous one'),
    },
    (params) => guard(() => {
      // Catch typo'd project names at create time rather than letting every node
      // fail at run with "Project not found". Templated ({{var}}) projects are
      // resolved per-run, so they can't be checked here — skip them.
      const unknown = [...new Set(
        params.steps.map((s) => s.project).filter((p) => !p.includes('{{') && !getProjectInfo(p)),
      )];
      if (unknown.length) {
        return fail(`Unknown project(s): ${unknown.join(', ')}. Use forge_list_projects, or pass a {{var}} template resolved at run time.`);
      }

      // Validate explicit agent ids too, so a typo fails at create time rather than
      // at the node that uses it. (Omitted agent defaults to "claude" below.)
      const validAgents = new Set(listAgents().map((a) => a.id));
      const badAgents = [...new Set(
        params.steps.map((s) => s.agent).filter((a): a is string => !!a && !a.includes('{{') && !validAgents.has(a)),
      )];
      if (badAgents.length) {
        return fail(`Unknown agent id(s): ${badAgents.join(', ')}. Use forge_list_agents, or omit to default to "claude".`);
      }

      const safeName = params.name.replace(/[^a-zA-Z0-9_-]/g, '-');
      const nodes: Record<string, unknown> = {};
      params.steps.forEach((st, i) => {
        const id = `s${i + 1}`;
        nodes[id] = {
          project: st.project,
          prompt: st.prompt,
          agent: st.agent || 'claude',
          ...(i > 0 ? { depends_on: [`s${i}`] } : {}),
        };
      });
      const wf = { name: safeName, ...(params.description ? { description: params.description } : {}), nodes };
      const yaml = YAML.stringify(wf);

      // Validate against the engine's own parser before writing.
      try { parseWorkflow(yaml); }
      catch (e) { return fail(`Generated workflow is invalid: ${(e as Error).message}`); }

      const dir = join(getDataDir(), 'flows');
      mkdirSync(dir, { recursive: true });
      const file = join(dir, `${safeName}.yaml`);
      if (existsSync(file)) return fail(`A workflow named "${safeName}" already exists. Pick another name.`);
      writeFileSync(file, yaml, 'utf-8');
      return text(`Created workflow "${safeName}" with ${params.steps.length} step(s).\nRun it with forge_run_pipeline workflow="${safeName}".`);
    }),
  );
}
