import fs from 'node:fs';
import path from 'node:path';

import { STAGE_SEQUENCE, STAGE_SPECS } from './constants.js';
import { determineBriefClass, stagesForClass } from './brief-classifier.js';
import { ForgeCapabilityError, providerErrorCode } from './errors.js';
import {
  blockedGateResults,
  evaluateStageGates,
  isCapabilityGate,
  simulatedGateResults,
  waitingGateResults,
} from './outcomes.js';
import { mapStageToTask } from './stage-adapter.js';
import { createSimulatedExecutor } from './simulated-executor.js';
import type {
  ForgeTask,
  ForgeTaskResult,
  PipelineOptions,
  PipelineResult,
  RunManifest,
  RunMode,
  StageStatus,
  TaskExecutor,
} from './types.js';

/** Reason stamped on stages that complete under explicit simulation. */
const SIMULATED_STAGE_REASON =
  'simulated execution (--simulate): stage was not executed by a real executor';

/**
 * Generate a timestamp-based run ID.
 */
export function generateRunId(): string {
  const now = new Date();
  const pad = (n: number, w = 2) => String(n).padStart(w, '0');
  return [
    now.getUTCFullYear(),
    pad(now.getUTCMonth() + 1),
    pad(now.getUTCDate()),
    '-',
    pad(now.getUTCHours()),
    pad(now.getUTCMinutes()),
    pad(now.getUTCSeconds()),
  ].join('');
}

/**
 * Get the ISO timestamp for now.
 */
function nowISO(): string {
  return new Date().toISOString();
}

/**
 * Create and persist a run manifest.
 */
function createManifest(opts: {
  runId: string;
  briefPath: string;
  codebase: string;
  briefClass: RunManifest['briefClass'];
  classSource: RunManifest['classSource'];
  forceBoard: boolean;
  mode: RunMode;
  runDir: string;
}): RunManifest {
  const ts = nowISO();
  const manifest: RunManifest = {
    runId: opts.runId,
    brief: opts.briefPath,
    codebase: opts.codebase,
    briefClass: opts.briefClass,
    classSource: opts.classSource,
    forceBoard: opts.forceBoard,
    mode: opts.mode,
    createdAt: ts,
    updatedAt: ts,
    currentStage: '',
    status: 'in_progress',
    stages: {},
  };
  saveManifest(opts.runDir, manifest);
  return manifest;
}

/**
 * Save a manifest to disk.
 */
export function saveManifest(runDir: string, manifest: RunManifest): void {
  manifest.updatedAt = nowISO();
  const manifestPath = path.join(runDir, 'manifest.json');
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
}

/**
 * Load a manifest from disk.
 */
export function loadManifest(runDir: string): RunManifest {
  const manifestPath = path.join(runDir, 'manifest.json');
  if (!fs.existsSync(manifestPath)) {
    throw new Error(`manifest.json not found: ${manifestPath}`);
  }
  return JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as RunManifest;
}

/**
 * Select and validate stages, optionally skipping to a specific stage.
 */
export function selectStages(stages?: string[], skipTo?: string): string[] {
  const selected = stages ?? [...STAGE_SEQUENCE];

  const unknown = selected.filter((s) => !STAGE_SEQUENCE.includes(s));
  if (unknown.length > 0) {
    throw new Error(`Unknown Forge stages requested: ${unknown.join(', ')}`);
  }

  if (!skipTo) return selected;

  if (!selected.includes(skipTo)) {
    throw new Error(`skip_to stage '${skipTo}' is not present in the selected stage list`);
  }
  const skipIndex = selected.indexOf(skipTo);
  return selected.slice(skipIndex);
}

/**
 * Fail closed when the required executor capability is missing (SDLC-D-035).
 */
function requireExecutor(executor: TaskExecutor | undefined, simulate: boolean): TaskExecutor {
  if (executor) return executor;
  if (simulate) return createSimulatedExecutor({ log: false });
  throw new ForgeCapabilityError(
    'FORGE_NO_EXECUTOR',
    'task-executor',
    'no task executor is wired; refusing to run the pipeline with a stub executor (fail closed). ' +
      'Pass --simulate to opt into explicitly simulated execution.',
  );
}

/**
 * Pre-flight a stage's gates in normal mode (fail closed, SDLC-D-035).
 *
 * - authority gates: record a typed `waiting-for-authority` stage result and
 *   raise FORGE_AUTHORITY_REQUIRED — approval-based gates never pass vacuously.
 * - provider gates: record a typed `blocked` stage result and raise the typed
 *   capability error for the missing provider.
 *
 * Returns the stage status to record when the pre-flight blocks, or undefined
 * when the stage may proceed.
 */
