import { ok } from '@atcute/client';
import type { ResolvedActor } from '@atcute/identity-resolver';
import type { Did, Nsid, ResourceUri } from '@atcute/lexicons/syntax';
import { ShTangledRepo } from '@atcute/tangled';

import {
	appviewFallback,
	appviewJson,
	appviewOptionalKnotRequest,
	getRepoViaAppview,
	listAllAppviewRecords,
	type AppviewRecordView,
} from './appview';
import { getRpc, normalizeServiceUrl } from './client';
import { REPO_COLLECTION } from './constants';
import { resolveActor } from './identity';
import {
	maxTimestamp,
	parseAtUri,
	timestampFromDate,
} from './records';

export interface RepoContext {
	owner: ResolvedActor;
	record: RepoRecord;
	repoDid: Did;
	slug: string;
	rkey: string;
	knot: string;
}

export interface RepoRecord {
	uri: ResourceUri;
	cid: string;
	rkey: string;
	value: ShTangledRepo.Main;
}

export interface TreeResponse {
	ref: string;
	parent?: string;
	dotdot?: string;
	lastCommit?: CommitHeadline;
	readme?: {
		contents: string;
		filename: string;
	};
	files: TreeEntry[];
}

export interface TreeEntry {
	name: string;
	mode: string;
	size: number;
	last_commit?: CommitHeadline;
}

export interface CommitHeadline {
	hash: string;
	message: string;
	when: string;
	author?: {
		email: string;
		name: string;
		when: string;
	};
}

export interface BranchResponse {
	branches: BranchEntry[];
}

export interface DefaultBranchResponse {
	name?: string;
	branch?: string;
	reference?: {
		name?: string;
	};
	hash?: string;
	when?: string;
}

export interface BranchEntry {
	reference: {
		name: string;
		hash: string;
	};
	commit?: {
		Message: string;
		Author?: {
			Name: string;
			Email: string;
			When: string;
		};
		Committer?: {
			Name: string;
			Email: string;
			When: string;
		};
	};
	is_default?: boolean;
}

export interface TagResponse {
	tags: Array<{
		name: string;
		hash: string;
		message?: string;
		tag?: {
			Name: string;
			Message: string;
			Tagger?: {
				Name: string;
				Email: string;
				When: string;
			};
		};
	}>;
}

export interface LanguageResponse {
	ref: string;
	totalFiles?: number;
	totalSize?: number;
	languages: Array<{
		name: string;
		percentage: number;
		size: number;
		color?: string;
	}>;
}

export type GitHashValue =
	| string
	| number[]
	| {
			String?: string;
			string?: string;
	  };

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

export interface CommitEntry {
	hash?: GitHashValue;
	this: string;
	message: string;
	tree?: string;
	author?: CommitSignature;
	committer?: CommitSignature;
	pgp_signature?: string;
	parent?: string;
	parent_hashes?: GitHashValue[];
}

export interface LogResponse {
	ref: string;
	total: number;
	page: number;
	per_page: number;
	log: boolean;
	commits: CommitEntry[];
}

export interface DiffTextLine {
	Op: 0 | 1 | 2 | ' ' | '-' | '+';
	Line: string;
}

export interface DiffTextFragment {
	Comment?: string;
	OldPosition: number;
	OldLines: number;
	NewPosition: number;
	NewLines: number;
	LinesAdded: number;
	LinesDeleted: number;
	LeadingContext?: number;
	TrailingContext?: number;
	Lines: DiffTextLine[];
}

export interface RepoDiffFile {
	name: {
		old: string;
		new: string;
	};
	text_fragments: DiffTextFragment[];
	is_binary: boolean;
	is_new: boolean;
	is_delete: boolean;
	is_copy: boolean;
	is_rename: boolean;
}

export interface RepoDiffStat {
	insertions: number;
	deletions: number;
	files_changed: number;
}

export interface RepoDiff {
	commit: CommitEntry;
	stat: RepoDiffStat;
	diff: RepoDiffFile[];
}

export interface RepoCommitResponse {
	ref: string;
	diff?: RepoDiff;
}

