/**
 * commands-plan.js — sf plan promote / list / diff
 *
 * Purpose: Provide the canonical path for moving planning artifacts from
 * ~/.sf/projects/<hash>/ into the repo (promote), plus visibility (list)
 * and comparison (diff) companions.
 *
 * Consumer: SF ops handler (commands/handlers/ops.js) via `/plan <subcmd>`.
 */

import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
	copyFileSync,
	existsSync,
	mkdirSync,
	readdirSync,
	readFileSync,
	statSync,
	writeFileSync,
} from "node:fs";
import {
	basename,
	dirname,
	extname,
	isAbsolute,
	join,
	relative,
	resolve,
} from "node:path";
import { projectRoot } from "./commands/context.js";
import { repoIdentity } from "./repo-identity.js";
import { sfHome } from "./sf-home.js";
import { PROMOTED_SPEC_PROJECTIONS } from "./spec-projections.js";

// ─── Shared helpers ─────────────────────────────────────────────────────────

function resolveExternalSfRoot() {
	const root = projectRoot();
	const id = repoIdentity(root);
	return join(sfHome(), "projects", id);
}

function resolveSourcePath(source) {
	if (isAbsolute(source)) {
		return resolve(source);
	}
	const external = resolveExternalSfRoot();
	return resolve(external, source);
}

function slugify(name) {
	return name
		.toLowerCase()
		.replace(/[^a-z0-9]+/g, "-")
		.replace(/^-|-$/g, "");
}

function sha256File(path) {
	const data = readFileSync(path);
	return createHash("sha256").update(data).digest("hex");
}

function _sha256String(data) {
	return createHash("sha256").update(data).digest("hex");
}

