import type { BobbinContext, XrpcRequestInit } from "./client";
import { createBobbinClient } from "./client";
import { buildUrl, jsonGet, rawGet } from "./_request";
import type { RepoTagResponse } from "./repo";
import type * as RepoBlob from "./lexicons/types/sh/tangled/repo/blob";
import type * as LegacyTree from "./lexicons/types/sh/tangled/git/temp/getTree";
import type * as Tree from "./lexicons/types/sh/tangled/repo/tree";
import { treeEntryKind, type BranchesResponse, type LogResponse, type TagsResponse } from "./repo";

const TREE_NSID = "sh.tangled.git.temp.getTree";
const ENTRY_NSID = "sh.tangled.git.temp.getEntry";
const BLOB_NSID = "sh.tangled.git.temp.getBlob";
const LOG_NSID = "sh.tangled.git.temp.listCommits";
const BRANCHES_NSID = "sh.tangled.git.temp.listBranches";
const TAGS_NSID = "sh.tangled.git.temp.listTags";
const LANGUAGES_NSID = "sh.tangled.git.temp.listLanguages";
const GET_TAG_NSID = "sh.tangled.git.temp.getTag";

const MAX_README_BYTES = 1 << 20;

export interface KnotMirrorLanguagesResponse {
	languages?: { name: string; size: number }[];
	total?: number;
	ref?: string;
}

export const createKnotMirrorClient = (serviceUrl: string, fetch: typeof globalThis.fetch) =>
	createBobbinClient({ serviceUrl, fetch });

export interface KnotMirrorEntry {
	name: string;
	mode: string;
	oid?: string;
	size?: number;
	lastCommit?: {
		hash: string;
		message?: string;
		author?: { name: string; email: string; when: string };
		committer?: { name: string; email: string; when: string };
	};
}

export const entry = (
	ctx: BobbinContext,
	params: { repo: string; ref: string; path: string },
	init?: XrpcRequestInit
) => jsonGet<KnotMirrorEntry>(ctx, ENTRY_NSID, params, init);

export const blobRawUrl = (
	serviceUrl: string,
	params: { repo: string; ref: string; path: string }
): string => buildUrl(serviceUrl, BLOB_NSID, params).toString();

// same heuristic as git: a null byte in the first chunk means binary
const looksBinary = (bytes: Uint8Array): boolean => bytes.subarray(0, 8000).includes(0);

// temp.getBlob answers raw bytes only, metadata comes from getEntry and
// the binary flag from a content sniff
export const blob = async (
	ctx: BobbinContext,
	params: { repo: string; ref: string; path: string },
	maxBytes: number,
	init?: XrpcRequestInit
): Promise<RepoBlob.$output> => {
	const meta = await entry(ctx, params, init);

	const lastCommit = meta.lastCommit
		? {
				hash: meta.lastCommit.hash,
				message: meta.lastCommit.message ?? "",
				when: meta.lastCommit.committer?.when ?? meta.lastCommit.author?.when ?? "",
				author: meta.lastCommit.author
			}
		: undefined;

	// a gitlink is a submodule pointer, not a file
	if (treeEntryKind(meta.mode) === "submodule") {
		return {
			path: params.path,
			ref: params.ref,
			size: meta.size,
			submodule: { name: meta.name, url: "" }
		};
	}

	if (meta.size !== undefined && meta.size > maxBytes) {
		return { path: params.path, ref: params.ref, size: meta.size, fileTooLarge: true, lastCommit };
	}

	const bytes = new Uint8Array(await (await rawGet(ctx, BLOB_NSID, params, init)).arrayBuffer());
	if (looksBinary(bytes)) {
		return {
			path: params.path,
			ref: params.ref,
			size: meta.size ?? bytes.length,
			isBinary: true,
			lastCommit
		};
	}

	return {
		path: params.path,
		ref: params.ref,
		size: meta.size ?? bytes.length,
		isBinary: false,
		encoding: "utf-8",
		content: new TextDecoder().decode(bytes),
		lastCommit
	};
};

const normalizeSignature = (
	signature: LegacyTree.Signature | undefined
): Tree.Signature | undefined =>
	signature ? { name: signature.name, email: signature.email, when: signature.when } : undefined;

const normalizeCommit = (commit: LegacyTree.LastCommit | undefined): Tree.LastCommit | undefined =>
	commit
		? {
				hash: commit.hash,
				message: commit.message,
				when: commit.when,
				author: normalizeSignature(commit.author)
			}
		: undefined;

const normalizeTree = (tree: LegacyTree.$output): Tree.$output => ({
	ref: tree.ref,
	parent: tree.parent,
	dotdot: tree.dotdot,
	lastCommit: normalizeCommit(tree.lastCommit),
	files: tree.files.map((entry) => ({
		name: entry.name,
		mode: entry.mode,
		size: entry.size,
		last_commit: normalizeCommit(entry.last_commit)
	}))
});

const isReadmeFile = (name: string, mode: string): boolean =>
	/^readme(?:\.[^.]+)?$/i.test(name) && treeEntryKind(mode) === "file";

const pathInTree = (path: string | undefined, name: string): string =>
	path ? `${path}/${name}` : name;

export const tree = async (
	ctx: BobbinContext,
	params: { repo: string; ref: string; path?: string },
	init?: XrpcRequestInit
): Promise<Tree.$output> => {
	const response = await jsonGet<LegacyTree.$output>(ctx, TREE_NSID, params, init);
	const result = normalizeTree(response);
	const readme = response.files.find((entry) => isReadmeFile(entry.name, entry.mode));
	if (!readme) return result;
	if (readme.size !== undefined && readme.size > MAX_README_BYTES) return result;

	try {
		const blob = await rawGet(
			ctx,
			BLOB_NSID,
			{ repo: params.repo, ref: params.ref, path: pathInTree(params.path, readme.name) },
			init
		);
		return { ...result, readme: { filename: readme.name, contents: await blob.text() } };
	} catch {
		return result;
	}
};

export const log = (
	ctx: BobbinContext,
	params: { repo: string; ref: string; limit: number; cursor?: string },
	init?: XrpcRequestInit
) => jsonGet<LogResponse>(ctx, LOG_NSID, params, init);

export const branches = (
	ctx: BobbinContext,
	params: { repo: string; limit: number; cursor?: string },
	init?: XrpcRequestInit
) => jsonGet<BranchesResponse>(ctx, BRANCHES_NSID, params, init);

export const tags = (
	ctx: BobbinContext,
	params: { repo: string; limit: number; cursor?: string },
	init?: XrpcRequestInit
) => jsonGet<TagsResponse>(ctx, TAGS_NSID, params, init);

export const getTag = (
	ctx: BobbinContext,
	params: { repo: string; tag: string },
	init?: XrpcRequestInit
) => jsonGet<RepoTagResponse>(ctx, GET_TAG_NSID, params, init);

export const languages = (
	ctx: BobbinContext,
	params: { repo: string; ref: string },
	init?: XrpcRequestInit
) => jsonGet<KnotMirrorLanguagesResponse>(ctx, LANGUAGES_NSID, params, init);
