/**
 * Plan-to-Graph Compiler (Issue #910, E2-1)
 *
 * Converts a PlanContract into a CompiledGraph using the existing
 * GraphBuilder. Each stage becomes a graph node. Dependencies become
 * edges. Policy gates become gate nodes inserted between stages.
 *
 * @module pipeline/plan-compiler
 */
import { GraphBuilder, overwrite, append } from '../orchestration/graph/graph-builder.js';
import { START, END } from '../orchestration/graph/graph-types.js';

import { formatCompileError } from '../orchestration/graph/graph-types.js';
import type { CompiledGraph, GraphState } from '../orchestration/graph/graph-types.js';
import type { PlanContract, StageSpec, PolicyGateSpec } from './task-contract.js';
import type { IPluginRegistry } from './plugin-types.js';

/** Result of plan compilation. */
type CompileResult =
  | { readonly ok: true; readonly value: CompiledGraph }
  | { readonly ok: false; readonly error: string };

/** Options for plan compilation. */
export interface PlanCompileOptions {
  /** Plugin registry for resolving stage handlers. When provided, stages with
   *  a registered pluginId will use the plugin's execute() method. */
  readonly pluginRegistry?: IPluginRegistry;
}

// ============================================================================
// Public API
// ============================================================================

/**
 * Compiles a PlanContract into a CompiledGraph.
 *
 * - Each stage becomes a node with a handler (plugin-backed or placeholder)
 * - Dependencies become fixed edges
 * - Policy gates become gate nodes between stages
 * - Stages with no dependencies get edges from START
 * - Stages with no dependents get edges to END
 */
export function compilePlan(plan: PlanContract, options?: PlanCompileOptions): CompileResult {
  if (plan.stages.length === 0) {
    return { ok: false, error: 'Plan must have at least one stage' };
  }

  // Validate dependency references
  const stageIds = new Set(plan.stages.map((s) => s.id));
  for (const stage of plan.stages) {
    for (const dep of stage.dependencies) {
      if (!stageIds.has(dep)) {
        return {
          ok: false,
          error: `Stage "${stage.id}" depends on unknown stage "${dep}"`,
        };
      }
    }
  }

  const builder = new GraphBuilder();
  addPipelineState(builder);
  addStageNodes(builder, plan.stages, options?.pluginRegistry);
  addGateNodes(builder, plan.policyGates);
  addEdges(builder, plan.stages, plan.policyGates);

  const result = builder.compile();
  if (result.ok) {
    return { ok: true, value: result.value };
  }
  return { ok: false, error: formatCompileError(result.error) };
}

// ============================================================================
// Internal Helpers
// ============================================================================

/** Adds pipeline-level state fields to the graph. */
function addPipelineState(builder: GraphBuilder): void {
  builder.addState('stageResults', append<Record<string, unknown>>([]));
  builder.addState('artifacts', append<string>([]));
  builder.addState('currentStage', overwrite(''));
  builder.addState('pipelineStatus', overwrite('running'));
  builder.addState('error', overwrite(''));
}

/**
 * Creates a handler for a pipeline stage.
 * Resolves from PluginRegistry when available; falls back to placeholder.
 */
function createStageHandler(
  stage: StageSpec,
  registry?: IPluginRegistry
): (state: Readonly<GraphState>) => Promise<Partial<GraphState>> {
  const plugin = registry?.resolve(stage.pluginId);
  if (plugin !== undefined) {
    return async (_state: Readonly<GraphState>) => {
      const ctx = { signal: AbortSignal.timeout(30_000), task: {} as never, config: stage.config };
      const result = await plugin.execute(stage, ctx);
      return {
        currentStage: stage.id,
        stageResults: [{ stageId: stage.id, status: result.success ? 'completed' : 'failed' }],
        ...(result.outputArtifacts.length > 0
          ? { artifacts: result.outputArtifacts.map((a) => a.id) }
          : {}),
      };
    };
  }
  // Placeholder when no plugin registered for this stage's pluginId
  return (_state: Readonly<GraphState>) =>
    Promise.resolve({
      currentStage: stage.id,
      stageResults: [{ stageId: stage.id, status: 'completed' }],
    });
}

/** Creates a handler for a policy gate node. */
function createGateHandler(
  gate: PolicyGateSpec
): (state: Readonly<GraphState>) => Promise<Partial<GraphState>> {
  return (_state: Readonly<GraphState>) =>
    Promise.resolve({
      currentStage: gate.id,
      stageResults: [{ gateId: gate.id, status: 'passed' }],
    });
}

/** Adds stage nodes to the graph builder. */
function addStageNodes(
  builder: GraphBuilder,
  stages: readonly StageSpec[],
  registry?: IPluginRegistry
): void {
  for (const stage of stages) {
    builder.addNode(stage.id, createStageHandler(stage, registry));
  }
}

/** Adds policy gate nodes to the graph builder. */
function addGateNodes(builder: GraphBuilder, gates: readonly PolicyGateSpec[]): void {
  for (const gate of gates) {
    builder.addNode(gate.id, createGateHandler(gate));
  }
}

/**
 * Adds edges between nodes based on stage dependencies and gates.
 *
 * Logic:
 * 1. If a gate exists afterStage→beforeStage, insert gate node between
 * 2. Otherwise, add direct dependency edge
 * 3. Stages with no dependencies get START→stage edges
 * 4. Stages with no dependents get stage→END edges
 */
function addEdges(
  builder: GraphBuilder,
  stages: readonly StageSpec[],
  gates: readonly PolicyGateSpec[]
): void {
  const isDependedOn = new Set<string>();

  // Build gate lookup: key = "afterStage→beforeStage"
  const gateMap = new Map<string, PolicyGateSpec>();
  for (const gate of gates) {
    gateMap.set(`${gate.afterStage}→${gate.beforeStage}`, gate);
  }

  // Add dependency edges (with gate interposition)
  for (const stage of stages) {
    for (const dep of stage.dependencies) {
      isDependedOn.add(dep);
      const gateKey = `${dep}→${stage.id}`;
      const gate = gateMap.get(gateKey);
      if (gate !== undefined) {
        builder.addEdge(dep, gate.id);
        builder.addEdge(gate.id, stage.id);
        gateMap.delete(gateKey); // consumed
      } else {
        builder.addEdge(dep, stage.id);
      }
    }
  }

  // Stages with no dependencies → START edges
  for (const stage of stages) {
    if (stage.dependencies.length === 0) {
      builder.addEdge(START, stage.id);
    }
  }

  // Stages not depended upon → END edges
  for (const stage of stages) {
    if (!isDependedOn.has(stage.id)) {
      builder.addEdge(stage.id, END);
    }
  }
}