function humanSize(bytes) {
	if (bytes < 1024) return `${bytes} B`;
	if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
	return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

function relativeTime(date) {
	const now = Date.now();
	const diff = now - date.getTime();
	const seconds = Math.floor(diff / 1000);
	if (seconds < 60) return "just now";
	const minutes = Math.floor(seconds / 60);
	if (minutes < 60) return `${minutes}m ago`;
	const hours = Math.floor(minutes / 60);
	if (hours < 24) return `${hours}h ago`;
	const days = Math.floor(hours / 24);
	if (days < 7) return `${days}d ago`;
	return date.toISOString().slice(0, 10);
}

function* walkDir(dir) {
	const entries = readdirSync(dir, { withFileTypes: true });
	for (const entry of entries) {
		const path = join(dir, entry.name);
		if (entry.isDirectory()) {
			yield* walkDir(path);
		} else if (entry.isFile()) {
			yield path;
		}
	}
}

function collectPromotedFiles() {
	const dirs = ["docs/plans", "docs/adr", "docs/specs"];
	const files = [];
	for (const dir of dirs) {
		if (!existsSync(dir)) continue;
		for (const entry of readdirSync(dir, { withFileTypes: true })) {
			if (entry.isFile()) {
				files.push(join(dir, entry.name));
			}
		}
	}
	return files;
}

function findPromotedByHash(sourceHash) {
	for (const path of collectPromotedFiles()) {
		try {
			if (sha256File(path) === sourceHash) {
				return path;
			}
		} catch {
			/* skip unreadable */
		}
	}
	return null;
}

function findPromotedByBasename(basename) {
	for (const path of collectPromotedFiles()) {
		if (path.endsWith(basename)) {
			return path;
		}
	}
	return null;
}

function nextAdrNumber() {
	const dir = "docs/adr";
	if (!existsSync(dir)) return 1;
	let max = 0;
	for (const entry of readdirSync(dir, { withFileTypes: true })) {
		if (!entry.isFile()) continue;
		const m = entry.name.match(/^(\d{4})-/);
		if (m) {
			const n = parseInt(m[1], 10);
			if (n > max) max = n;
		}
	}
	return max + 1;
}

function _formatDiffLine(line) {
	if (line.startsWith("+")) return line; // keep color codes if present
	if (line.startsWith("-")) return line;
	if (line.startsWith("@@")) return line;
	return ` ${line}`;
}

function unifiedDiffStrings(labelA, contentA, labelB, contentB) {
	if (contentA === contentB) return "";
	const aLines = contentA.split("\n");
	const bLines = contentB.split("\n");
	const lines = [`--- ${labelA}`, `+++ ${labelB}`];
	const max = Math.max(aLines.length, bLines.length);
	for (let i = 0; i < max; i++) {
		const a = aLines[i];
		const b = bLines[i];
		if (a === b) continue;
		lines.push(`@@ line ${i + 1} @@`);
		if (a !== undefined) lines.push(`-${a}`);
		if (b !== undefined) lines.push(`+${b}`);
	}
	return `${lines.join("\n")}\n`;
}

// ─── Subcommand: promote ────────────────────────────────────────────────────

export async function handlePlanPromote(args, ctx) {
	const parts = args.trim().split(/\s+/);

	let source = "";
	let toDir = "docs/plans";
	let rename = null;
	let edit = false;

	// Parse flags
	for (let i = 0; i < parts.length; i++) {
		const p = parts[i];
		if (p === "--to" || p === "-t") {
			toDir = parts[++i];
		} else if (p === "--rename" || p === "-r") {
			rename = parts[++i];
		} else if (p === "--edit" || p === "-e") {
			edit = true;
		} else if (!source) {
			source = p;
		}
	}

	if (!source) {
		ctx.ui.notify(
			"Usage: /plan promote <source> [--to <dir>] [--rename <name>] [--edit]",
			"warning",
		);
		return;
	}

	const sourcePath = resolveSourcePath(source);
	if (!existsSync(sourcePath)) {
		ctx.ui.notify(`Source not found: ${sourcePath}`, "error");
		return;
	}

	// Determine target filename
	let targetName;
	if (rename) {
		targetName = rename;
	} else if (toDir === "docs/adr" || toDir === "docs/adr/") {
		const slug = slugify(basename(source, extname(source)));
		const num = nextAdrNumber();
		targetName = `${String(num).padStart(4, "0")}-${slug}.md`;
	} else {
		targetName = basename(sourcePath);
	}

	const targetPath = join(toDir, targetName);

	// Ensure target directory exists
	mkdirSync(dirname(targetPath), { recursive: true });

	// Copy file
	copyFileSync(sourcePath, targetPath);

	// --edit flag
	if (edit) {
		const editor = process.env.EDITOR;
		if (editor) {
			spawnSync(editor, [targetPath], { stdio: "inherit" });
		} else {
			ctx.ui.notify("Warning: $EDITOR is not set. Skipping edit.", "warning");
		}
	}

	ctx.ui.notify(
		`Promoted: ${relative(process.cwd(), sourcePath)} → ${targetPath}`,
		"info",
	);
	ctx.ui.notify(`Suggested: git add ${targetPath}`, "info");
}

// ─── Subcommand: list ───────────────────────────────────────────────────────

export async function handlePlanList(_args, ctx) {
	const external = resolveExternalSfRoot();
	if (!existsSync(external)) {
		ctx.ui.notify("No external SF state found.", "info");
		return;
	}

	const promotedHashes = new Map();
	for (const path of collectPromotedFiles()) {
		try {
			promotedHashes.set(path, sha256File(path));
		} catch {
			/* skip unreadable */
		}
	}

	const rows = [];
	for (const path of walkDir(external)) {
		const rel = relative(external, path);
		const stat = statSync(path);
		const size = humanSize(stat.size);
		const modified = relativeTime(stat.mtime);
		const hash = sha256File(path);

		let promoted = "";
		for (const [pPath, pHash] of promotedHashes) {
			if (pHash === hash) {
				promoted = `✓ ${pPath}`;
				break;
			}
		}

		rows.push({ rel, size, modified, promoted });
	}

	// Sort by mtime desc
	rows.sort((_a, _b) => {
		// We don't have mtime in the row object, re-stat or use original order
		// For simplicity, keep walk order (not guaranteed sorted)
		return 0;
	});

	if (rows.length === 0) {
		ctx.ui.notify("No planning artifacts found.", "info");
		return;
	}

	const maxRel = Math.max(...rows.map((r) => r.rel.length), 4);
	const maxSize = Math.max(...rows.map((r) => r.size.length), 4);
	const maxMod = Math.max(...rows.map((r) => r.modified.length), 8);

	const lines = [
		`${"PATH".padEnd(maxRel)}  ${"SIZE".padEnd(maxSize)}  ${"MODIFIED".padEnd(maxMod)}  PROMOTED`,
	];
	for (const r of rows) {
		lines.push(
			`${r.rel.padEnd(maxRel)}  ${r.size.padEnd(maxSize)}  ${r.modified.padEnd(maxMod)}  ${r.promoted}`,
		);
	}

	ctx.ui.notify(lines.join("\n"), "info");
}

// ─── Subcommand: diff ───────────────────────────────────────────────────────

export async function handlePlanDiff(args, ctx) {
	const source = args.trim();
	if (!source) {
		ctx.ui.notify("Usage: /plan diff <source>", "warning");
		return;
	}

	const sourcePath = resolveSourcePath(source);
	if (!existsSync(sourcePath)) {
		ctx.ui.notify(`Source not found: ${sourcePath}`, "error");
		return;
	}

	// Find promoted version: by basename first, then by content hash
	let promotedPath = findPromotedByBasename(basename(sourcePath));
	if (!promotedPath) {
		const sourceHash = sha256File(sourcePath);
		promotedPath = findPromotedByHash(sourceHash);
	}

	if (!promotedPath) {
		ctx.ui.notify(`${source} has not been promoted yet.`, "info");
		return;
	}

	// Compare content
	const sourceContent = readFileSync(sourcePath, "utf-8");
	const promotedContent = readFileSync(promotedPath, "utf-8");

	if (sourceContent === promotedContent) {
		ctx.ui.notify(`${source} is up to date with ${promotedPath}.`, "info");
		return;
	}

	// Use git diff for unified diff output
	const result = spawnSync(
		"git",
		["diff", "--no-index", "--", sourcePath, promotedPath],
		{
			encoding: "utf-8",
			stdio: ["pipe", "pipe", "pipe"],
		},
	);

	// git diff --no-index exits 1 when files differ (expected)
	const output = result.stdout || result.stderr || "";
	// Strip the "diff --git" prefix lines that include absolute paths
	const lines = output.split("\n");
	const filtered = [];
	const _skipNext = false;
	for (const line of lines) {
		if (line.startsWith("diff --git")) {
			filtered.push(`--- a/${relative(process.cwd(), sourcePath)}`);
			filtered.push(`+++ b/${promotedPath}`);
			continue;
		}
		if (line.startsWith("--- ") || line.startsWith("+++ ")) {
			continue;
		}
		if (line.startsWith("index ")) continue;
		filtered.push(line);
	}

	ctx.ui.notify(filtered.join("\n"), "info");
}

// ─── Subcommand: specs ──────────────────────────────────────────────────────

/**
 * Generate, diff, or check docs/specs human exports.
 *
 * Purpose: make docs/specs reproducible human exports from SF's DB-first
 * working model instead of unmanaged snapshots.
 *
 * Consumer: handlePlan router for `/plan specs ...`.
 */
export async function handlePlanSpecs(args, ctx) {
	const subcmd = args.trim() || "diff";
	const root = projectRoot();
	const dbPath = join(root, ".sf", "sf.db");
	if (!existsSync(dbPath)) {
		ctx.ui.notify(
			"Cannot project specs: .sf/sf.db is missing. Initialize or recover SF state first.",
			"error",
		);
		return { ok: false, changed: [] };
	}

	const changed = [];
	const diffs = [];
	for (const projection of PROMOTED_SPEC_PROJECTIONS) {
		const targetPath = join(root, projection.path);
		const generated = projection.generate(root);
		const existing = existsSync(targetPath)
			? readFileSync(targetPath, "utf-8")
			: "";
		if (existing !== generated) {
			changed.push(projection.path);
			if (subcmd === "generate" || subcmd === "write") {
				mkdirSync(dirname(targetPath), { recursive: true });
				writeFileSync(targetPath, generated);
			} else {
				diffs.push(
					unifiedDiffStrings(
						projection.path,
						existing,
						`${projection.path} (generated)`,
						generated,
					),
				);
			}
		}
	}

	if (subcmd === "generate" || subcmd === "write") {
		ctx.ui.notify(
			changed.length === 0
				? "Spec exports already match generated projections."
				: `Generated spec exports:\n${changed.map((p) => `  ${p}`).join("\n")}`,
			"info",
		);
		return { ok: true, changed };
	}

	if (subcmd === "check") {
		if (changed.length === 0) {
			ctx.ui.notify("Spec exports match generated projections.", "info");
			return { ok: true, changed };
		}
		ctx.ui.notify(
			`Spec exports are stale:\n${changed.map((p) => `  ${p}`).join("\n")}\nRun /plan specs generate.`,
			"error",
		);
		return { ok: false, changed };
	}

	if (subcmd === "diff") {
		ctx.ui.notify(
			changed.length === 0
				? "Spec exports match generated projections."
				: diffs.join("\n"),
			changed.length === 0 ? "info" : "warning",
		);
		return { ok: changed.length === 0, changed };
	}

	ctx.ui.notify("Usage: /plan specs generate|diff|check", "warning");
	return { ok: false, changed: [] };
}

// ─── Top-level router ───────────────────────────────────────────────────────

export async function handlePlan(args, ctx) {
	const trimmed = args.trim();
	if (trimmed === "specs" || trimmed.startsWith("specs ")) {
		await handlePlanSpecs(trimmed.replace(/^specs\s*/, ""), ctx);
		return true;
	}
	if (trimmed.startsWith("promote ") || trimmed === "promote") {
		await handlePlanPromote(trimmed.replace(/^promote\s*/, ""), ctx);
		return true;
	}
	if (trimmed === "list") {
		await handlePlanList("", ctx);
		return true;
	}
	if (trimmed.startsWith("diff ") || trimmed === "diff") {
		await handlePlanDiff(trimmed.replace(/^diff\s*/, ""), ctx);
		return true;
	}
	if (trimmed === "") {
		ctx.ui.notify("Usage: /plan promote|list|diff|specs ...", "info");
		return true;
	}
	return false;
}
