import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import type { ParsedFile } from "./diff-parser.ts";

export interface GitError {
	error: string;
}

export async function validateGitRepo(pi: ExtensionAPI, cwd: string): Promise<true | GitError> {
	const { code } = await pi.exec("git", ["rev-parse", "--git-dir"], { cwd });
	if (code !== 0) {
		return { error: "Not a git repository (or any of the parent directories)" };
	}
	return true;
}

async function refExists(pi: ExtensionAPI, cwd: string, ref: string): Promise<boolean> {
	const { code } = await pi.exec("git", ["rev-parse", "--verify", ref], { cwd });
	return code === 0;
}

async function detectRemote(pi: ExtensionAPI, cwd: string): Promise<string | null> {
	const branch = await pi.exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd });
	if (branch.code === 0 && branch.stdout.trim()) {
		const upstream = await pi.exec(
			"git",
			["for-each-ref", "--format=%(upstream:remotename)", `refs/heads/${branch.stdout.trim()}`],
			{ cwd },
		);
		if (upstream.code === 0 && upstream.stdout.trim()) {
			return upstream.stdout.trim();
		}
	}
	const remotes = await pi.exec("git", ["remote"], { cwd });
	if (remotes.code === 0 && remotes.stdout.trim()) {
		return remotes.stdout.trim().split("\n")[0];
	}
	return null;
}

export async function resolveBase(
	pi: ExtensionAPI,
	cwd: string,
	explicit?: string,
): Promise<{ base: string } | GitError> {
	if (explicit) {
		const exists = await refExists(pi, cwd, explicit);
		if (!exists) {
			return { error: `Base ref "${explicit}" does not exist` };
		}
		return { base: explicit };
	}
	const remote = await detectRemote(pi, cwd);
	for (const branch of ["main", "master"]) {
		if (remote && (await refExists(pi, cwd, `${remote}/${branch}`))) {
			return { base: `${remote}/${branch}` };
		}
		if (await refExists(pi, cwd, branch)) {
			return { base: branch };
		}
	}
	return {
		error: 'No default base branch found. Neither "main" nor "master" exists. Pass an explicit base: /show-diff <base>',
	};
}

export async function computeDiff(
	pi: ExtensionAPI,
	cwd: string,
	base: string,
): Promise<{ diff: string } | GitError> {
	const { stdout, code, stderr } = await pi.exec("git", ["diff", `${base}...HEAD`], { cwd });
	if (code !== 0) {
		return { error: `git diff failed: ${stderr.trim() || "unknown error"}` };
	}
	return { diff: stdout };
}

export interface FileContent {
	path: string;
	status: "added" | "modified" | "deleted" | "renamed";
	additions: number;
	deletions: number;
	binary: boolean;
	original: string;
	modified: string;
}

async function gitShow(pi: ExtensionAPI, cwd: string, ref: string): Promise<string> {
	const { stdout, code } = await pi.exec("git", ["show", ref], { cwd });
	if (code !== 0) return "";
	if (stdout.includes("\0")) return "";
	return stdout;
}

const MAX_CONCURRENCY = 10;

async function mapWithLimit<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {
	const results: R[] = new Array(items.length);
	let next = 0;

	async function worker() {
		while (next < items.length) {
			const i = next++;
			results[i] = await fn(items[i]);
		}
	}

	await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
	return results;
}

export async function fetchFileContents(
	pi: ExtensionAPI,
	cwd: string,
	base: string,
	files: ParsedFile[],
): Promise<FileContent[]> {
	return mapWithLimit(files, MAX_CONCURRENCY, async (file) => {
		if (file.binary) {
			return {
				path: file.path,
				status: file.status,
				additions: file.additions,
				deletions: file.deletions,
				binary: true,
				original: "",
				modified: "",
			};
		}

		let original = "";
		let modified = "";

		switch (file.status) {
			case "modified":
				original = await gitShow(pi, cwd, `${base}:${file.path}`);
				modified = await gitShow(pi, cwd, `HEAD:${file.path}`);
				break;
			case "added":
				modified = await gitShow(pi, cwd, `HEAD:${file.newPath}`);
				break;
			case "deleted":
				original = await gitShow(pi, cwd, `${base}:${file.oldPath}`);
				break;
			case "renamed":
				original = await gitShow(pi, cwd, `${base}:${file.oldPath}`);
				modified = await gitShow(pi, cwd, `HEAD:${file.newPath}`);
				break;
		}

		return {
			path: file.path,
			status: file.status,
			additions: file.additions,
			deletions: file.deletions,
			binary: false,
			original,
			modified,
		};
	});
}

export interface ReviewContext {
	repoRoot: string;
	repoName: string;
	branch: string;
	base: string;
}

export async function hasWorkingTreeChanges(pi: ExtensionAPI, cwd: string): Promise<boolean | GitError> {
	const { stdout, code, stderr } = await pi.exec("git", ["status", "--porcelain"], { cwd });
	if (code !== 0) {
		return { error: `Unable to inspect working tree status: ${stderr.trim() || "unknown error"}` };
	}
	return stdout.trim().length > 0;
}

export async function getReviewContext(
	pi: ExtensionAPI,
	cwd: string,
	base: string,
): Promise<ReviewContext | GitError> {
	const rootResult = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd });
	if (rootResult.code !== 0) {
		return { error: `Unable to resolve repository root: ${rootResult.stderr.trim() || "unknown error"}` };
	}

	const branchResult = await pi.exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd });
	if (branchResult.code !== 0) {
		return { error: `Unable to resolve current branch: ${branchResult.stderr.trim() || "unknown error"}` };
	}

	const repoRoot = rootResult.stdout.trim();
	const parts = repoRoot.split(/[\\/]/).filter(Boolean);
	const repoName = parts[parts.length - 1] ?? "repo";
	const branch = branchResult.stdout.trim() || "HEAD";

	return { repoRoot, repoName, branch, base };
}

