/**
 * get-secrets-from-user — paged secure env var collection + apply
 *
 * Collects secrets one-per-page via masked TUI input, then writes them
 * to .env (local), Vercel, or Convex. No ctx.callTool, no external deps.
 * Uses Node fs/promises for file I/O and pi.exec() for CLI sinks.
 */
import { existsSync, statSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { Type } from "@sinclair/typebox";
import {
	Editor,
	Key,
	matchesKey,
	Text,
	truncateToWidth,
	wrapTextWithAnsi,
} from "@singularity-forge/tui";
import { formatSecretsManifest, parseSecretsManifest } from "./sf/files.js";
import { resolveMilestoneFile } from "./sf/paths.js";
import { maskEditorLine } from "./shared/mod.js";
import { makeUI } from "./shared/tui.js";

// ─── Helpers ──────────────────────────────────────────────────────────────────
function maskPreview(value) {
	if (!value) return "";
	if (value.length <= 8) return "*".repeat(value.length);
	return `${value.slice(0, 4)}${"*".repeat(Math.max(4, value.length - 8))}${value.slice(-4)}`;
}
function shellEscapeSingle(value) {
	return `'${value.replace(/'/g, `'\\''`)}'`;
}
function isSafeEnvVarKey(key) {
	return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
}
function isSupportedDeploymentEnvironment(env) {
	return env === "development" || env === "preview" || env === "production";
}
function hydrateProcessEnv(key, value) {
	// Make newly collected secrets immediately visible to the current session.
	// Some extensions read process.env directly and do not reload .env on every call.
	process.env[key] = value;
}
async function writeEnvKey(filePath, key, value) {
	if (typeof value !== "string") {
		throw new TypeError(
			`writeEnvKey expects a string value for key "${key}", got ${typeof value}`,
		);
	}
	let content = "";
	try {
		content = await readFile(filePath, "utf8");
	} catch {
		content = "";
	}
	const escaped = value
		.replace(/\\/g, "\\\\")
		.replace(/\n/g, "\\n")
		.replace(/\r/g, "");
	const line = `${key}=${escaped}`;
	const regex = new RegExp(
		`^${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=.*$`,
		"m",
	);
	if (regex.test(content)) {
		content = content.replace(regex, line);
	} else {
		if (content.length > 0 && !content.endsWith("\n")) content += "\n";
		content += `${line}\n`;
	}
	await writeFile(filePath, content, "utf8");
}

// ─── Exported utilities ───────────────────────────────────────────────────────
// Re-export from env-utils.ts so existing consumers still work.
// The implementation lives in env-utils.ts to avoid pulling @singularity-forge/tui
// into modules that only need env-checking (e.g. files.ts during reports).
import { checkExistingEnvKeys } from "./sf/env-utils.js";

export { checkExistingEnvKeys };
/**
 * Detect the write destination based on project files in basePath.
 * Priority: vercel.json → convex/ dir → fallback "dotenv".
 */
export function detectDestination(basePath) {
	if (existsSync(resolve(basePath, "vercel.json"))) {
		return "vercel";
	}
	const convexPath = resolve(basePath, "convex");
	try {
		if (existsSync(convexPath) && statSync(convexPath).isDirectory()) {
			return "convex";
		}
	} catch {
		// stat error — treat as not found
	}
	return "dotenv";
}
// ─── Paged secure input UI ────────────────────────────────────────────────────
/**
 * Show a single-key masked input page via ctx.ui.custom().
 * Returns the entered value, or null if skipped/cancelled.
 */
async function collectOneSecret(
	ctx,
	pageIndex,
	totalPages,
	keyName,
	hint,
	guidance,
) {
	if (!ctx.hasUI) return null;
	const customResult = await ctx.ui.custom((tui, theme, _kb, done) => {
		let value = "";
		let cachedLines;
		const editorTheme = {
			borderColor: (s) => theme.fg("accent", s),
			selectList: {
				selectedPrefix: (t) => theme.fg("accent", t),
				selectedText: (t) => theme.fg("accent", t),
				description: (t) => theme.fg("muted", t),
				scrollInfo: (t) => theme.fg("dim", t),
				noMatch: (t) => theme.fg("warning", t),
			},
		};
		const editor = new Editor(tui, editorTheme, { paddingX: 1 });
		function refresh() {
			cachedLines = undefined;
			tui.requestRender();
		}
		function handleInput(data) {
			if (matchesKey(data, Key.enter)) {
				value = editor.getText().trim();
				done(value.length > 0 ? value : null);
				return;
			}
			if (matchesKey(data, Key.escape)) {
				done(null);
				return;
			}
			// ctrl+s = skip this key
			if (data === "\x13") {
				done(null);
				return;
			}
			editor.handleInput(data);
			refresh();
		}
		function render(width) {
			if (cachedLines) return cachedLines;
			const lines = [];
			const add = (s) => lines.push(truncateToWidth(s, width));
			add(theme.fg("accent", "─".repeat(width)));
			add(
				theme.fg(
					"dim",
					` Page ${pageIndex + 1}/${totalPages} · Secure Env Setup`,
				),
			);
			lines.push("");
			// Key name as big header
			add(theme.fg("accent", theme.bold(` ${keyName}`)));
			if (hint) {
				add(theme.fg("muted", `  ${hint}`));
			}
			// Guidance steps (numbered, dim, wrapped for long URLs)
			if (guidance && guidance.length > 0) {
				lines.push("");
				for (let g = 0; g < guidance.length; g++) {
					const prefix = `  ${g + 1}. `;
					const step = guidance[g];
					const wrappedLines = wrapTextWithAnsi(step, width - 4);
					for (let w = 0; w < wrappedLines.length; w++) {
						const indent = w === 0 ? prefix : " ".repeat(prefix.length);
						lines.push(theme.fg("dim", `${indent}${wrappedLines[w]}`));
					}
				}
			}
			lines.push("");
			// Masked preview
			const raw = editor.getText();
			const preview =
				raw.length > 0
					? maskPreview(raw)
					: theme.fg("dim", "(empty — press enter to skip)");
			add(theme.fg("text", `  Preview: ${preview}`));
			lines.push("");
			// Editor
			add(theme.fg("muted", " Enter value:"));
			for (const line of editor.render(width - 2)) {
				add(theme.fg("text", maskEditorLine(line)));
			}
			lines.push("");
			add(
				theme.fg(
					"dim",
					` enter to confirm  |  ctrl+s or esc to skip  |  esc cancels`,
				),
			);
			add(theme.fg("accent", "─".repeat(width)));
			cachedLines = lines;
			return lines;
		}
		return {
			render,
			invalidate: () => {
				cachedLines = undefined;
			},
			handleInput,
		};
	});
	// RPC/web surfaces may not implement ctx.ui.custom(). Fall back to a
	// standard input prompt so users can still provide the secret.
	if (customResult !== undefined) {
		return customResult;
	}
	if (typeof ctx.ui?.input !== "function") {
		return null;
	}
	const inputTitle = `Secure value for ${keyName} (${pageIndex + 1}/${totalPages})`;
	const inputPlaceholder = hint || "Enter secret value";
	const inputResult = await ctx.ui.input(inputTitle, inputPlaceholder, {
		secure: true,
	});
	if (typeof inputResult !== "string") {
		return null;
	}
	const trimmed = inputResult.trim();
	return trimmed.length > 0 ? trimmed : null;
}
/**
 * Exported wrapper around collectOneSecret for testing.
 * Exposes the same interface with guidance parameter for test verification.
 */
export const collectOneSecretWithGuidance = collectOneSecret;
// ─── Summary Screen ───────────────────────────────────────────────────────────
/**
 * Read-only summary screen showing all manifest entries with status indicators.
 * Follows the confirm-ui.ts pattern: render → any key → done.
 *
 * Status mapping:
 * - collected → done
 * - pending   → pending
 * - skipped   → skipped
 * - existing keys (in existingKeys) → done with "already set" annotation
 */
export async function showSecretsSummary(ctx, entries, existingKeys) {
	if (!ctx.hasUI) return;
	const existingSet = new Set(existingKeys);
	await ctx.ui.custom((_tui, theme, _kb, done) => {
		let cachedLines;
		function handleInput(_data) {
			// Any key dismisses — pass null to satisfy the typed done() callback
			done(null);
		}
		function render(width) {
			if (cachedLines) return cachedLines;
			const ui = makeUI(theme, width);
			const lines = [];
			const push = (...rows) => {
				for (const r of rows) lines.push(...r);
			};
			push(ui.bar());
			push(ui.blank());
			push(ui.header("  Secrets Summary"));
			push(ui.blank());
			for (const entry of entries) {
				let status;
				let detail;
				if (existingSet.has(entry.key)) {
					status = "done";
					detail = "already set";
				} else if (entry.status === "collected") {
					status = "done";
				} else if (entry.status === "skipped") {
					status = "skipped";
				} else {
					status = "pending";
				}
				push(ui.progressItem(entry.key, status, { detail }));
			}
			push(ui.blank());
			push(ui.hints(["any key to continue"]));
			push(ui.bar());
			cachedLines = lines;
			return lines;
		}
		return {
			render,
			invalidate: () => {
				cachedLines = undefined;
			},
			handleInput,
		};
	});
}
// ─── Destination Write Helper ─────────────────────────────────────────────────
/**
 * Apply collected secrets to the target destination.
 * Dotenv writes are handled directly; vercel/convex require pi.exec.
 */
async function applySecrets(provided, destination, opts) {
	const applied = [];
	const errors = [];
	if (destination === "dotenv") {
		for (const { key, value } of provided) {
			try {
				await writeEnvKey(opts.envFilePath, key, value);
				applied.push(key);
				hydrateProcessEnv(key, value);
			} catch (err) {
				errors.push(`${key}: ${err.message}`);
			}
		}
	}
	if ((destination === "vercel" || destination === "convex") && opts.exec) {
		const env = opts.environment ?? "development";
		if (!isSupportedDeploymentEnvironment(env)) {
			errors.push(`environment: unsupported target environment "${env}"`);
			return { applied, errors };
		}
		for (const { key, value } of provided) {
			if (!isSafeEnvVarKey(key)) {
				errors.push(`${key}: invalid environment variable name`);
				continue;
			}
			const cmd =
				destination === "vercel"
					? `printf %s ${shellEscapeSingle(value)} | vercel env add ${key} ${env}`
					: "";
			try {
				const result =
					destination === "vercel"
						? await opts.exec("sh", ["-c", cmd])
						: await opts.exec("npx", ["convex", "env", "set", key, value]);
				if (result.code !== 0) {
					errors.push(`${key}: ${result.stderr.slice(0, 200)}`);
				} else {
					applied.push(key);
					hydrateProcessEnv(key, value);
				}
			} catch (err) {
				errors.push(`${key}: ${err.message}`);
			}
		}
	}
	return { applied, errors };
}
// ─── Manifest Orchestrator ────────────────────────────────────────────────────
/**
 * Full orchestrator: reads manifest, checks env, shows summary, collects
 * only pending keys (with guidance + hint), updates manifest statuses,
 * writes back, and applies collected values to the destination.
 *
 * Returns a structured result matching the tool result shape.
 */
export async function collectSecretsFromManifest(base, milestoneId, ctx) {
	// (a) Resolve manifest path
	const manifestPath = resolveMilestoneFile(base, milestoneId, "SECRETS");
	if (!manifestPath) {
		throw new Error(
			`Secrets manifest not found for milestone ${milestoneId} in ${base}`,
		);
	}
	// (b) Read and parse manifest
	const content = await readFile(manifestPath, "utf8");
	const manifest = parseSecretsManifest(content);
	// (c) Check existing keys
	const envPath = resolve(base, ".env");
	const allKeys = manifest.entries.map((e) => e.key);
	const existingKeys = await checkExistingEnvKeys(allKeys, envPath);
	const existingSet = new Set(existingKeys);
	// (d) Build categorization
	const existingSkipped = [];
	const alreadySkipped = [];
	const pendingEntries = [];
	for (const entry of manifest.entries) {
		if (existingSet.has(entry.key)) {
			existingSkipped.push(entry.key);
		} else if (entry.status === "skipped") {
			alreadySkipped.push(entry.key);
		} else if (entry.status === "pending") {
			pendingEntries.push(entry);
		}
		// collected entries that are not in env are left as-is
	}
	// (e) Show summary screen
	await showSecretsSummary(ctx, manifest.entries, existingKeys);
	// (f) Detect destination
	const destination = detectDestination(ctx.cwd);
	// (g) Collect only pending keys that are not already existing
	const collected = [];
	for (let i = 0; i < pendingEntries.length; i++) {
		const entry = pendingEntries[i];
		const value = await collectOneSecret(
			ctx,
			i,
			pendingEntries.length,
			entry.key,
			entry.formatHint || undefined,
			entry.guidance.length > 0 ? entry.guidance : undefined,
		);
		collected.push({ key: entry.key, value });
	}
	// (h) Update manifest entry statuses
	for (const { key, value } of collected) {
		const entry = manifest.entries.find((e) => e.key === key);
		if (entry) {
			entry.status = value != null ? "collected" : "skipped";
		}
	}
	// (i) Write manifest back to disk
	await writeFile(manifestPath, formatSecretsManifest(manifest), "utf8");
	// (j) Apply collected values to destination
	const provided = collected.filter((c) => c.value != null);
	const { applied } = await applySecrets(provided, destination, {
		envFilePath: resolve(ctx.cwd, ".env"),
	});
	const skipped = [
		...alreadySkipped,
		...collected.filter((c) => c.value == null).map((c) => c.key),
	];
	return { applied, skipped, existingSkipped };
}
// ─── Extension ────────────────────────────────────────────────────────────────
export default function secureEnv(pi) {
	pi.registerTool({
		name: "secure_env_collect",
		label: "Secure Env Collect",
		description:
			"Collect one or more env vars through a paged masked-input UI, then write them to .env, Vercel, or Convex. " +
			"Values are shown masked to the user (e.g. sk-ir***dgdh) and never echoed in tool output.",
		promptSnippet:
			"Collect and apply env vars securely without asking user to edit files manually.",
		promptGuidelines: [
			"NEVER ask the user to manually edit .env files, copy-paste into a terminal, or open a dashboard to set env vars. Always use secure_env_collect instead.",
			"When a command fails due to a missing env var (e.g. 'OPENAI_API_KEY is not set', 'Missing required environment variable', 'Invalid API key', 'authentication required'), immediately call secure_env_collect with the missing keys before retrying.",
			"When starting a new project or running setup steps that require secrets (API keys, tokens, database URLs), proactively call secure_env_collect before the first command that needs them.",
			"Detect the right destination: use 'dotenv' for local dev, 'vercel' when deploying to Vercel, 'convex' when using Convex backend.",
			"After secure_env_collect completes, re-run the originally blocked command to verify the fix worked.",
			"Never echo, log, or repeat secret values in your responses. Only report key names and applied/skipped status.",
		],
		parameters: Type.Object({
			destination: Type.Optional(
				Type.Union(
					[
						Type.Literal("dotenv"),
						Type.Literal("vercel"),
						Type.Literal("convex"),
					],
					{ description: "Where to write the collected secrets" },
				),
			),
			keys: Type.Array(
				Type.Object({
					key: Type.String({
						description: "Env var name, e.g. OPENAI_API_KEY",
					}),
					hint: Type.Optional(
						Type.String({
							description: "Format hint shown to user, e.g. 'starts with sk-'",
						}),
					),
					required: Type.Optional(Type.Boolean()),
					guidance: Type.Optional(
						Type.Array(Type.String(), {
							description: "Step-by-step guidance for finding this key",
						}),
					),
				}),
				{ minItems: 1 },
			),
			envFilePath: Type.Optional(
				Type.String({
					description:
						"Path to .env file (dotenv only). Defaults to .env in cwd.",
				}),
			),
			environment: Type.Optional(
				Type.Union(
					[
						Type.Literal("development"),
						Type.Literal("preview"),
						Type.Literal("production"),
					],
					{ description: "Target environment (vercel only)" },
				),
			),
		}),
		async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
			if (!ctx.hasUI) {
				return {
					content: [
						{
							type: "text",
							text: "Error: UI not available (interactive mode required for secure env collection).",
						},
					],
					isError: true,
					details: undefined,
				};
			}
			// Auto-detect destination when not provided
			const destinationAutoDetected = params.destination == null;
			const destination = params.destination ?? detectDestination(ctx.cwd);
			const collected = [];
			// Collect one key per page
			for (let i = 0; i < params.keys.length; i++) {
				const item = params.keys[i];
				const value = await collectOneSecret(
					ctx,
					i,
					params.keys.length,
					item.key,
					item.hint,
					item.guidance,
				);
				collected.push({ key: item.key, value });
			}
			const provided = collected.filter((c) => c.value != null);
			const skipped = collected
				.filter((c) => c.value == null)
				.map((c) => c.key);
			// Apply to destination via shared helper
			const { applied, errors } = await applySecrets(provided, destination, {
				envFilePath: resolve(ctx.cwd, params.envFilePath ?? ".env"),
				environment: params.environment,
				exec: (cmd, args) => pi.exec(cmd, args),
			});
			const details = {
				destination,
				environment: params.environment,
				applied,
				skipped,
				...(destinationAutoDetected
					? { detectedDestination: destination }
					: {}),
			};
			const lines = [
				`destination: ${destination}${destinationAutoDetected ? " (auto-detected)" : ""}${params.environment ? ` (${params.environment})` : ""}`,
				...applied.map((k) => `✓ ${k}: applied`),
				...skipped.map((k) => `• ${k}: skipped`),
				...errors.map((e) => `✗ ${e}`),
			];
			return {
				content: [{ type: "text", text: lines.join("\n") }],
				details,
				isError: errors.length > 0 && applied.length === 0,
			};
		},
		renderCall(args, theme) {
			const count = Array.isArray(args.keys) ? args.keys.length : 0;
			return new Text(
				theme.fg("toolTitle", theme.bold("secure_env_collect ")) +
					theme.fg("muted", `→ ${args.destination ?? "auto"}`) +
					theme.fg("dim", `  ${count} key${count !== 1 ? "s" : ""}`),
				0,
				0,
			);
		},
		renderResult(result, _options, theme) {
			const details = result.details;
			if (!details) {
				const t = result.content[0];
				return new Text(t?.type === "text" ? t.text : "", 0, 0);
			}
			const lines = [
				`${theme.fg("success", "✓")} ${details.destination}${details.environment ? ` (${details.environment})` : ""}`,
				...details.applied.map(
					(k) => `  ${theme.fg("success", "✓")} ${k}: applied`,
				),
				...details.skipped.map(
					(k) => `  ${theme.fg("warning", "•")} ${k}: skipped`,
				),
			];
			return new Text(lines.join("\n"), 0, 0);
		},
	});
}
