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 "@mariozechner/pi-coding-agent";
import {
	getGitContext,
	reviewRuntime,
	runGitDiff,
	startAnnotateServer,
	startPlanReviewServer,
	startReviewServer,
	type DiffType,
} 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 } from "./generated/config.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;
}

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";
}

function openBrowserForServer(serverUrl: string, ctx: ExtensionContext): void {
	const browserResult = 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 openBrowserAndWait<T>(
	server: { url: string; stop: () => void },
	ctx: ExtensionContext,
	waitForResult: () => Promise<T>,
): Promise<T> {
	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 async function openCodeReview(
	ctx: ExtensionContext,
	options: { cwd?: string; defaultBranch?: string; diffType?: DiffType; prUrl?: string } = {},
): 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 } = {},
): 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 getGitContext>> | undefined;
	let prMetadata: Awaited<ReturnType<typeof fetchPR>>["metadata"] | undefined;
	let diffType: DiffType | undefined;
	let agentCwd: string | undefined;
	let initialBase: string | undefined;
	let worktreeCleanup: (() => void | Promise<void>) | undefined;
	let worktreePool: WorktreePool | undefined;
	let exitHandler: (() => void) | 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;

		// Create local worktree for agent file access (--local is the default for PR reviews)
		let localPath: string | undefined;
		let sessionDir: string | undefined;
		try {
			const repoDir = options.cwd ?? ctx.cwd;
			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;
		gitCtx = await getGitContext(cwd);
		const defaultBranch = options.defaultBranch ?? gitCtx.defaultBranch;
		const config = loadConfig();
		diffType = options.diffType ?? resolveDefaultDiffType(config);
		const result = await runGitDiff(diffType, defaultBranch, cwd, {
			hideWhitespace: config.diffOptions?.hideWhitespace ?? false,
		});
		rawPatch = result.patch;
		gitRef = result.label;
		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 = defaultBranch;
	}

	const server = await startReviewServer({
		rawPatch,
		gitRef,
		error: diffError,
		origin: "pi",
		diffType,
		gitContext: gitCtx,
		initialBase,
		prMetadata,
		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 }> {
	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,
): Promise<BrowserDecisionSession<{ feedback: string; exit?: boolean; approved?: boolean }>> {
	if (!ctx.hasUI || !planHtmlContent) {
		throw new Error("Plannotator annotation browser is unavailable in this session.");
	}

	let resolvedMarkdown = markdown;
	if (!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,
		sourceInfo,
		sourceConverted,
		gate,
		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,
): Promise<{ feedback: string; exit?: boolean; approved?: boolean }> {
	return openMarkdownAnnotation(ctx, "last-message", lastText, "annotate-last", undefined, undefined, undefined, gate);
}

export async function startLastMessageAnnotationSession(
	ctx: ExtensionContext,
	lastText: string,
	gate?: boolean,
): Promise<BrowserDecisionSession<{ feedback: string; exit?: boolean; approved?: boolean }>> {
	return startMarkdownAnnotationSession(
		ctx,
		"last-message",
		lastText,
		"annotate-last",
		undefined,
		undefined,
		undefined,
		gate,
	);
}

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 };
	});
}
