import type { IssueComment, PullComment, RepoContext, TreeEntry } from './api';
import { ENRY_LANGUAGE_COLOR_FALLBACK, ENRY_LANGUAGE_COLORS, ENRY_LANGUAGE_GROUPS } from './language-colors';

const RECENT_REPOS_KEY = 'untangled.recentRepos';

export const REPO_LIST_PAGE_LIMIT = 10;

export type SearchParamValue = string | string[] | undefined;

export const parseIntegerSearchParam = (value: SearchParamValue, fallback: number, minimum: number): number => {
	const raw = Array.isArray(value) ? value[0] : value;
	const parsed = Number(raw);

	if (!Number.isInteger(parsed) || parsed < minimum) {
		return fallback;
	}

	return parsed;
};

export interface RecentRepo {
	owner: string;
	slug: string;
	title: string;
	description?: string;
	isFork?: boolean;
	language?: string;
	stars?: number;
	openIssues?: number;
	openPRs?: number;
	updatedAt: string;
}

export interface RecentIssuePull {
	uri: string;
	kind: 'issue' | 'pull';
	title: string;
	repoLabel: string;
	href: string;
	state: 'open' | 'closed' | 'merged';
	number: number;
	author: string;
	updatedAt: string;
}

export const parseRepoJump = (value: string): { owner: string; repo: string } | null => {
	const trimmed = value.trim();
	if (!trimmed) {
		return null;
	}

	if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
		try {
			const url = new URL(trimmed);
			const [owner, repo] = url.pathname.split('/').filter(Boolean);
			return owner && repo ? { owner, repo } : null;
		} catch {
			return null;
		}
	}

	const [owner, repo] = trimmed.split('/').filter(Boolean);
	return owner && repo ? { owner, repo } : null;
};

export const formatRelativeTime = (value?: string): string => {
	if (!value) {
		return 'unknown time';
	}

	const date = new Date(value);
	if (Number.isNaN(date.getTime())) {
		return value;
	}

	const diff = date.getTime() - Date.now();
	const seconds = Math.round(diff / 1000);
	const absSeconds = Math.abs(seconds);
	const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });

	if (absSeconds < 60) return rtf.format(seconds, 'second');
	if (absSeconds < 3600) return rtf.format(Math.round(seconds / 60), 'minute');
	if (absSeconds < 86_400) return rtf.format(Math.round(seconds / 3600), 'hour');
	if (absSeconds < 2_592_000) return rtf.format(Math.round(seconds / 86_400), 'day');
	if (absSeconds < 31_536_000) return rtf.format(Math.round(seconds / 2_592_000), 'month');
	return rtf.format(Math.round(seconds / 31_536_000), 'year');
};

export const getErrorMessage = (cause: unknown): string => {
	if (cause instanceof Error) {
		const message = cause.message;
		if (cause.name === 'ScopeMissingError' || message.includes('ScopeMissingError')) {
			return `please relogin (${message})`;
		}
		return message;
	}
	return 'Unknown error';
};

export const formatBytes = (value: number): string => {
	if (value < 1024) {
		return `${value} B`;
	}
	const units = ['KB', 'MB', 'GB'];
	let current = value / 1024;
	let unitIndex = 0;
	while (current >= 1024 && unitIndex < units.length - 1) {
		current /= 1024;
		unitIndex += 1;
	}
	return `${current.toFixed(current < 10 ? 1 : 0)} ${units[unitIndex]}`;
};

export const treeHref = (repo: RepoContext, ref: string, path = ''): string =>
	`/${repo.owner.handle}/${repo.slug}/tree/${encodeURIComponent(ref)}${path ? `/${encodePath(path)}` : ''}`;

export const blobHref = (repo: RepoContext, ref: string, path: string): string =>
	`/${repo.owner.handle}/${repo.slug}/blob/${encodeURIComponent(ref)}/${encodePath(path)}`;

export const commitsHref = (repo: RepoContext, ref: string): string =>
	`/${repo.owner.handle}/${repo.slug}/commits/${encodeURIComponent(ref)}`;

export const commitHref = (repo: RepoContext, ref: string): string =>
	`/${repo.owner.handle}/${repo.slug}/commit/${encodeURIComponent(ref)}`;

const encodeRouteParam = (value: string | number): string => encodeURIComponent(String(value));

export const issueHref = (repo: RepoContext, issueRef: string | number): string =>
	`/${repo.owner.handle}/${repo.slug}/issues/${encodeRouteParam(issueRef)}`;

export const pullHref = (repo: RepoContext, pullRef: string | number): string =>
	`/${repo.owner.handle}/${repo.slug}/pulls/${encodeRouteParam(pullRef)}`;

export const encodePath = (path: string): string =>
	path
		.split('/')
		.filter(Boolean)
		.map((segment) => encodeURIComponent(segment))
		.join('/');

export const decodeRoutePath = (path?: string): string => {
	if (!path) {
		return '';
	}

	return path
		.split('/')
		.filter(Boolean)
		.map((segment) => safeDecode(segment))
		.join('/');
};

export const safeDecode = (value?: string): string => {
	if (!value) {
		return '';
	}
	try {
		return decodeURIComponent(value);
	} catch {
		return value;
	}
};

export const getParentPath = (path: string): string | null => {
	if (!path) {
		return null;
	}

	const segments = path.split('/').filter(Boolean);
	if (segments.length <= 1) {
		return '';
	}

	return segments.slice(0, -1).join('/');
};

