import { ok } from '@atcute/client';
import type { ResourceUri } from '@atcute/lexicons/syntax';
import type { OAuthUserAgent } from '@atcute/oauth-browser-client';
import {
	ShTangledRepoIssue,
	ShTangledRepoIssueComment,
	ShTangledRepoIssueState,
} from '@atcute/tangled';

import {
	appviewFallback,
	appviewJson,
	getAppviewRecordCount,
	listAllAppviewRecords,
	type AppviewRecordView,
	type AppviewStatefulRecordView,
} from './appview';
import {
	getBacklinks,
	getLatestBacklinkTimestamp,
	getUniqueBacklinkRefs,
	latestBacklinkRefByRkey,
	listBacklinkedRecordsPage,
} from './backlinks';
import {
	ISSUE_COLLECTION,
	ISSUE_COMMENT_COLLECTION,
	ISSUE_STATE_COLLECTION,
	FEED_COMMENT_COLLECTION,
} from './constants';
import {
	appviewRecordToHydrated,
	fetchRecordValue,
	hydrateBacklinks,
	hydrateBacklinksFromAppview,
} from './hydration';
import { createAuthRpc } from './identity';
import {
	extractAtUris,
	extractDids,
	findNumberedRecord,
	type HydratedRecord,
	maxTimestamp,
	optionalString,
	type PaginatedResult,
	parseAtUri,
	parseDirectRecordRef,
	sortByCreatedAt,
	timestampFromDate,
	timestampFromTid,
	toNumberMap,
	type RecordCreationResult,
	type BacklinkRecordRef,
} from './records';
import type { RepoContext } from './repos';
import {
	getAppviewRecordSortTimestamp,
	listAppviewStatefulRecordsPage,
	listSearchedStatefulRecordsPage,
	sortStatefulRecordsByNewestRecord,
	sortStatefulRecordsByUpdatedAt,
	type StatefulPageOptions,
} from './stateful';

export interface IssueSummary extends HydratedRecord<ShTangledRepoIssue.Main> {
	number: number;
	state: 'open' | 'closed';
}

export interface IssueComment extends HydratedRecord<ShTangledRepoIssueComment.Main> {}

export interface IssueDetail {
	issue: IssueSummary;
	comments: IssueComment[];
}

export interface CreateIssueInput {
	title: string;
	body: string;
}

const normalizeIssueState = (value?: string): 'open' | 'closed' =>
	value === 'closed' || value?.endsWith('.closed') ? 'closed' : 'open';

export const getIssueRecord = async (issueUri: ResourceUri): Promise<HydratedRecord<ShTangledRepoIssue.Main>> => {
	const record = await appviewJson<AppviewRecordView<ShTangledRepoIssue.Main>>(
		'sh.tangled.repo.getIssue',
		{ issue: issueUri },
	);
	return appviewRecordToHydrated(record);
};

const hydrateIssueBacklinks = (refs: BacklinkRecordRef[]): Promise<Array<HydratedRecord<ShTangledRepoIssue.Main>>> =>
	hydrateBacklinksFromAppview<ShTangledRepoIssue.Main>(
		refs,
		'sh.tangled.repo.getIssues',
		'issues',
	);

const getLatestIssueState = async (issueUri: ResourceUri): Promise<'open' | 'closed'> => {
	const refs = await getBacklinks(issueUri, 'sh.tangled.repo.issue.state:issue');
	if (refs.length === 0) {
		return 'open';
	}

	const state = await fetchRecordValue<ShTangledRepoIssueState.Main>(latestBacklinkRefByRkey(refs)!);
	return normalizeIssueState(state.state);
};

const getIssueUpdatedAt = async (
	issue: HydratedRecord<ShTangledRepoIssue.Main>,
	stateUpdatedAt?: string,
): Promise<number> =>
	maxTimestamp(
		timestampFromDate(issue.value.createdAt),
		timestampFromTid(issue.rkey),
		timestampFromDate(stateUpdatedAt),
		await getLatestBacklinkTimestamp(issue.uri, [
			`${ISSUE_COMMENT_COLLECTION}:issue`,
			`${ISSUE_STATE_COLLECTION}:issue`,
		]),
	);

const listIssuesFromBacklinks = async (repo: RepoContext): Promise<IssueSummary[]> => {
	const refs = await getUniqueBacklinkRefs(repo.repoDid, [
		'sh.tangled.repo.issue:repoDid',
		'sh.tangled.repo.issue:repo',
	]);
	const issues = await hydrateIssueBacklinks(refs);
	const numbers = toNumberMap(issues);
	const states = await Promise.all(issues.map((issue) => getLatestIssueState(issue.uri)));

	const summaries = issues
		.map((issue, index) => ({
			...issue,
			number: numbers.get(issue.uri) ?? index + 1,
			state: states[index],
		}));

	return sortStatefulRecordsByUpdatedAt(summaries, (issue) => getIssueUpdatedAt(issue));
};

