import { existsSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { spawnSync } from "node:child_process";
import { createWorktreePool, type WorktreePool } from "./generated/worktree-pool.js";
import { fileURLToPath } from "node:url";
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import {
	prepareLocalReviewDiff,
	reviewRuntime,
	detectManagedVcs,
	getVcsContext,
	getVcsDiffFingerprint,
	getVcsFileContentsForDiff,
	canStageFiles,
	runVcsDiff,
	stageFile,
	startAnnotateServer,
	startPlanReviewServer,
	startReviewServer,
	type DiffType,
	type VcsSelection,
	unstageFile,
} from "./server.js";
import { openBrowser, isRemoteSession } from "./server/network.js";
import { parsePRUrl, checkPRAuth, fetchPR } from "./server/pr.js";
import {
	getMRLabel,
	getMRNumberLabel,
	getDisplayRepo,
	getCliName,
	getCliInstallUrl,
} from "./generated/pr-provider.js";
import { parseRemoteUrl } from "./generated/repo.js";
import { fetchRef, createWorktree, removeWorktree, ensureObjectAvailable } from "./generated/worktree.js";
import { loadConfig, resolveDefaultDiffType, resolveJjDefaultDiffType } from "./generated/config.js";
import {
	WorkspaceReviewSession,
	type WorkspaceDiffType,
} from "./generated/review-workspace.js";
export { getLastAssistantMessageText } from "./assistant-message.js";

export type AnnotateMode = "annotate" | "annotate-folder" | "annotate-last";
export interface PlanReviewDecision {
	approved: boolean;
	feedback?: string;
	savedPath?: string;
	agentSwitch?: string;
	permissionMode?: string;
	approvalSession?: "current" | "fresh";
}

export interface BrowserDecisionSession<T> {
	url: string;
	waitForDecision: () => Promise<T>;
	stop: () => void;
}

export interface PlanReviewBrowserSession extends BrowserDecisionSession<PlanReviewDecision> {
	reviewId: string;
	onDecision: (listener: (result: PlanReviewDecision) => void | Promise<void>) => () => void;
}

const __dirname = dirname(fileURLToPath(import.meta.url));
let planHtmlContent = "";
let reviewHtmlContent = "";

try {
	planHtmlContent = readFileSync(resolve(__dirname, "plannotator.html"), "utf-8");
} catch {
	// built assets unavailable
}

try {
	reviewHtmlContent = readFileSync(resolve(__dirname, "review-editor.html"), "utf-8");
} catch {
	// built assets unavailable
}

function delay(ms: number): Promise<void> {
	return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
}

export function hasPlanBrowserHtml(): boolean {
	return Boolean(planHtmlContent);
}

export function hasReviewBrowserHtml(): boolean {
	return Boolean(reviewHtmlContent);
}

export function getStartupErrorMessage(err: unknown): string {
	return err instanceof Error ? err.message : "Unknown error";
}

async function openBrowserForServer(serverUrl: string, ctx: ExtensionContext): Promise<void> {
	const browserResult = await openBrowser(serverUrl);
	if (isRemoteSession()) {
		ctx.ui.notify(`[Plannotator] ${serverUrl}`, "info");
	} else if (!browserResult.opened) {
		ctx.ui.notify(`Open this URL to review: ${serverUrl}`, "info");
	}
}

async function buildLocalWorkspaceReview(
	root: string,
	options: { requestedDiffType?: DiffType | WorkspaceDiffType; configuredDiffType?: DiffType; configuredJjDiffType?: DiffType; hideWhitespace?: boolean } = {},
): Promise<WorkspaceReviewSession> {
	return WorkspaceReviewSession.create({
		getVcsContext,
		runVcsDiff,
		getVcsFileContentsForDiff,
		getVcsDiffFingerprint,
		canStageFiles,
		stageFile,
		unstageFile,
	}, root, options);
}

async function openBrowserAndWait<T>(
	server: { url: string; stop: () => void },
	ctx: ExtensionContext,
	waitForResult: () => Promise<T>,
): Promise<T> {
	await openBrowserForServer(server.url, ctx);
	return waitForDecisionWithCleanup(server, waitForResult);
}

async function waitForDecisionWithCleanup<T>(
	server: { url: string; stop: () => void },
	waitForResult: () => Promise<T>,
): Promise<T> {
	try {
		const result = await waitForResult();
		await delay(1500);
		return result;
	} finally {
		server.stop();
	}
}

function startBrowserDecisionSession<T>(
	server: { url: string; stop: () => void },
	ctx: ExtensionContext,
	waitForResult: () => Promise<T>,
): BrowserDecisionSession<T> {
	openBrowserForServer(server.url, ctx);
	let stopped = false;
	let stopReject: ((err: Error) => void) | undefined;
	let decisionPromise: Promise<T> | undefined;
	const createStoppedError = () => new Error("Plannotator browser session was stopped.");
	const stop = () => {
		if (stopped) return;
		stopped = true;
		server.stop();
		stopReject?.(createStoppedError());
		stopReject = undefined;
	};

	return {
		url: server.url,
		waitForDecision: () => {
			if (decisionPromise) return decisionPromise;
			if (stopped) return Promise.reject(createStoppedError());
			decisionPromise = (async () => {
				const stoppedPromise = new Promise<never>((_, reject) => {
					stopReject = reject;
				});
				try {
					const result = await Promise.race([waitForResult(), stoppedPromise]);
					stopReject = undefined;
					await delay(1500);
					return result;
				} finally {
					stop();
				}
			})();
			return decisionPromise;
		},
		stop,
	};
}

export async function startPlanReviewBrowserSession(
	ctx: ExtensionContext,
	planContent: string,
): Promise<PlanReviewBrowserSession> {
	if (!ctx.hasUI || !planHtmlContent) {
		throw new Error("Plannotator browser review is unavailable in this session.");
	}

	const server = await startPlanReviewServer({
		plan: planContent,
		htmlContent: planHtmlContent,
		origin: "pi",
		sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
		shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,
		pasteApiUrl: process.env.PLANNOTATOR_PASTE_URL || undefined,
	});

	const session = startBrowserDecisionSession(server, ctx, server.waitForDecision);
	server.onDecision(() => {
		setTimeout(() => session.stop(), 1500);
	});

	return {
		...session,
		reviewId: server.reviewId,
		onDecision: server.onDecision,
	};
}

export async function openPlanReviewBrowser(
	ctx: ExtensionContext,
	planContent: string,
): Promise<PlanReviewDecision> {
	const session = await startPlanReviewBrowserSession(ctx, planContent);
	return session.waitForDecision();
}

export function shouldUseLocalPrCheckout(options: { useLocal?: boolean; vcsType?: VcsSelection; detectedVcsType?: VcsSelection }): boolean {
	if (options.useLocal === false) return false;
	if (options.detectedVcsType === "jj" && options.vcsType !== "git") return false;
	return true;
}

export async function openCodeReview(
	ctx: ExtensionContext,
	options: { cwd?: string; defaultBranch?: string; diffType?: DiffType; prUrl?: string; vcsType?: VcsSelection; useLocal?: boolean; jjDefaultDiffType?: DiffType } = {},
): Promise<{ approved: boolean; feedback?: string; annotations?: unknown[]; agentSwitch?: string; exit?: boolean }> {
	const session = await startCodeReviewBrowserSession(ctx, options);
	return session.waitForDecision();
}

export async function startCodeReviewBrowserSession(
	ctx: ExtensionContext,
	options: { cwd?: string; defaultBranch?: string; diffType?: DiffType; prUrl?: string; vcsType?: VcsSelection; useLocal?: boolean; jjDefaultDiffType?: DiffType } = {},
): Promise<
	BrowserDecisionSession<{
		approved: boolean;
		feedback?: string;
		annotations?: unknown[];
		agentSwitch?: string;
		exit?: boolean;
	}>
> {
	if (!ctx.hasUI || !reviewHtmlContent) {
		throw new Error("Plannotator code review browser is unavailable in this session.");
	}

	const urlArg = options.prUrl;
	const isPRMode = urlArg?.startsWith("http://") || urlArg?.startsWith("https://");

	let rawPatch: string;
	let gitRef: string;
	let diffError: string | undefined;
	let gitCtx: Awaited<ReturnType<typeof prepareLocalReviewDiff>>["gitContext"] | undefined;
	let prMetadata: Awaited<ReturnType<typeof fetchPR>>["metadata"] | undefined;
	let prPatchIncomplete = false;
	let diffType: DiffType | WorkspaceDiffType | undefined;
	let agentCwd: string | undefined;
	let initialBase: string | undefined;
	let worktreeCleanup: (() => void | Promise<void>) | undefined;
	let worktreePool: WorktreePool | undefined;
	let exitHandler: (() => void) | undefined;
	let workspace: WorkspaceReviewSession | undefined;

	if (isPRMode && urlArg) {
		// --- PR Review Mode ---
		const prRef = parsePRUrl(urlArg);
		if (!prRef) {
			throw new Error(
				`Invalid PR/MR URL: ${urlArg}\n` +
				"Supported formats:\n" +
				"  GitHub: https://github.com/owner/repo/pull/123\n" +
				"  GitLab: https://gitlab.com/group/project/-/merge_requests/42",
			);
		}

		const cliName = getCliName(prRef);
		const cliUrl = getCliInstallUrl(prRef);

		try {
			await checkPRAuth(prRef);
		} catch (err) {
			const msg = err instanceof Error ? err.message : String(err);
			if (msg.includes("not found") || msg.includes("ENOENT")) {
				throw new Error(`${cliName === "gh" ? "GitHub" : "GitLab"} CLI (${cliName}) is not installed. Install it from ${cliUrl}`);
			}
			throw err;
		}

		console.error(`Fetching ${getMRLabel(prRef)} ${getMRNumberLabel(prRef)} from ${getDisplayRepo(prRef)}...`);
		const pr = await fetchPR(prRef);
		rawPatch = pr.rawPatch;
		gitRef = `${getMRLabel(prRef)} ${getMRNumberLabel(prRef)}`;
		prMetadata = pr.metadata;
		prPatchIncomplete = pr.patchIncomplete ?? false;

		const repoDir = options.cwd ?? ctx.cwd;
		const detectedPrVcs = await detectManagedVcs(repoDir, "auto").catch(() => null);
		const detectedVcsType = detectedPrVcs?.id === "git" || detectedPrVcs?.id === "jj" || detectedPrVcs?.id === "p4"
			? detectedPrVcs.id
			: undefined;
		if (shouldUseLocalPrCheckout({ ...options, detectedVcsType })) {
			// Create local worktree for agent file access. In JJ repos this intentionally
			// requires --git because it uses Git checkout/worktree behavior.
			let localPath: string | undefined;
			let sessionDir: string | undefined;
			try {
				const identifier = prMetadata.platform === "github"
					? `${prMetadata.owner}-${prMetadata.repo}-${prMetadata.number}`
					: `${prMetadata.projectPath.replace(/\//g, "-")}-${prMetadata.iid}`;
				const suffix = Math.random().toString(36).slice(2, 8);
				const prNumber = prMetadata.platform === "github" ? prMetadata.number : prMetadata.iid;
				sessionDir = join(realpathSync(tmpdir()), `plannotator-pr-${identifier}-${suffix}`);
				localPath = join(sessionDir, "pool", `pr-${prNumber}`);
				const fetchRefStr = prMetadata.platform === "github"
					? `refs/pull/${prMetadata.number}/head`
					: `refs/merge-requests/${prMetadata.iid}/head`;

				// Validate inputs from platform API to prevent git flag/path injection
				if (prMetadata.baseBranch.includes('..') || prMetadata.baseBranch.startsWith('-')) throw new Error(`Invalid base branch: ${prMetadata.baseBranch}`);
				if (!/^[0-9a-f]{40,64}$/i.test(prMetadata.baseSha)) throw new Error(`Invalid base SHA: ${prMetadata.baseSha}`);

				// Detect same-repo vs cross-repo (must match both owner/repo AND host)
				let isSameRepo = false;
				try {
					const remoteResult = await reviewRuntime.runGit(["remote", "get-url", "origin"], { cwd: repoDir });
					if (remoteResult.exitCode === 0) {
						const remoteUrl = remoteResult.stdout.trim();
						const currentRepo = parseRemoteUrl(remoteUrl);
						const prRepo = prMetadata.platform === "github"
							? `${prMetadata.owner}/${prMetadata.repo}`
							: prMetadata.projectPath;
						const repoMatches = !!currentRepo && currentRepo.toLowerCase() === prRepo.toLowerCase();
						const sshHost = remoteUrl.match(/^[^@]+@([^:]+):/)?.[1];
						const httpsHost = (() => { try { return new URL(remoteUrl).hostname; } catch { return null; } })();
						const remoteHost = (sshHost || httpsHost || "").toLowerCase();
						const prHost = prMetadata.host.toLowerCase();
						isSameRepo = repoMatches && remoteHost === prHost;
					}
				} catch { /* not in a git repo — cross-repo path */ }

				if (isSameRepo) {
					// ── Same-repo: fast worktree path ──
					console.error("Fetching PR branch and creating local worktree...");
					await fetchRef(reviewRuntime, prMetadata.baseBranch, { cwd: repoDir });
					await ensureObjectAvailable(reviewRuntime, prMetadata.baseSha, { cwd: repoDir });
					await fetchRef(reviewRuntime, fetchRefStr, { cwd: repoDir });

					await createWorktree(reviewRuntime, {
						ref: "FETCH_HEAD",
						path: localPath,
						detach: true,
						cwd: repoDir,
					});

					const wtRepoDir = repoDir;
					exitHandler = () => {
						try {
							for (const entry of worktreePool?.entries() ?? []) {
								spawnSync("git", ["worktree", "remove", "--force", entry.path], { cwd: wtRepoDir });
							}
						} catch {}
						if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
					};
					worktreeCleanup = async () => {
						if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
						if (worktreePool) await worktreePool.cleanup(reviewRuntime);
						if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
					};
					process.once("exit", exitHandler);
				} else {
					// ── Cross-repo: shallow clone + fetch PR head ──
					const prRepo = prMetadata.platform === "github"
						? `${prMetadata.owner}/${prMetadata.repo}`
						: prMetadata.projectPath;
					if (/^-/.test(prRepo)) throw new Error(`Invalid repository identifier: ${prRepo}`);
					const cli = prMetadata.platform === "github" ? "gh" : "glab";
					const host = prMetadata.host;
					// gh/glab repo clone doesn't accept --hostname; set GH_HOST/GITLAB_HOST env instead
					const isDefaultHost = host === "github.com" || host === "gitlab.com";
					const cloneEnv = isDefaultHost ? undefined : {
						...process.env,
						...(prMetadata.platform === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }),
					};

					console.error(`Cloning ${prRepo} (shallow)...`);
					const cloneResult = spawnSync(cli, ["repo", "clone", prRepo, localPath, "--", "--depth=1", "--no-checkout"], { encoding: "utf-8", env: cloneEnv });
					if ((cloneResult.status ?? 1) !== 0) {
						throw new Error(`${cli} repo clone failed: ${(cloneResult.stderr ?? "").trim()}`);
					}

					console.error("Fetching PR branch...");
					const fetchResult = await reviewRuntime.runGit(["fetch", "--depth=200", "origin", fetchRefStr], { cwd: localPath });
					if (fetchResult.exitCode !== 0) throw new Error(`Failed to fetch PR head ref: ${fetchResult.stderr.trim()}`);

					const checkoutResult = await reviewRuntime.runGit(["checkout", "FETCH_HEAD"], { cwd: localPath });
					if (checkoutResult.exitCode !== 0) {
						throw new Error(`git checkout FETCH_HEAD failed: ${checkoutResult.stderr.trim()}`);
					}

					// Best-effort: create base refs so agent diffs work
					const baseFetch = await reviewRuntime.runGit(["fetch", "--depth=200", "origin", prMetadata.baseSha], { cwd: localPath });
					if (baseFetch.exitCode !== 0) console.error("Warning: failed to fetch baseSha, agent diffs may be inaccurate");
					await reviewRuntime.runGit(["branch", "--", prMetadata.baseBranch, prMetadata.baseSha], { cwd: localPath });
					await reviewRuntime.runGit(["update-ref", `refs/remotes/origin/${prMetadata.baseBranch}`, prMetadata.baseSha], { cwd: localPath });

					exitHandler = () => {
						if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
					};
					worktreeCleanup = () => {
						if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
						if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
					};
					process.once("exit", exitHandler);
				}

				agentCwd = localPath;
				worktreePool = createWorktreePool(
					{ sessionDir: sessionDir!, repoDir, isSameRepo },
					{ path: localPath, prUrl: prMetadata.url, number: prNumber, ready: true },
				);
				console.error(`Local checkout ready at ${localPath}`);
			} catch (err) {
				console.error("Warning: local worktree creation failed, falling back to remote diff");
				console.error(err instanceof Error ? err.message : String(err));
				if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
				if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
				agentCwd = undefined;
				worktreePool = undefined;
				worktreeCleanup = undefined;
			}
		}
	} else {
		// --- Local Review Mode ---
		const cwd = options.cwd ?? ctx.cwd;
		const config = loadConfig();
		const managedVcs = await detectManagedVcs(cwd, options.vcsType);
		const configuredJjDiffType = options.jjDefaultDiffType ?? resolveJjDefaultDiffType(config);
		const forcedVcs = !!options.vcsType && options.vcsType !== "auto";
		if (managedVcs || forcedVcs) {
			const result = await prepareLocalReviewDiff({
				cwd,
				vcsType: options.vcsType,
				requestedDiffType: options.diffType,
				requestedBase: options.defaultBranch,
				configuredDiffType: resolveDefaultDiffType(config),
				configuredJjDiffType,
				hideWhitespace: config.diffOptions?.hideWhitespace ?? false,
			});
			gitCtx = result.gitContext;
			diffType = result.diffType;
			rawPatch = result.rawPatch;
			gitRef = result.gitRef;
			diffError = result.error;
			// Remember which base the initial diff was computed against so it can
			// be forwarded to the server below. Only matters when the caller
			// overrode the detected default; otherwise it matches gitCtx already.
			initialBase = result.base;
		} else {
			workspace = await buildLocalWorkspaceReview(cwd, {
				requestedDiffType: options.diffType,
				configuredDiffType: resolveDefaultDiffType(config),
				configuredJjDiffType,
				hideWhitespace: config.diffOptions?.hideWhitespace ?? false,
			});
			if (workspace.repos.length === 0) {
				throw new Error("Not in a VCS repo and no nested Git/JJ repositories were found.");
			}
			rawPatch = workspace.rawPatch;
			gitRef = workspace.gitRef;
			diffError = workspace.error;
			diffType = workspace.diffType;
			agentCwd = workspace.root;
		}
	}

	const server = await startReviewServer({
		rawPatch,
		gitRef,
		error: diffError,
		origin: "pi",
		diffType,
		gitContext: gitCtx,
		initialBase,
		prMetadata,
		prPatchIncomplete,
		workspace,
		agentCwd,
		worktreePool,
		htmlContent: reviewHtmlContent,
		sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
		shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,
		pasteApiUrl: process.env.PLANNOTATOR_PASTE_URL || undefined,
		onCleanup: worktreeCleanup,
	});

	return startBrowserDecisionSession(server, ctx, server.waitForDecision);
}

