import {
	existsSync,
	mkdirSync,
	readdirSync,
	readFileSync,
	renameSync,
} from "node:fs";
import { join, resolve, sep } from "node:path";
import {
	agentDir as defaultAgentDir,
	sessionsDir as defaultSessionsDir,
	webPreferencesPath as defaultWebPreferencesPath,
} from "./app-paths.js";
import { getProjectSessionsDir } from "./project-sessions.js";
import {
	launchWebMode,
	stopWebMode,
	type WebModeLaunchStatus,
	type WebModeStopOptions,
	type WebModeStopResult,
} from "./web-mode.js";

export interface CliFlags {
	mode?: "text" | "json" | "rpc";
	print?: boolean;
	continue?: boolean;
	noSession?: boolean;
	worktree?: boolean | string;
	model?: string;
	listModels?: string | true;
	discover?: boolean;
	/** `sf --maintain` — refresh model catalogs and audit benchmark coverage, then exit. Idempotent / TTL-checked. Used by the daemon's periodic maintenance timer. */
	maintain?: boolean;
	extensions: string[];
	appendSystemPrompt?: string;
	tools?: string[];
	messages: string[];
	/** Optional project path for server mode: `sf server start <path>` or `sf server <path>` */
	webPath?: string;
	/** Custom host to bind SF server to: `--host 0.0.0.0` */
	webHost?: string;
	/** Custom port for SF server: `--port 8080` */
	webPort?: number;
	/** Additional allowed origins for CORS: `--allowed-origins http://192.168.1.10:8080` */
	webAllowedOrigins?: string[];

	/** Set by `sf sessions` when the user picks a specific session to resume */
	_selectedSessionPath?: string;
	/** `sf sessions --all` — list sessions across all projects */
	allSessions?: boolean;
}

type WritableLike = Pick<typeof process.stderr, "write">;

export interface RunWebCliBranchDeps {
	runWebMode?: typeof launchWebMode;
	stopWebMode?: (
		deps: Parameters<typeof stopWebMode>[0],
		options?: WebModeStopOptions,
	) => WebModeStopResult;
	cwd?: () => string;
	stderr?: WritableLike;
	baseSessionsDir?: string;
	agentDir?: string;
	webPreferencesPath?: string;
}

export function parseCliArgs(argv: string[]): CliFlags {
	const flags: CliFlags = { extensions: [], messages: [] };
	const args = argv.slice(2);
	for (let i = 0; i < args.length; i++) {
		const arg = args[i];
		if (arg === "--mode" && i + 1 < args.length) {
			const mode = args[++i];
			if (mode === "text" || mode === "json" || mode === "rpc")
				flags.mode = mode;
		} else if (arg === "--print" || arg === "-p") {
			flags.print = true;
		} else if (arg === "--continue" || arg === "-c") {
			flags.continue = true;
		} else if (arg === "--no-session") {
			flags.noSession = true;
		} else if (arg === "--all" || arg === "-a") {
			flags.allSessions = true;
		} else if (arg === "--worktree" || arg === "-w") {
			// -w with no value → auto-generate name; -w <name> → use that name
			if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
				flags.worktree = args[++i];
			} else {
				flags.worktree = true;
			}
		} else if (arg === "--host" && i + 1 < args.length) {
			flags.webHost = args[++i];
		} else if (arg === "--port" && i + 1 < args.length) {
			const portStr = args[++i];
			const port = parseInt(portStr, 10);
			if (Number.isFinite(port) && port > 0 && port < 65536) {
				flags.webPort = port;
			}
		} else if (arg === "--allowed-origins" && i + 1 < args.length) {
			const origins = args[++i]
				.split(",")
				.map((o) => o.trim())
				.filter(Boolean);
			flags.webAllowedOrigins = (flags.webAllowedOrigins ?? []).concat(origins);
		} else if (arg === "--model" && i + 1 < args.length) {
			flags.model = args[++i];
		} else if (arg === "--extension" && i + 1 < args.length) {
			flags.extensions.push(args[++i]);
		} else if (arg === "--append-system-prompt" && i + 1 < args.length) {
			flags.appendSystemPrompt = args[++i];
		} else if (arg === "--tools" && i + 1 < args.length) {
			flags.tools = args[++i].split(",");
		} else if (arg === "--list-models") {
			flags.listModels =
				i + 1 < args.length && !args[i + 1].startsWith("-") ? args[++i] : true;
		} else if (arg === "--discover") {
			flags.discover = true;
		} else if (arg === "--maintain") {
			flags.maintain = true;
		} else if (!arg.startsWith("--") && !arg.startsWith("-")) {
			flags.messages.push(arg);
		}
	}
	return flags;
}

