/**
 * run-manager.ts — Create and list isolated workflow run directories.
 *
 * Each run lives under `.sf/workflow-runs/<name>/<timestamp>/` and contains:
 * - DEFINITION.yaml — frozen snapshot of the workflow definition at run-creation time
 * - GRAPH.yaml — initialized step graph with all steps pending
 * - PARAMS.json — (optional) parameter overrides used for this run
 *
 * Observability:
 * - All run state is on disk in human-readable YAML/JSON — inspectable with cat/less.
 * - `listRuns()` returns structured metadata including step counts and overall status.
 * - Timestamp directory names are filesystem-safe (ISO with hyphens replacing colons).
 * - Errors include the full path context for diagnosis.
 */
import {
	existsSync,
	mkdirSync,
	readdirSync,
	readFileSync,
	statSync,
	writeFileSync,
} from "node:fs";
import { join } from "node:path";
import { stringify } from "yaml";
import {
	loadDefinition,
	substituteParams,
	validateDefinition,
} from "./definition-loader.js";
import { initializeGraph, readGraph, writeGraph } from "./graph.js";

// ─── Constants ───────────────────────────────────────────────────────────
const RUNS_DIR = "workflow-runs";
const DEFS_DIR = "workflow-defs";
const RUN_METADATA_FILENAME = "RUN.json";
// ─── Helpers ─────────────────────────────────────────────────────────────
/**
 * Generate a filesystem-safe timestamp: `YYYY-MM-DDTHH-MM-SS`.
 * Replaces colons with hyphens so the string is safe as a directory name
 * on all platforms (Windows forbids colons in paths).
 */
function makeTimestamp(date = new Date()) {
	return date
		.toISOString()
		.replace(/:/g, "-")
		.replace(/\.\d{3}Z$/, "");
}
/**
 * Derive overall status from a graph's step statuses.
 */
function deriveStatus(graph) {
	const hasActive = graph.steps.some((s) => s.status === "active");
	const allDone = graph.steps.every(
		(s) => s.status === "complete" || s.status === "expanded",
	);
	if (allDone) return "complete";
	if (hasActive) return "running";
	return "pending";
}
function createRunDirectory(basePath, runName) {
	const timestamp = makeTimestamp();
	const runDir = join(basePath, ".sf", RUNS_DIR, runName, timestamp);
	mkdirSync(runDir, { recursive: true });
	return runDir;
}
function writeRunFiles(runDir, def, options) {
	writeFileSync(join(runDir, "DEFINITION.yaml"), stringify(def), "utf-8");
	const graph = initializeGraph(def);
	writeGraph(runDir, graph);
	if (options?.params && Object.keys(options.params).length > 0) {
		writeFileSync(
			join(runDir, "PARAMS.json"),
			JSON.stringify(options.params, null, 2),
			"utf-8",
		);
	}
	if (options?.source) {
		writeFileSync(
			join(runDir, RUN_METADATA_FILENAME),
			JSON.stringify(options.source, null, 2) + "\n",
			"utf-8",
		);
	}
}
function readRunSource(runDir) {
	const metadataPath = join(runDir, RUN_METADATA_FILENAME);
	if (!existsSync(metadataPath)) return undefined;
	try {
		return JSON.parse(readFileSync(metadataPath, "utf-8"));
	} catch {
		return undefined;
	}
}
// ─── Public API ──────────────────────────────────────────────────────────
/**
 * Create a new isolated run directory for a workflow definition.
 *
 * 1. Loads the definition from `<basePath>/.sf/workflow-defs/<defName>.yaml`
 * 2. Applies parameter substitution if overrides are provided
 * 3. Creates `<basePath>/.sf/workflow-runs/<defName>/<timestamp>/`
 * 4. Writes frozen DEFINITION.yaml, initialized GRAPH.yaml, and optional PARAMS.json
 *
 * @param basePath — project root directory
 * @param defName — definition filename (without .yaml extension)
 * @param overrides — optional parameter overrides (merged with definition defaults)
 * @returns Full path to the created run directory
 * @throws Error if the definition file doesn't exist or is invalid
 */
export function createRun(basePath, defName, overrides) {
	const defsDir = join(basePath, ".sf", DEFS_DIR);
	// Load and validate the definition
	const rawDef = loadDefinition(defsDir, defName);
	// Apply parameter substitution if overrides provided
	const def = overrides
		? substituteParams(rawDef, overrides)
		: substituteParams(rawDef); // still resolve default params if any
	const runDir = createRunDirectory(basePath, defName);
	writeRunFiles(runDir, def, {
		params: overrides,
		source: {
			kind: "definition",
			mode: "explicit",
			createdAt: new Date().toISOString(),
		},
	});
	return runDir;
}
/**
 * Create a new isolated run directory from an already-built definition.
 *
 * Purpose: let non-YAML authoring surfaces, such as `/start` templates,
 * enter the same graph-backed runtime as custom YAML definitions.
 *
 * Consumer: workflow template commands after resolving and compiling a template.
 */
export function createRunFromDefinition(basePath, runName, definition, source) {
	const validation = validateDefinition(definition);
	if (!validation.valid) {
		throw new Error(
			`Invalid workflow definition for run "${runName}":\n  - ${validation.errors.join("\n  - ")}`,
		);
	}
	const runDir = createRunDirectory(basePath, runName);
	writeRunFiles(runDir, definition, {
		source: source
			? {
					...source,
					createdAt: new Date().toISOString(),
				}
			: undefined,
	});
	return runDir;
}
/**
 * List existing workflow runs with metadata.
 *
 * Scans `<basePath>/.sf/workflow-runs/` for run directories. Each run's
 * GRAPH.yaml is read to derive step counts and overall status.
 *
 * @param basePath — project root directory
 * @param defName — optional filter: only list runs for this definition name
 * @returns Array of run metadata, sorted newest-first within each definition
 */
export function listRuns(basePath, defName) {
	const runsRoot = join(basePath, ".sf", RUNS_DIR);
	if (!existsSync(runsRoot)) return [];
	const results = [];
	// Get workflow name directories
	const nameDirs = defName
		? [defName]
		: readdirSync(runsRoot).filter((entry) => {
				const full = join(runsRoot, entry);
				return statSync(full).isDirectory();
			});
	for (const name of nameDirs) {
		const nameDir = join(runsRoot, name);
		if (!existsSync(nameDir)) continue;
		const timestamps = readdirSync(nameDir).filter((entry) => {
			const full = join(nameDir, entry);
			return statSync(full).isDirectory();
		});
		// Sort newest-first (ISO strings sort lexicographically)
		timestamps.sort().reverse();
		for (const ts of timestamps) {
			const runDir = join(nameDir, ts);
			try {
				const graph = readGraph(runDir);
				const total = graph.steps.length;
				const completed = graph.steps.filter(
					(s) => s.status === "complete",
				).length;
				const pending = graph.steps.filter(
					(s) => s.status === "pending",
				).length;
				const active = graph.steps.filter((s) => s.status === "active").length;
				results.push({
					name,
					timestamp: ts,
					runDir,
					steps: { total, completed, pending, active },
					status: deriveStatus(graph),
					source: readRunSource(runDir),
				});
			} catch {
				// Skip runs with invalid/missing GRAPH.yaml
			}
		}
	}
	return results;
}