const listIssuesPageFromBacklinks = async (
	repo: RepoContext,
	options: StatefulPageOptions<'open' | 'closed'>,
): Promise<PaginatedResult<IssueSummary>> =>
	listBacklinkedRecordsPage<ShTangledRepoIssue.Main, 'open' | 'closed'>(
		repo.repoDid,
		['sh.tangled.repo.issue:repoDid', 'sh.tangled.repo.issue:repo'],
		options,
		getLatestIssueState,
		(issue) => getIssueUpdatedAt(issue),
		hydrateIssueBacklinks,
	);

const getIssueCountFromBacklinks = async (repo: RepoContext): Promise<number> =>
	(await getUniqueBacklinkRefs(repo.repoDid, [
		'sh.tangled.repo.issue:repoDid',
		'sh.tangled.repo.issue:repo',
	])).length;

const normalizeIssueComment = (hydrated: HydratedRecord<any>, issueUri: ResourceUri): IssueComment => {
	const val = hydrated.value;
	if (!val || typeof val !== 'object') {
		return hydrated as IssueComment;
	}
	const bodyText = (val.body && typeof val.body === 'object' && 'text' in val.body)
		? val.body.text
		: (typeof val.body === 'string' ? val.body : '');
	
	const replyTo = val.replyTo?.uri ?? val.replyTo;

	const mappedValue: ShTangledRepoIssueComment.Main = {
		$type: 'sh.tangled.repo.issue.comment',
		body: bodyText,
		createdAt: val.createdAt || new Date().toISOString(),
		issue: val.issue ?? val.subject?.uri ?? issueUri,
		replyTo,
		references: val.references ?? (replyTo ? [replyTo] : []),
	};

	return {
		...hydrated,
		value: mappedValue,
	};
};

const getIssueFromBacklinks = async (repo: RepoContext, issueRef: string): Promise<IssueDetail> => {
	const issues = await listIssuesFromBacklinks(repo);
	const issue = findNumberedRecord(issues, issueRef);
	if (!issue) {
		throw new Error(`Issue not found`);
	}

	const [newCommentRefs, legacyCommentRefs] = await Promise.all([
		getBacklinks(issue.uri, 'sh.tangled.feed.comment:subject'),
		getBacklinks(issue.uri, 'sh.tangled.repo.issue.comment:issue'),
	]);
	
	const commentRefs = [...newCommentRefs];
	const seenUris = new Set(commentRefs.map(r => `at://${r.did}/${r.collection}/${r.rkey}`));
	for (const ref of legacyCommentRefs) {
		const refUri = `at://${ref.did}/${ref.collection}/${ref.rkey}`;
		if (!seenUris.has(refUri)) {
			commentRefs.push(ref);
			seenUris.add(refUri);
		}
	}

	const hydrated = await hydrateBacklinks<any>(commentRefs);
	const comments = hydrated.map((record) => normalizeIssueComment(record, issue.uri));
	comments.sort(sortByCreatedAt);

	return {
		issue,
		comments,
	};
};

const listIssueCommentsFromAppview = async (issueUri: ResourceUri): Promise<IssueComment[]> => {
	const records = await listAllAppviewRecords<any>(
		'sh.tangled.feed.listComments',
		issueUri,
	);
	const comments = await Promise.all(records.map(async (record) => {
		const hydrated = await appviewRecordToHydrated<any>(record);
		return normalizeIssueComment(hydrated, issueUri);
	}));
	comments.sort(sortByCreatedAt);
	return comments;
};

const listIssuesFromAppview = async (repo: RepoContext): Promise<IssueSummary[]> => {
	const records = await listAllAppviewRecords<ShTangledRepoIssue.Main>(
		'sh.tangled.repo.listIssues',
		repo.repoDid,
	) as Array<AppviewStatefulRecordView<ShTangledRepoIssue.Main>>;
	const issues = await Promise.all(records.map((record) => appviewRecordToHydrated(record)));
	const numbers = toNumberMap(issues);

	const summaries = issues
		.map((issue, index) => ({
			...issue,
			number: numbers.get(issue.uri) ?? index + 1,
			state: normalizeIssueState(records[index].state),
		}));

	return sortStatefulRecordsByNewestRecord(summaries);
};

const listIssuesPageFromAppview = async (
	repo: RepoContext,
	options: StatefulPageOptions<'open' | 'closed'>,
): Promise<PaginatedResult<IssueSummary>> => {
	if (options.query?.trim()) {
		// Appview search can scope to repo/author/nsid. State is still derived
		// from separate state records, so filter state after each search hit.
		return listSearchedStatefulRecordsPage<ShTangledRepoIssue.Main, 'open' | 'closed'>(
			'sh.tangled.repo.issue',
			repo,
			options,
			getLatestIssueState,
		);
	}

	// Temporary until Bobbin exposes newest-first/reverse repo issue pagination.
	// This loads all appview issue records, but sorts without per-item backlinks.
	return listAppviewStatefulRecordsPage<ShTangledRepoIssue.Main, 'open' | 'closed'>(
		'sh.tangled.repo.listIssues',
		repo.repoDid,
		options,
		normalizeIssueState,
		options.author ? { author: options.author } : {},
		(issue) => Promise.resolve(getAppviewRecordSortTimestamp(issue)),
	);
};