export interface BlobResponse {
	path: string;
	ref: string;
	content?: string;
	encoding?: 'base64' | 'utf-8';
	fileTooLarge?: boolean;
	isBinary?: boolean;
	mimeType?: string;
	size?: number;
	lastCommit?: CommitHeadline;
	submodule?: {
		name: string;
		url: string;
		branch?: string;
	};
}

export interface ComparePatch {
	Title: string;
	Body: string;
	Raw: string;
	SHA: string;
	Author?: string;
	Committer?: string;
	AuthorDate?: string;
	CommitterDate?: string;
	SubjectPrefix?: string;
	RawHeaders?: string;
	BodyAppendix?: string;
}

export interface CompareResponse {
	rev1: string;
	rev2: string;
	patch: string;
	format_patch: ComparePatch[];
}

const fetchAllRepoRecords = async (
	actor: ResolvedActor,
	collection: Nsid,
): Promise<Array<{ uri: ResourceUri; cid: string; value: unknown }>> => {
	const rpc = getRpc(actor.pds);
	const records: Array<{ uri: ResourceUri; cid: string; value: unknown }> = [];
	let cursor: string | undefined;

	do {
		const page = await ok(
			rpc.get('com.atproto.repo.listRecords', {
				params: {
					repo: actor.did,
					collection,
					limit: 100,
					cursor,
					reverse: true,
				},
			}),
		);

		records.push(...page.records);
		cursor = page.cursor;
	} while (cursor);

	return records;
};

const listRepoRecordsFromAppview = async (owner: string | ResolvedActor): Promise<RepoRecord[]> => {
	const actor = typeof owner === 'string' ? await resolveActor(owner) : owner;
	const records = await listAllAppviewRecords<ShTangledRepo.Main>('sh.tangled.repo.listRepos', actor.did);

	return records.map((record) => ({
		uri: record.uri,
		cid: record.cid ?? '',
		rkey: parseAtUri(record.uri).rkey,
		value: record.value,
	}));
};

const listRepoRecordsFromPds = async (owner: string | ResolvedActor): Promise<RepoRecord[]> => {
	const actor = typeof owner === 'string' ? await resolveActor(owner) : owner;
	const records = await fetchAllRepoRecords(actor, REPO_COLLECTION);

	return records.map((record) => ({
		uri: record.uri,
		cid: record.cid,
		rkey: parseAtUri(record.uri).rkey,
		value: record.value as ShTangledRepo.Main,
	}));
};

export const listRepoRecords = async (owner: string | ResolvedActor): Promise<RepoRecord[]> =>
	appviewFallback(
		() => listRepoRecordsFromAppview(owner),
		() => listRepoRecordsFromPds(owner),
	);

export const getRepo = async (ownerIdentifier: string, repoSlug: string): Promise<RepoContext> => {
	const owner = await resolveActor(ownerIdentifier);
	const all = await listRepoRecords(owner);
	const wanted = repoSlug.trim().toLowerCase();
	const match = all.find((record) => {
		const name = record.value.name?.trim().toLowerCase();
		return name === wanted || record.rkey.toLowerCase() === wanted;
	});

	if (!match) {
		throw new Error(`Repository not found: ${ownerIdentifier}/${repoSlug}`);
	}

	if (!match.value.repoDid) {
		throw new Error(`Repository is missing repoDid metadata`);
	}

	const slug = match.value.name?.trim() || match.rkey;

	return {
		owner,
		record: match,
		repoDid: match.value.repoDid,
		slug,
		rkey: match.rkey,
		knot: normalizeServiceUrl(match.value.knot),
	};
};

export const getRepoByDid = async (repoDid: Did): Promise<RepoContext> => {
	const record = await appviewJson<AppviewRecordView<ShTangledRepo.Main>>(
		'sh.tangled.repo.getRepoByRepoDid',
		{ repoDid },
	);
	const parsed = parseAtUri(record.uri);
	const owner = await resolveActor(parsed.did);
	const slug = record.value.name?.trim() || parsed.rkey;

	if (!record.value.repoDid) {
		throw new Error(`Repository is missing repoDid metadata`);
	}

	return {
		owner,
		record: {
			uri: record.uri,
			cid: record.cid ?? '',
			rkey: parsed.rkey,
			value: record.value,
		},
		repoDid: record.value.repoDid,
		slug,
		rkey: parsed.rkey,
		knot: normalizeServiceUrl(record.value.knot),
	};
};

