/**
 * commands-scaffold-migrate.js — `/scaffold migrate --profile <name>` (ADR-022).
 *
 * Purpose: transition an existing repo to a different scaffold profile, safely.
 * Files leaving the active profile are stamped `state=disabled` (or pruned with
 * `--prune`). Files re-entering the profile have their `state=disabled` reversed
 * to `state=pending`. Files the user has edited or completed are left alone with
 * a warning.
 *
 * Consumer: user-triggered via `/scaffold migrate --profile infra` after
 * onboarding SF into a non-code-shaped repo.
 */
import { existsSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { detectRepoProfile } from "./agentic-docs-scaffold.js";
import { projectRoot } from "./commands/context.js";
import { PROFILE_NAMES, SCAFFOLD_FILES } from "./scaffold-constants.js";
import { detectScaffoldDrift } from "./scaffold-drift.js";
import { resolveActiveProfileSet } from "./scaffold-profiles.js";
import {
	bodyHash,
	extractMarker,
	readScaffoldManifest,
	stampScaffoldFile,
	writeScaffoldManifest,
} from "./scaffold-versioning.js";

/** Parse args for `/scaffold migrate`. */
export function parseScaffoldMigrateArgs(args) {
	const trimmed = (args || "").trim();
	const tokens = trimmed.length > 0 ? trimmed.split(/\s+/) : [];
	const opts = {
		profile: null,
		prune: false,
		dryRun: false,
	};
	for (const tok of tokens) {
		if (tok.startsWith("--profile=")) {
			opts.profile = tok.slice("--profile=".length).trim() || null;
		} else if (tok === "--prune") {
			opts.prune = true;
		} else if (tok === "--dry-run") {
			opts.dryRun = true;
		}
	}
	return opts;
}

/**
 * Core migrate algorithm (ADR-022 §5).
 *
 * Returns a result object describing what was changed and what needs review.
 * Does not mutate the filesystem when `dryRun` is true.
 *
 * @param {string} basePath
 * @param {string} targetProfile
 * @param {{ prune?: boolean, dryRun?: boolean }} opts
 */
export function runScaffoldMigrate(basePath, targetProfile, opts = {}) {
	const { prune = false, dryRun = false } = opts;
	const sfVersion = process.env.SF_VERSION || "0.0.0";
	const manifest = readScaffoldManifest(basePath);
	const { profileSet: targetSet, warning } = resolveActiveProfileSet(
		basePath,
		manifest,
		targetProfile,
	);
	const result = {
		reEnabled: [], // state=disabled → state=pending (re-entered profile)
		disabled: [], // state=pending → state=disabled (left profile)
		pruned: [], // deleted with --prune
		warnings: [], // editing/completed/hash-diverged — left alone
	};
	if (warning) {
		result.warnings.push({ path: "(profile)", reason: warning });
	}

	// Step 1: re-enable files that are IN the target profile but currently disabled.
	for (const file of SCAFFOLD_FILES) {
		if (!targetSet.has(file.path)) continue;
		const target = join(basePath, file.path);
		if (!existsSync(target)) continue;
		try {
			const { marker, body } = extractMarker(target);
			if (!marker || marker.state !== "disabled") continue;
			if (bodyHash(body) === marker.hash) {
				// Clean hash → safe to re-enable.
				result.reEnabled.push(file.path);
				if (!dryRun) {
					stampScaffoldFile(target, file.path, sfVersion, "pending");
				}
			} else {
				result.warnings.push({
					path: file.path,
					reason: "disabled, hash diverged — left as-is (manual edit detected)",
				});
			}
		} catch (err) {
			result.warnings.push({
				path: file.path,
				reason: `read error: ${err.message}`,
			});
		}
	}

	// Step 2: disable (or prune) files NOT in the target profile.
	for (const file of SCAFFOLD_FILES) {
		if (targetSet.has(file.path)) continue;
		const target = join(basePath, file.path);
		if (!existsSync(target)) continue;
		try {
			const { marker, body } = extractMarker(target);
			if (!marker) continue; // untracked/customised — leave alone silently
			if (marker.state === "disabled") continue; // already disabled
			if (marker.state === "editing" || marker.state === "completed") {
				result.warnings.push({
					path: file.path,
					reason: `state=${marker.state} — not auto-disabled (review manually)`,
				});
				continue;
			}
			// state=pending — check hash safety using extracted body (without marker).
			const currentHash = bodyHash(body);
			if (currentHash !== marker.hash) {
				// User edited the file but marker still says pending — editing-drift.
				result.warnings.push({
					path: file.path,
					reason:
						"state=pending but hash diverged — not auto-disabled (editing-drift)",
				});
				continue;
			}
			// Safe: pending + clean hash.
			if (prune) {
				result.pruned.push(file.path);
				if (!dryRun) {
					rmSync(target);
				}
			} else {
				result.disabled.push(file.path);
				if (!dryRun) {
					stampScaffoldFile(target, file.path, sfVersion, "disabled");
				}
			}
		} catch (err) {
			result.warnings.push({
				path: file.path,
				reason: `read error: ${err.message}`,
			});
		}
	}

	// Step 3: update manifest profile field.
	if (!dryRun) {
		try {
			writeScaffoldManifest(basePath, { ...manifest, profile: targetProfile });
		} catch (err) {
			result.warnings.push({
				path: "manifest",
				reason: `manifest write failed: ${err.message}`,
			});
		}
	}

	return result;
}

function formatMigrateResult(result, targetProfile, dryRun) {
	const prefix = dryRun ? "[dry-run] " : "";
	const lines = [`${prefix}Migrate to profile '${targetProfile}':`];
	if (result.reEnabled.length > 0) {
		lines.push(`  Re-enabled (${result.reEnabled.length}):`);
		for (const p of result.reEnabled) lines.push(`    + ${p}`);
	}
	if (result.disabled.length > 0) {
		lines.push(`  Disabled (${result.disabled.length}):`);
		for (const p of result.disabled) lines.push(`    - ${p}`);
	}
	if (result.pruned.length > 0) {
		lines.push(`  Pruned/deleted (${result.pruned.length}):`);
		for (const p of result.pruned) lines.push(`    x ${p}`);
	}
	if (result.warnings.length > 0) {
		lines.push(`  Needs review (${result.warnings.length}):`);
		for (const w of result.warnings) lines.push(`    ! ${w.path}: ${w.reason}`);
	}
	if (
		result.reEnabled.length === 0 &&
		result.disabled.length === 0 &&
		result.pruned.length === 0 &&
		result.warnings.length === 0
	) {
		lines.push("  Nothing to do — repo is already on this profile.");
	}
	return lines.join("\n");
}

/**
 * Top-level handler for `/scaffold migrate [args]`.
 *
 * Purpose: apply a profile change to an existing repo, stamping out-of-profile
 * files as `disabled` and restoring in-profile files from `disabled` to
 * `pending`. Safe: never touches user-edited or completed files without warning.
 *
 * Consumer: user, via `/scaffold migrate --profile infra` in the SF TUI.
 */
export async function handleScaffoldMigrate(args, ctx) {
	const opts = parseScaffoldMigrateArgs(args);
	const basePath = projectRoot();

	// If no --profile, auto-detect and report what we'd do.
	let targetProfile = opts.profile;
	if (!targetProfile) {
		targetProfile = detectRepoProfile(basePath);
		ctx.ui.notify(
			`No profile specified — auto-detected: '${targetProfile}'\nRe-run with --profile=${targetProfile} to apply, or --profile=<name> to override.\nBuilt-in profiles: ${PROFILE_NAMES.join(", ")}\nCustom profiles: create ~/.sf/profiles/<name>.yaml (extends + add/remove)`,
			"info",
		);
		return;
	}

	const result = runScaffoldMigrate(basePath, targetProfile, {
		prune: opts.prune,
		dryRun: opts.dryRun,
	});
	ctx.ui.notify(
		formatMigrateResult(result, targetProfile, opts.dryRun),
		"info",
	);

	if (!opts.dryRun) {
		// Run a drift sync to ensure in-profile files that are now pending get written.
		const drift = detectScaffoldDrift(basePath, targetProfile);
		const missingCount = drift.countsByBucket.missing ?? 0;
		if (missingCount > 0) {
			ctx.ui.notify(
				`${missingCount} in-profile file(s) missing — run \`/scaffold sync\` to write them.`,
				"info",
			);
		}
	}
}