function preflightStageGates(
  stageName: string,
  manifest: RunManifest,
): { status: StageStatus; error: ForgeCapabilityError } | undefined {
  const spec = STAGE_SPECS[stageName];
  if (!spec) throw new Error(`Unknown Forge stage: ${stageName}`);

  for (const gate of spec.qualityGates) {
    if (!isCapabilityGate(gate)) continue;

    const startedAt = manifest.stages[stageName]?.startedAt;
    const completedAt = nowISO();

    if (gate.kind === 'authority') {
      const reason = `gate '${gate.capability}' requires authority sign-off; no mechanical implementation exists (${gate.reason})`;
      return {
        status: {
          status: 'waiting-for-authority',
          reason,
          startedAt,
          completedAt,
          gateResults: waitingGateResults(spec.qualityGates, reason),
        },
        error: new ForgeCapabilityError(
          'FORGE_AUTHORITY_REQUIRED',
          gate.capability,
          `stage '${stageName}' is blocked on authority gate '${gate.capability}': ${gate.reason}. ` +
            'The pipeline fails closed instead of passing vacuously. Record the approval out-of-band ' +
            'or run with --simulate for explicitly simulated execution.',
        ),
      };
    }

    const reason = `gate '${gate.capability}' requires provider '${gate.capability}' and none is wired (${gate.reason})`;
    return {
      status: {
        status: 'blocked',
        reason,
        startedAt,
        completedAt,
        gateResults: blockedGateResults(spec.qualityGates, reason),
      },
      error: new ForgeCapabilityError(
        providerErrorCode(gate.capability),
        gate.capability,
        `stage '${stageName}' requires provider '${gate.capability}' which is not wired: ${gate.reason}. ` +
          'The pipeline fails closed instead of passing vacuously.',
      ),
    };
  }

  return undefined;
}

/**
 * Execute the given stage tasks sequentially, updating the manifest.
 *
 * Normal mode requires a real executor and evaluates every declared command
 * gate through the typed outcome model; any non-verified result fails closed.
 * Simulate mode types every stage and gate result as `simulated`.
 */
async function executeStages(opts: {
  manifest: RunManifest;
  runDir: string;
  tasks: ForgeTask[];
  stageNames: string[];
  executor: TaskExecutor;
  simulate: boolean;
}): Promise<void> {
  const { manifest, runDir, tasks, stageNames, executor, simulate } = opts;

  for (let i = 0; i < tasks.length; i++) {
    const task = tasks[i]!;
    const stageName = stageNames[i]!;
    const spec = STAGE_SPECS[stageName];
    if (!spec) throw new Error(`Unknown Forge stage: ${stageName}`);

    // Update manifest: stage in progress
    manifest.currentStage = stageName;
    manifest.stages[stageName] = {
      status: 'in_progress',
      startedAt: nowISO(),
    };
    saveManifest(runDir, manifest);

    // Fail-closed pre-flight (normal mode only): authority/provider gates have
    // no mechanical implementation and must never pass vacuously.
    if (!simulate) {
      const blocked = preflightStageGates(stageName, manifest);
      if (blocked) {
        manifest.stages[stageName] = blocked.status;
        manifest.status =
          blocked.status.status === 'waiting-for-authority' ? 'waiting-for-authority' : 'failed';
        saveManifest(runDir, manifest);
        throw blocked.error;
      }
    }

    let result: ForgeTaskResult;
    try {
      await executor.submitTask(task);
      result = await executor.waitForCompletion(task.id, task.timeoutSeconds * 1000);
    } catch (error) {
      // Process errors (including timeouts) map to the fail-closed `error` outcome.
      const reason = error instanceof Error ? error.message : String(error);
      manifest.stages[stageName] = {
        status: 'error',
        reason: `executor error: ${reason}`,
        startedAt: manifest.stages[stageName]?.startedAt,
        completedAt: nowISO(),
        gateResults: [],
      };
      manifest.status = 'failed';
      saveManifest(runDir, manifest);
      throw error instanceof Error ? error : new Error(reason);
    }

    if (simulate) {
      manifest.stages[stageName] = {
        status: 'simulated',
        reason: SIMULATED_STAGE_REASON,
        startedAt: manifest.stages[stageName]?.startedAt,
        completedAt: nowISO(),
        gateResults: simulatedGateResults(spec.qualityGates),
      };
      saveManifest(runDir, manifest);
      continue;
    }

    const evaluation = evaluateStageGates(stageName, spec.qualityGates, result);
    manifest.stages[stageName] = {
      status: evaluation.outcome,
      reason: evaluation.reason,
      startedAt: manifest.stages[stageName]?.startedAt,
      completedAt: nowISO(),
      gateResults: evaluation.gateResults,
    };

    if (evaluation.outcome !== 'passed') {
      manifest.status =
        evaluation.outcome === 'waiting-for-authority' ? 'waiting-for-authority' : 'failed';
      saveManifest(runDir, manifest);
      throw new Error(`Stage ${stageName} ${evaluation.outcome}: ${evaluation.reason}`);
    }

    saveManifest(runDir, manifest);
  }
}

/**
 * Run the Forge pipeline.
 *
 * 1. Fail closed unless a real executor is wired or simulation is explicit
 * 2. Classify the brief
 * 3. Generate a run ID and create run directory
 * 4. Map stages to tasks and submit to TaskExecutor
 * 5. Track manifest with typed stage outcomes
 * 6. Return pipeline result
 */