const getRepoTreeFromKnot = async (
	repo: RepoContext,
	ref: string,
	path = '',
): Promise<TreeResponse> =>
	(await ok(
		getRpc(repo.knot).get('sh.tangled.repo.tree', {
			params: {
				repo: repo.repoDid,
				ref,
				path,
			},
		}),
	)) as TreeResponse;

const getRepoBranchesFromKnot = async (repo: RepoContext): Promise<BranchResponse> =>
	(await ok(
		getRpc(repo.knot).get('sh.tangled.repo.branches', {
			params: {
				repo: repo.repoDid,
			},
			as: 'json',
		}),
	)) as BranchResponse;

const branchUpdatedTimestamp = (branch: BranchEntry): number =>
	maxTimestamp(
		timestampFromDate(branch.commit?.Committer?.When),
		timestampFromDate(branch.commit?.Author?.When),
	);

const sortBranchResponseByUpdatedAt = (response: BranchResponse): BranchResponse => ({
	...response,
	branches: [...response.branches].sort((left, right) => {
		const updatedDiff = branchUpdatedTimestamp(right) - branchUpdatedTimestamp(left);
		if (updatedDiff !== 0) {
			return updatedDiff;
		}

		if (left.is_default !== right.is_default) {
			return left.is_default ? -1 : 1;
		}

		return left.reference.name.localeCompare(right.reference.name);
	}),
});

const getRepoDefaultBranchFromKnot = async (repo: RepoContext): Promise<DefaultBranchResponse> =>
	(await ok(
		getRpc(repo.knot).get('sh.tangled.repo.getDefaultBranch', {
			params: {
				repo: repo.repoDid,
			},
		}),
	)) as DefaultBranchResponse;

const getRepoTagsFromKnot = async (repo: RepoContext): Promise<TagResponse> =>
	(await ok(
		getRpc(repo.knot).get('sh.tangled.repo.tags', {
			params: {
				repo: repo.repoDid,
			},
			as: 'json',
		}),
	)) as TagResponse;

const getRepoLanguagesFromKnot = async (repo: RepoContext, ref: string): Promise<LanguageResponse> =>
	(await ok(
		getRpc(repo.knot).get('sh.tangled.repo.languages', {
			params: {
				repo: repo.repoDid,
				ref,
			},
		}),
	)) as LanguageResponse;

export interface RepoLogOptions {
	limit?: number;
	cursor?: string;
	path?: string;
}

const getRepoLogFromKnot = async (
	repo: RepoContext,
	ref: string,
	options: RepoLogOptions = {},
): Promise<LogResponse> =>
	(await ok(
		getRpc(repo.knot).get('sh.tangled.repo.log', {
			params: {
				repo: repo.repoDid,
				ref,
				limit: options.limit ?? 8,
				...(options.cursor ? { cursor: options.cursor } : {}),
				...(options.path ? { path: options.path } : {}),
			},
			as: 'json',
		}),
	)) as LogResponse;

const getRepoCommitFromKnot = async (
	repo: RepoContext,
	ref: string,
): Promise<RepoCommitResponse> =>
	(await ok(
		getRpc(repo.knot).get('sh.tangled.repo.diff', {
			params: {
				repo: repo.repoDid,
				ref,
			},
			as: 'json',
		}),
	)) as RepoCommitResponse;

const getRepoBlobFromKnot = async (
	repo: RepoContext,
	ref: string,
	path: string,
): Promise<BlobResponse> =>
	(await ok(
		getRpc(repo.knot).get('sh.tangled.repo.blob', {
			params: {
				repo: repo.repoDid,
				ref,
				path,
			},
		}),
	)) as BlobResponse;

