import { ClientResponseError, type BobbinContext, type XrpcRequestInit } from "./client";
import { getRepoByName, type RecordView, type RepoRecord } from "./records";
import { branches as knotBranches, log as knotLog, tags as knotTags } from "./knot";
import { httpStatusFor } from "./load";
import { rkeyFromUri } from "./uri";
import type * as Tree from "./lexicons/types/sh/tangled/repo/tree";

// log, branches and tags are `*/*` in the lexicons, so these shapes are copied
// from core/types by hand. anything go-git touches keeps its go field names and
// writes hashes as byte arrays, so the hex comes from a sibling field

export interface GitSignature {
	Name: string;
	Email: string;
	When: string;
}

export interface GitCommit {
	Author?: GitSignature;
	Committer?: GitSignature;
	Message?: string;
}

// `this` and `parent` are the hex hashes, `hash` is the byte array
export interface LogCommit {
	this?: string;
	parent?: string;
	author?: GitSignature;
	committer?: GitSignature;
	message?: string;
	tree?: string;
	change_id?: string;
}

export interface LogResponse {
	commits?: LogCommit[];
	ref?: string;
	total?: number;
	page?: number;
}

export interface GitReference {
	name: string;
	hash: string;
}

export interface BranchEntry {
	reference: GitReference;
	commit?: GitCommit;
	is_default?: boolean;
}

export interface BranchesResponse {
	branches?: BranchEntry[];
	total?: number;
}

export interface TagEntry {
	name: string;
	hash: string;
	message?: string;
	tag?: {
		Tagger?: GitSignature;
		Message?: string;
		/** the commit an annotated tag points at, bytes like every go-git hash */
		Target?: number[];
	};
}

export interface TagsResponse {
	tags?: TagEntry[];
	total?: number;
}

export const repoNameOf = (view: RecordView<RepoRecord>): string =>
	view.value.name ?? rkeyFromUri(view.uri);

// a repo bobbin has never indexed is a miss, not an error
export const resolveRepoByName = async (
	ctx: BobbinContext,
	ownerDid: string,
	name: string,
	init?: XrpcRequestInit
): Promise<RecordView<RepoRecord> | null> => {
	try {
		return await getRepoByName(ctx, ownerDid, name, init);
	} catch (cause) {
		if (cause instanceof ClientResponseError && httpStatusFor(cause) === 404) return null;
		throw cause;
	}
};

export interface CommitSummary {
	hash: string;
	shortHash: string;
	subject: string;
	body: string;
	authorName: string;
	authorEmail: string;
	when: string;
	changeId?: string;
}

const splitMessage = (message: string): [string, string] => {
	const separator = message.indexOf("\n\n");
	if (separator === -1) return [message.trim(), ""];
	return [message.slice(0, separator).trim(), message.slice(separator + 2).trim()];
};

/** the subject is the first paragraph, not the first line */
export const subjectOf = (message: string): string => splitMessage(message)[0];

export const toCommitSummary = (commit: LogCommit): CommitSummary => {
	const [subject, body] = splitMessage(commit.message ?? "");
	const hash = commit.this ?? "";
	return {
		hash,
		shortHash: hash.slice(0, 8),
		subject,
		body,
		authorName: commit.author?.Name ?? "",
		authorEmail: commit.author?.Email ?? "",
		when: commit.committer?.When ?? commit.author?.When ?? "",
		changeId: commit.change_id
	};
};

// the tree endpoint uses lexicon casing where the log ones pass through go field
// names, so this cannot share `toCommitSummary`
export const toTreeCommitSummary = (commit: Tree.LastCommit): CommitSummary => {
	const [subject, body] = splitMessage(commit.message ?? "");
	return {
		hash: commit.hash,
		shortHash: commit.hash.slice(0, 8),
		subject,
		body,
		authorName: commit.author?.name ?? "",
		authorEmail: commit.author?.email ?? "",
		when: commit.when ?? commit.author?.when ?? ""
	};
};

export interface BranchSummary {
	name: string;
	hash: string;
	when?: string;
	isDefault: boolean;
}

export const toBranchSummary = (branch: BranchEntry): BranchSummary => ({
	name: branch.reference.name,
	hash: branch.reference.hash,
	when: branch.commit?.Committer?.When ?? branch.commit?.Author?.When,
	isDefault: branch.is_default === true
});

export interface TagSummary {
	name: string;
	hash: string;
	/** an annotated tag has its own hash, this is the commit it points at */
	commitHash: string;
	when?: string;
	message?: string;
}

const hexFromBytes = (bytes: number[]): string =>
	bytes.map((byte) => byte.toString(16).padStart(2, "0")).join("");

export const toTagSummary = (tag: TagEntry): TagSummary => {
	const target = tag.tag?.Target;
	return {
		name: tag.name,
		hash: tag.hash,
		commitHash: target?.length ? hexFromBytes(target) : tag.hash,
		when: tag.tag?.Tagger?.When,
		message: tag.message ?? tag.tag?.Message
	};
};

export type TreeEntryKind = "file" | "directory" | "symlink" | "submodule";

// modes come back octal and zero padded
export const treeEntryKind = (mode: string): TreeEntryKind => {
	switch (mode.replace(/^0+/, "").padStart(6, "0")) {
		case "040000":
			return "directory";
		case "120000":
			return "symlink";
		case "160000":
			return "submodule";
		default:
			return "file";
	}
};

export interface TreeEntrySummary {
	name: string;
	kind: TreeEntryKind;
	size: number;
	lastCommitHash?: string;
	lastCommitWhen?: string;
	lastCommitMessage?: string;
}

export const toTreeEntrySummary = (entry: Tree.TreeEntry): TreeEntrySummary => ({
	name: entry.name,
	kind: treeEntryKind(entry.mode),
	size: entry.size,
	lastCommitHash: entry.last_commit?.hash,
	lastCommitWhen: entry.last_commit?.when,
	lastCommitMessage: entry.last_commit?.message
});

export const tagsByCommitHash = (
	commits: CommitSummary[],
	tags: TagSummary[]
): Record<string, string[]> => {
	const shown = new Set(commits.map((commit) => commit.hash));
	return tags.reduce<Record<string, string[]>>((acc, tag) => {
		if (shown.has(tag.commitHash)) (acc[tag.commitHash] ??= []).push(tag.name);
		return acc;
	}, {});
};

export const sortTreeEntries = (entries: TreeEntrySummary[]): TreeEntrySummary[] =>
	[...entries].sort((a, b) => {
		const aDir = a.kind === "directory" || a.kind === "submodule";
		const bDir = b.kind === "directory" || b.kind === "submodule";
		if (aDir !== bDir) return aDir ? -1 : 1;
		return a.name.localeCompare(b.name);
	});

export const logFor = (
	ctx: BobbinContext,
	repo: string,
	ref: string,
	limit: number,
	init?: XrpcRequestInit
) => knotLog<LogResponse>(ctx, { repo, ref, limit }, init);

export const branchesFor = (
	ctx: BobbinContext,
	repo: string,
	limit: number,
	init?: XrpcRequestInit
) => knotBranches<BranchesResponse>(ctx, { repo, limit }, init);

export const tagsFor = (ctx: BobbinContext, repo: string, limit: number, init?: XrpcRequestInit) =>
	knotTags<TagsResponse>(ctx, { repo, limit }, init);