export async function runPipeline(
  briefPath: string,
  projectRoot: string,
  options: PipelineOptions,
): Promise<PipelineResult> {
  const simulate = options.simulate ?? false;
  const executor = requireExecutor(options.executor, simulate);
  const mode: RunMode = simulate ? 'simulated' : 'normal';

  const resolvedRoot = path.resolve(projectRoot);
  const resolvedBrief = path.resolve(briefPath);
  const briefContent = fs.readFileSync(resolvedBrief, 'utf-8');

  // Classify brief
  const { briefClass, classSource } = determineBriefClass(briefContent, options.briefClass);

  // Determine stages
  const classStages = options.stages ?? stagesForClass(briefClass, options.forceBoard);
  const selectedStages = selectStages(classStages, options.skipTo);

  // Create run directory
  const runId = generateRunId();
  const runDir = path.join(resolvedRoot, '.forge', 'runs', runId);
  fs.mkdirSync(runDir, { recursive: true });

  // Create manifest
  const manifest = createManifest({
    runId,
    briefPath: resolvedBrief,
    codebase: options.codebase ?? '',
    briefClass,
    classSource,
    forceBoard: options.forceBoard ?? false,
    mode,
    runDir,
  });

  // Map stages to tasks
  const tasks: ForgeTask[] = [];
  for (let i = 0; i < selectedStages.length; i++) {
    const stageName = selectedStages[i]!;
    const task = mapStageToTask({
      stageName,
      briefContent,
      projectRoot: resolvedRoot,
      runId,
      runDir,
    });

    // Override dependency chain for selected (possibly filtered) stages
    if (i > 0) {
      task.dependsOn = [tasks[i - 1]!.id];
    } else {
      delete task.dependsOn;
    }

    tasks.push(task);
  }

  // Execute stages
  await executeStages({ manifest, runDir, tasks, stageNames: selectedStages, executor, simulate });

  // All stages reached a terminal state for this mode
  manifest.status = simulate ? 'simulated' : 'completed';
  saveManifest(runDir, manifest);

  return {
    runId,
    briefPath: resolvedBrief,
    projectRoot: resolvedRoot,
    runDir,
    taskIds: tasks.map((t) => t.id),
    stages: selectedStages,
    manifest,
  };
}

/**
 * Resume a pipeline from the last non-passed stage.
 */
export async function resumePipeline(
  runDir: string,
  executor?: TaskExecutor,
  options?: { simulate?: boolean },
): Promise<PipelineResult> {
  const simulate = options?.simulate ?? false;
  const wiredExecutor = requireExecutor(executor, simulate);
  const mode: RunMode = simulate ? 'simulated' : 'normal';

  const manifest = loadManifest(runDir);
  const resolvedRoot = path.dirname(path.dirname(path.dirname(runDir))); // .forge/runs/{id} → project root

  const briefContent = fs.readFileSync(manifest.brief, 'utf-8');
  const allStages = stagesForClass(manifest.briefClass, manifest.forceBoard);

  manifest.mode = mode;

  // Find first non-satisfying stage (only a verified `passed` counts as done;
  // simulated and waiting-for-authority stages are re-run).
  const resumeFrom = allStages.find((s) => manifest.stages[s]?.status !== 'passed');
  if (!resumeFrom) {
    manifest.status = mode === 'simulated' ? 'simulated' : 'completed';
    saveManifest(runDir, manifest);
    return {
      runId: manifest.runId,
      briefPath: manifest.brief,
      projectRoot: resolvedRoot,
      runDir,
      taskIds: [],
      stages: allStages,
      manifest,
    };
  }

  const remainingStages = selectStages(allStages, resumeFrom);
  manifest.status = 'in_progress';

  const tasks: ForgeTask[] = [];
  for (let i = 0; i < remainingStages.length; i++) {
    const stageName = remainingStages[i]!;
    const task = mapStageToTask({
      stageName,
      briefContent,
      projectRoot: resolvedRoot,
      runId: manifest.runId,
      runDir,
    });

    if (i > 0) {
      task.dependsOn = [tasks[i - 1]!.id];
    } else {
      delete task.dependsOn;
    }
    tasks.push(task);
  }

  await executeStages({
    manifest,
    runDir,
    tasks,
    stageNames: remainingStages,
    executor: wiredExecutor,
    simulate,
  });

  manifest.status = simulate ? 'simulated' : 'completed';
  saveManifest(runDir, manifest);

  return {
    runId: manifest.runId,
    briefPath: manifest.brief,
    projectRoot: resolvedRoot,
    runDir,
    taskIds: tasks.map((t) => t.id),
    stages: remainingStages,
    manifest,
  };
}

/**
 * Get the status of a pipeline run.
 */
export function getPipelineStatus(runDir: string): RunManifest {
  return loadManifest(runDir);
}