export { getProjectSessionsDir } from "./project-sessions.js";

export function migrateLegacyFlatSessions(
	baseSessionsDir: string,
	projectSessionsDir: string,
): void {
	if (!existsSync(baseSessionsDir)) return;

	try {
		const entries = readdirSync(baseSessionsDir);
		const flatJsonl = entries.filter((file) => file.endsWith(".jsonl"));
		if (flatJsonl.length === 0) return;

		mkdirSync(projectSessionsDir, { recursive: true });
		for (const file of flatJsonl) {
			const src = join(baseSessionsDir, file);
			const dst = join(projectSessionsDir, file);
			if (!existsSync(dst)) {
				renameSync(src, dst);
			}
		}
	} catch {
		// Non-fatal — don't block startup if migration fails
	}
}

function emitWebModeFailure(
	stderr: WritableLike,
	status: WebModeLaunchStatus,
): void {
	if (status.ok) return;
	stderr.write(
		`[forge] Web mode launch failed: ${(status as any).failureReason}\n`,
	);
}

/**
 * Resolve the working directory for context-aware launch detection.
 *
 * If the user has configured a dev root via onboarding and their cwd is inside
 * a project under that dev root, return the one-level-deep project directory.
 * Otherwise, return the cwd unchanged (browser picker handles selection).
 *
 * Edge cases handled:
 * - Missing or unreadable prefs file → cwd unchanged
 * - No devRoot field in prefs → cwd unchanged
 * - devRoot path doesn't exist (stale) → cwd unchanged
 * - cwd IS the devRoot → cwd unchanged (picker selects)
 * - cwd outside devRoot → cwd unchanged
 */
export function resolveContextAwareCwd(
	currentCwd: string,
	prefsPath: string,
): string {
	// 1. Read preferences file
	let prefs: Record<string, unknown>;
	try {
		const raw = readFileSync(prefsPath, "utf-8");
		prefs = JSON.parse(raw);
	} catch {
		return currentCwd;
	}

	// 2. Extract devRoot
	const devRoot = prefs.devRoot;
	if (typeof devRoot !== "string" || !devRoot) {
		return currentCwd;
	}

	// 3. Resolve both paths to absolute
	const resolvedCwd = resolve(currentCwd);
	const resolvedDevRoot = resolve(devRoot);

	// 4. Check devRoot still exists
	if (!existsSync(resolvedDevRoot)) {
		return currentCwd;
	}

	// 5. If cwd IS the devRoot → unchanged (picker handles selection)
	if (resolvedCwd === resolvedDevRoot) {
		return currentCwd;
	}

	// 6. If cwd is inside devRoot, extract one-level-deep project directory
	const prefix = resolvedDevRoot + sep;
	if (resolvedCwd.startsWith(prefix)) {
		const relative = resolvedCwd.slice(prefix.length);
		const firstSegment = relative.split(sep)[0];
		if (firstSegment) {
			return join(resolvedDevRoot, firstSegment);
		}
	}

	// 7. cwd outside devRoot → unchanged
	return currentCwd;
}

export type RunWebCliBranchResult =
	| { handled: false }
	| {
			handled: true;
			exitCode: number;
			action: "start";
			status: WebModeLaunchStatus;
			launchInputs: {
				cwd: string;
				projectSessionsDir: string;
				agentDir: string;
			};
	  }
	| {
			handled: true;
			exitCode: number;
			action: "stop";
			stopResult: WebModeStopResult;
	  };