const getIssueFromAppview = async (repo: RepoContext, issueRef: string): Promise<IssueDetail> => {
	const directUri = parseDirectRecordRef(issueRef, ISSUE_COLLECTION);
	if (directUri) {
		const issueRecord = await getIssueRecord(directUri);
		if (issueRecord.value.repo !== repo.repoDid) {
			throw new Error(`Issue not found`);
		}

		const issue: IssueSummary = {
			...issueRecord,
			number: 0,
			state: await getLatestIssueState(issueRecord.uri),
		};

		return {
			issue,
			comments: await listIssueCommentsFromAppview(issue.uri),
		};
	}

	const issues = await listIssuesFromAppview(repo);
	const issue = findNumberedRecord(issues, issueRef);
	if (!issue) {
		throw new Error(`Issue not found`);
	}

	return {
		issue,
		comments: await listIssueCommentsFromAppview(issue.uri),
	};
};

export const listIssues = async (repo: RepoContext): Promise<IssueSummary[]> =>
	appviewFallback(
		() => listIssuesFromAppview(repo),
		() => listIssuesFromBacklinks(repo),
	);

export const listIssuesPage = async (
	repo: RepoContext,
	options: StatefulPageOptions<'open' | 'closed'>,
): Promise<PaginatedResult<IssueSummary>> =>
	appviewFallback(
		() => listIssuesPageFromAppview(repo, options),
		() => listIssuesPageFromBacklinks(repo, options),
	);

export const getRepoIssueCount = async (repo: RepoContext): Promise<number> =>
	appviewFallback(
		() => getAppviewRecordCount('sh.tangled.repo.countIssues', repo.repoDid),
		() => getIssueCountFromBacklinks(repo),
	);

export const getIssue = async (repo: RepoContext, issueRef: string): Promise<IssueDetail> =>
	appviewFallback(
		() => getIssueFromAppview(repo, issueRef),
		() => getIssueFromBacklinks(repo, issueRef),
	);

export const createIssue = async (
	agent: OAuthUserAgent,
	repo: RepoContext,
	input: CreateIssueInput,
): Promise<RecordCreationResult> => {
	const rpc = createAuthRpc(agent);
	const record: ShTangledRepoIssue.Main = {
		$type: 'sh.tangled.repo.issue',
		repo: repo.repoDid,
		title: input.title.trim(),
		body: optionalString(input.body),
		createdAt: new Date().toISOString(),
		mentions: extractDids(input.body),
		references: extractAtUris(input.body),
	};

	const result = await ok(
		rpc.post('com.atproto.repo.createRecord', {
			input: {
				repo: agent.sub,
				collection: ISSUE_COLLECTION,
				record,
			},
		}),
	);

	return {
		uri: result.uri,
		rkey: parseAtUri(result.uri).rkey,
	};
};

export const createIssueComment = async (
	agent: OAuthUserAgent,
	issueUri: ResourceUri,
	issueCid: string,
	body: string,
	replyTo?: { uri: ResourceUri; cid: string },
): Promise<RecordCreationResult> => {
	const rpc = createAuthRpc(agent);
	const record = {
		$type: 'sh.tangled.feed.comment',
		subject: {
			uri: issueUri,
			cid: issueCid,
		},
		body: {
			$type: 'sh.tangled.markup.markdown',
			text: body.trim(),
		},
		createdAt: new Date().toISOString(),
		...(replyTo ? {
			replyTo: {
				uri: replyTo.uri,
				cid: replyTo.cid,
			}
		} : {}),
	};

	const result = await ok(
		rpc.post('com.atproto.repo.createRecord', {
			input: {
				repo: agent.sub,
				collection: FEED_COMMENT_COLLECTION,
				record,
			},
		}),
	);

	return {
		uri: result.uri,
		rkey: parseAtUri(result.uri).rkey,
	};
};

export const setIssueState = async (
	agent: OAuthUserAgent,
	issueUri: ResourceUri,
	state: 'open' | 'closed',
): Promise<RecordCreationResult> => {
	const rpc = createAuthRpc(agent);
	const record: ShTangledRepoIssueState.Main = {
		$type: 'sh.tangled.repo.issue.state',
		issue: issueUri,
		state:
			state === 'closed'
				? 'sh.tangled.repo.issue.state.closed'
				: 'sh.tangled.repo.issue.state.open',
	};

	const result = await ok(
		rpc.post('com.atproto.repo.createRecord', {
			input: {
				repo: agent.sub,
				collection: ISSUE_STATE_COLLECTION,
				record,
			},
		}),
	);

	return {
		uri: result.uri,
		rkey: parseAtUri(result.uri).rkey,
	};
};