const compareBranchesFromKnot = async (
	repo: RepoContext,
	rev1: string,
	rev2: string,
): Promise<CompareResponse> =>
	(await ok(
		getRpc(repo.knot).get('sh.tangled.repo.compare', {
			params: {
				repo: repo.repoDid,
				rev1,
				rev2,
			},
			as: 'json',
		}),
	)) as CompareResponse;

export const getRepoTree = async (
	repo: RepoContext,
	ref: string,
	path = '',
): Promise<TreeResponse> =>
	appviewOptionalKnotRequest(
		() => getRepoViaAppview<TreeResponse>('sh.tangled.repo.tree', repo, { ref, path }),
		() => getRepoTreeFromKnot(repo, ref, path),
	);

export const getRepoBranches = async (repo: RepoContext): Promise<BranchResponse> =>
	appviewOptionalKnotRequest(
		() => getRepoViaAppview<BranchResponse>('sh.tangled.repo.branches', repo),
		() => getRepoBranchesFromKnot(repo),
	).then(sortBranchResponseByUpdatedAt);

export const getRepoDefaultBranch = async (repo: RepoContext): Promise<DefaultBranchResponse> =>
	appviewOptionalKnotRequest(
		() => getRepoViaAppview<DefaultBranchResponse>('sh.tangled.repo.getDefaultBranch', repo),
		() => getRepoDefaultBranchFromKnot(repo),
	);

export const getRepoTags = async (repo: RepoContext): Promise<TagResponse> =>
	appviewOptionalKnotRequest(
		() => getRepoViaAppview<TagResponse>('sh.tangled.repo.tags', repo),
		() => getRepoTagsFromKnot(repo),
	);

export const getRepoLanguages = async (repo: RepoContext, ref: string): Promise<LanguageResponse> =>
	appviewOptionalKnotRequest(
		() => getRepoViaAppview<LanguageResponse>('sh.tangled.repo.languages', repo, { ref }),
		() => getRepoLanguagesFromKnot(repo, ref),
	);

export const getRepoLog = async (
	repo: RepoContext,
	ref: string,
	options: RepoLogOptions = {},
): Promise<LogResponse> =>
	appviewOptionalKnotRequest(
		() => getRepoViaAppview<LogResponse>('sh.tangled.repo.log', repo, { ref, ...options, limit: options.limit ?? 8 }),
		() => getRepoLogFromKnot(repo, ref, options),
	);

export const getRepoCommit = async (
	repo: RepoContext,
	ref: string,
): Promise<RepoCommitResponse> =>
	appviewOptionalKnotRequest(
		() => getRepoViaAppview<RepoCommitResponse>('sh.tangled.repo.diff', repo, { ref }),
		() => getRepoCommitFromKnot(repo, ref),
	);

export const getRepoBlob = async (
	repo: RepoContext,
	ref: string,
	path: string,
): Promise<BlobResponse> =>
	appviewOptionalKnotRequest(
		() => getRepoViaAppview<BlobResponse>('sh.tangled.repo.blob', repo, { ref, path }),
		() => getRepoBlobFromKnot(repo, ref, path),
	);

export const compareBranches = async (
	repo: RepoContext,
	rev1: string,
	rev2: string,
): Promise<CompareResponse> =>
	appviewOptionalKnotRequest(
		() => getRepoViaAppview<CompareResponse>('sh.tangled.repo.compare', repo, { rev1, rev2 }),
		() => compareBranchesFromKnot(repo, rev1, rev2),
	);

export const decodeBlobText = (blob: BlobResponse): string => {
	if (!blob.content) {
		return '';
	}

	if (blob.encoding === 'base64') {
		const buffer = Uint8Array.from(atob(blob.content), (char) => char.charCodeAt(0));
		return new TextDecoder().decode(buffer);
	}

	return blob.content;
};

export const buildBlobDataUrl = (blob: BlobResponse): string | null => {
	if (!blob.content || !blob.mimeType) {
		return null;
	}

	if (blob.encoding === 'base64') {
		return `data:${blob.mimeType};base64,${blob.content}`;
	}

	return `data:${blob.mimeType};charset=utf-8,${encodeURIComponent(blob.content)}`;
};