export async function runWebCliBranch(
	flags: CliFlags,
	deps: RunWebCliBranchDeps = {},
): Promise<RunWebCliBranchResult> {
	// Handle `sf server stop [path|--all]` subcommand
	if (flags.messages[0] === "server" && flags.messages[1] === "stop") {
		const stderr = deps.stderr ?? process.stderr;
		const stopArg = flags.messages[2];
		const isAll = stopArg === "all";
		const stopCwd =
			stopArg && !isAll
				? resolve((deps.cwd ?? (() => process.cwd()))(), stopArg)
				: undefined;
		const stopResult = (deps.stopWebMode ?? stopWebMode)(
			{ stderr },
			{
				projectCwd: stopCwd,
				all: isAll,
			},
		);
		return {
			handled: true,
			exitCode: stopResult.ok ? 0 : 1,
			action: "stop",
			stopResult,
		};
	}

	// `sf server [start] [path]` starts the full operator server for one repo.
	// Matches: `sf server`, `sf server start`, `sf server start <path>`, `sf server <path>`
	const isWebSubcommand =
		flags.messages[0] === "server" && flags.messages[1] !== "stop";
	if (!isWebSubcommand) {
		return { handled: false };
	}

	const stderr = deps.stderr ?? process.stderr;
	const defaultCwd = (deps.cwd ?? (() => process.cwd()))();

	// Resolve project path from multiple forms:
	//   sf server start <path>    → messages[2]
	//   sf server <path>          → messages[1] (when not "start")
	let webPath = flags.webPath;
	if (!webPath && isWebSubcommand) {
		if (flags.messages[1] === "start") {
			webPath = flags.messages[2];
		} else if (flags.messages[1]) {
			webPath = flags.messages[1];
		}
	}

	let currentCwd: string;
	if (webPath) {
		currentCwd = resolve(defaultCwd, webPath);
		const checkExists = existsSync;
		if (!checkExists(currentCwd)) {
			stderr.write(`[forge] Project path does not exist: ${currentCwd}\n`);
			return {
				handled: true,
				exitCode: 1,
				action: "start",
				status: {
					mode: "web",
					ok: false,
					cwd: currentCwd,
					projectSessionsDir: "",
					host: "127.0.0.1",
					port: null,
					url: null,
					hostKind: "unresolved",
					hostPath: null,
					hostRoot: null,
					failureReason: `project path does not exist: ${currentCwd}`,
				},
				launchInputs: {
					cwd: currentCwd,
					projectSessionsDir: "",
					agentDir: deps.agentDir ?? defaultAgentDir,
				},
			};
		}
		stderr.write(`[forge] Using project path: ${currentCwd}\n`);
	} else {
		currentCwd = defaultCwd;
	}

	// Context-aware launch: if cwd is inside a project under the configured dev root,
	// resolve to the project directory so the browser opens directly into it
	currentCwd = resolveContextAwareCwd(
		currentCwd,
		deps.webPreferencesPath ?? defaultWebPreferencesPath,
	);

	const baseSessionsDir = deps.baseSessionsDir ?? defaultSessionsDir;
	const agentDir = deps.agentDir ?? defaultAgentDir;
	const projectSessionsDir = getProjectSessionsDir(currentCwd, baseSessionsDir);

	migrateLegacyFlatSessions(baseSessionsDir, projectSessionsDir);
	const status = await (deps.runWebMode ?? launchWebMode)({
		cwd: currentCwd,
		projectSessionsDir,
		agentDir,
		host: flags.webHost,
		port: flags.webPort,
		allowedOrigins: flags.webAllowedOrigins,
	});

	if (!status.ok) {
		emitWebModeFailure(stderr, status);
	}

	return {
		handled: true,
		exitCode: status.ok ? 0 : 1,
		action: "start",
		status,
		launchInputs: {
			cwd: currentCwd,
			projectSessionsDir,
			agentDir,
		},
	};
}