export async function openMarkdownAnnotation(
	ctx: ExtensionContext,
	filePath: string,
	markdown: string,
	mode: AnnotateMode,
	folderPath?: string,
	sourceInfo?: string,
	sourceConverted?: boolean,
	gate?: boolean,
): Promise<{ feedback: string; exit?: boolean; approved?: boolean; selectedMessageId?: string; feedbackScope?: "message" | "messages" }> {
	const session = await startMarkdownAnnotationSession(
		ctx,
		filePath,
		markdown,
		mode,
		folderPath,
		sourceInfo,
		sourceConverted,
		gate,
	);
	return session.waitForDecision();
}

export async function startMarkdownAnnotationSession(
	ctx: ExtensionContext,
	filePath: string,
	markdown: string,
	mode: AnnotateMode,
	folderPath?: string,
	sourceInfo?: string,
	sourceConverted?: boolean,
	gate?: boolean,
	rawHtml?: string,
	renderHtml?: boolean,
	recentMessages?: { messageId: string; text: string; timestamp?: string }[],
): Promise<BrowserDecisionSession<{ feedback: string; exit?: boolean; approved?: boolean; selectedMessageId?: string; feedbackScope?: "message" | "messages" }>> {
	if (!ctx.hasUI || !planHtmlContent) {
		throw new Error("Plannotator annotation browser is unavailable in this session.");
	}

	let resolvedMarkdown = markdown;
	if (!renderHtml && !resolvedMarkdown.trim() && existsSync(filePath)) {
		try {
			const fileStat = statSync(filePath);
			if (!fileStat.isDirectory()) {
				resolvedMarkdown = readFileSync(filePath, "utf-8");
			}
		} catch {
			// fall back to provided markdown
		}
	}

	const server = await startAnnotateServer({
		markdown: resolvedMarkdown,
		filePath,
		origin: "pi",
		mode,
		folderPath,
		recentMessages,
		sourceInfo,
		sourceConverted,
		gate,
		rawHtml,
		renderHtml,
		htmlContent: planHtmlContent,
		sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
		shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,
		pasteApiUrl: process.env.PLANNOTATOR_PASTE_URL || undefined,
	});

	return startBrowserDecisionSession(server, ctx, server.waitForDecision);
}