export const markdownLike = (path: string, mimeType?: string): boolean =>
	(mimeType?.includes('markdown') ?? false) || /\.mdx?$/i.test(path);

export const svgLike = (path: string, mimeType?: string): boolean =>
	mimeType === 'image/svg+xml' || /\.svg$/i.test(path);

export const imageLike = (path: string, mimeType?: string): boolean =>
	(mimeType?.startsWith('image/') ?? false) || /\.(png|jpe?g|gif|webp|avif|bmp|ico)$/i.test(path);

export const videoLike = (path: string, mimeType?: string): boolean =>
	(mimeType?.startsWith('video/') ?? false) || /\.(mp4|mov|webm|ogg)$/i.test(path);

export const isDirectory = (entry: TreeEntry): boolean => entry.mode.startsWith('004');

export const sortedTreeEntries = (entries: TreeEntry[]): TreeEntry[] =>
	[...entries].sort((left, right) => {
		const leftIsDirectory = isDirectory(left);
		const rightIsDirectory = isDirectory(right);
		if (leftIsDirectory !== rightIsDirectory) {
			return leftIsDirectory ? -1 : 1;
		}
		return left.name.localeCompare(right.name, undefined, { sensitivity: 'base' });
	});

export const joinPath = (base: string, name: string): string => (base ? `${base}/${name}` : name);

export const trimUri = (value: string): string => value.replace(/^https?:\/\//, '');

export const splitCodeLines = (value: string): string[] => {
	const normalized = value.endsWith('\n') ? value.slice(0, -1) : value;
	return normalized.length === 0 ? [''] : normalized.split('\n');
};

export const countLines = (value: string): number => splitCodeLines(value).length;

export const formatLanguagePercent = (value: number): string =>
	value > 0 && value < 0.05 ? '0.1%' : `${value.toFixed(1)}%`;

export const loadRecentRepos = (): RecentRepo[] => {
	try {
		const raw = localStorage.getItem(RECENT_REPOS_KEY);
		if (!raw) {
			return [];
		}
		return JSON.parse(raw) as RecentRepo[];
	} catch {
		return [];
	}
};

export const saveRecentRepo = (
	repo: RepoContext,
	stats?: { stars?: number; openIssues?: number; openPRs?: number }
) => {
	const currentItems = loadRecentRepos();
	const existing = currentItems.find((item) => item.owner === repo.owner.handle && item.slug === repo.slug);
	const next: RecentRepo = {
		owner: repo.owner.handle,
		slug: repo.slug,
		title: `${repo.owner.handle}/${repo.slug}`,
		description: repo.record.value.description?.trim() || undefined,
		isFork: Boolean(repo.record.value.source),
		language: existing?.language,
		stars: stats?.stars ?? existing?.stars,
		openIssues: stats?.openIssues ?? existing?.openIssues,
		openPRs: stats?.openPRs ?? existing?.openPRs,
		updatedAt: new Date().toISOString(),
	};
	const items = currentItems.filter((item) => !(item.owner === next.owner && item.slug === next.slug));
	items.unshift(next);
	localStorage.setItem(RECENT_REPOS_KEY, JSON.stringify(items.slice(0, 8)));
};

export const updateRecentRepoLanguage = (owner: string, slug: string, language: string) => {
	try {
		const items = loadRecentRepos();
		const idx = items.findIndex((item) => item.owner === owner && item.slug === slug);
		if (idx !== -1) {
			items[idx] = { ...items[idx], language };
			localStorage.setItem(RECENT_REPOS_KEY, JSON.stringify(items));
		}
	} catch {
		// ignore
	}
};

export const loadRecentIssuesPulls = (): RecentIssuePull[] => {
	try {
		const raw = localStorage.getItem('untangled.recentIssuesPulls');
		if (!raw) {
			return [];
		}
		return JSON.parse(raw) as RecentIssuePull[];
	} catch {
		return [];
	}
};

export const saveRecentIssueOrPull = (
	owner: string,
	slug: string,
	item: {
		uri: string;
		kind: 'issue' | 'pull';
		title: string;
		number: number;
		state: 'open' | 'closed' | 'merged';
		author: string;
	},
) => {
	const next: RecentIssuePull = {
		uri: item.uri,
		kind: item.kind,
		title: item.title,
		repoLabel: `${owner}/${slug}`,
		href: `/${owner}/${slug}/${item.kind}s/${item.number || item.uri.split('/').pop()}`,
		state: item.state,
		number: item.number,
		author: item.author,
		updatedAt: new Date().toISOString(),
	};
	const items = loadRecentIssuesPulls().filter((x) => x.uri !== next.uri);
	items.unshift(next);
	localStorage.setItem('untangled.recentIssuesPulls', JSON.stringify(items.slice(0, 10)));
};

export const uniqueCommenters = (
	comments: Array<IssueComment | PullComment>,
): Array<{ did: string; handle: string }> => {
	const seen = new Set<string>();
	const result: Array<{ did: string; handle: string }> = [];
	for (const comment of comments) {
		if (seen.has(comment.author.did)) {
			continue;
		}
		seen.add(comment.author.did);
		result.push({
			did: comment.author.did,
			handle: comment.author.handle,
		});
	}
	return result;
};

export const languageColor = (name: string): string => {
	const normalized = name.trim().toLowerCase();
	const group = ENRY_LANGUAGE_GROUPS[normalized];
	return ENRY_LANGUAGE_COLORS[normalized] ?? (group ? ENRY_LANGUAGE_COLORS[group] : undefined) ?? ENRY_LANGUAGE_COLOR_FALLBACK;
};