export async function openLastMessageAnnotation(
	ctx: ExtensionContext,
	lastText: string,
	gate?: boolean,
	recentMessages?: { messageId: string; text: string; timestamp?: string }[],
): Promise<{ feedback: string; exit?: boolean; approved?: boolean; selectedMessageId?: string; feedbackScope?: "message" | "messages" }> {
	const session = await startLastMessageAnnotationSession(ctx, lastText, gate, recentMessages);
	return session.waitForDecision();
}

export async function startLastMessageAnnotationSession(
	ctx: ExtensionContext,
	lastText: string,
	gate?: boolean,
	recentMessages?: { messageId: string; text: string; timestamp?: string }[],
): Promise<BrowserDecisionSession<{ feedback: string; exit?: boolean; approved?: boolean; selectedMessageId?: string; feedbackScope?: "message" | "messages" }>> {
	return startMarkdownAnnotationSession(
		ctx,
		"last-message",
		lastText,
		"annotate-last",
		undefined,
		undefined,
		undefined,
		gate,
		undefined,
		undefined,
		recentMessages,
	);
}

export async function openArchiveBrowserAction(
	ctx: ExtensionContext,
	customPlanPath?: string,
): Promise<{ opened: boolean }> {
	if (!ctx.hasUI || !planHtmlContent) {
		throw new Error("Plannotator archive browser is unavailable in this session.");
	}

	const server = await startPlanReviewServer({
		plan: "",
		htmlContent: planHtmlContent,
		origin: "pi",
		mode: "archive",
		customPlanPath,
		sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
		shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,
		pasteApiUrl: process.env.PLANNOTATOR_PASTE_URL || undefined,
	});

	return openBrowserAndWait(server, ctx, async () => {
		if (server.waitForDone) {
			await server.waitForDone();
		}
		return { opened: true };
	});
}
