import { A, useParams, useNavigate } from '@solidjs/router';
import { createQuery, useQueryClient } from '../lib/atproto-query';
import { Show, Switch, Match, type Component, createSignal, createMemo, createEffect, onCleanup } from 'solid-js';
import type { ResourceUri } from '@atcute/lexicons/syntax';
import { Avatar, ErrorState, inputStyles, textareaStyles } from '../components/common';
import { CodeView, CommentComposer, CommentThreadsSection, StringPageSkeleton, RepoFormSkeleton } from '../components/repo';
import {
	getString,
	deleteString,
	updateString,
	createString,
	listStringComments,
	createStringComment,
	getStringStarSummary,
	createStringStar,
	deleteStringStar,
	type StringComment,
} from '../lib/api/strings';
import { formatRelativeTime, getErrorMessage } from '../lib/repo-utils';
import { useAuth } from '../lib/auth';
import { Pencil, Trash2, Star, LoaderCircle, ArrowUp, X } from 'lucide-solid';
import clsx from 'clsx';

interface StringCommentThread {
	item: StringComment;
	replies: StringComment[];
}

const buildStringCommentThreads = (comments: StringComment[]): StringCommentThread[] => {
	const byUri = new Map(comments.map((comment) => [comment.uri, comment]));
	const topLevel: StringComment[] = [];
	const repliesByRoot = new Map<string, StringComment[]>();

	const findRoot = (comment: StringComment): StringComment | null => {
		let current = comment;
		const visited = new Set<string>([comment.uri]);
		while (true) {
			const replyToUri = typeof current.value.replyTo === 'string'
				? current.value.replyTo
				: (current.value.replyTo as { uri?: string } | undefined)?.uri;
			if (!replyToUri) {
				return current;
			}
			const parent = byUri.get(replyToUri as ResourceUri);
			if (!parent) {
				return current;
			}
			if (visited.has(parent.uri)) {
				return current === comment ? null : current;
			}
			visited.add(parent.uri);
			current = parent;
		}
	};

	for (const comment of comments) {
		const root = findRoot(comment);
		if (!root || root.uri === comment.uri) {
			topLevel.push(comment);
		} else {
			repliesByRoot.set(root.uri, [...(repliesByRoot.get(root.uri) ?? []), comment]);
		}
	}

	return topLevel.map((item) => ({
		item,
		replies: repliesByRoot.get(item.uri) ?? [],
	}));
};

export const StringPage: Component = () => {
	const params = useParams<{ actor: string; rkey: string }>();
	const auth = useAuth();
	const navigate = useNavigate();
	const queryClient = useQueryClient();

	// Deleting Signal
	const [deleting, setDeleting] = createSignal(false);

	// Star working Signal
	const [starWorking, setStarWorking] = createSignal(false);

	// Comment signals
	const [commentBody, setCommentBody] = createSignal('');
	const [commentWorking, setCommentWorking] = createSignal(false);
	const [commentError, setCommentError] = createSignal<string | null>(null);

	const stringQuery = createQuery(() => ({
		queryKey: ['string-detail', params.actor, params.rkey],
		queryFn: () => getString(params.actor, params.rkey),
	}));

	const starSummaryQuery = createQuery(() => ({
		queryKey: ['string-star-summary', stringQuery.data?.record.uri],
		enabled: !!stringQuery.data?.record.uri,
		queryFn: () => getStringStarSummary(stringQuery.data!.record.uri, auth.currentDid()),
	}));

	const commentsQuery = createQuery(() => ({
		queryKey: ['string-comments', stringQuery.data?.record.uri],
		enabled: !!stringQuery.data?.record.uri,
		queryFn: () => listStringComments(stringQuery.data!.record.uri),
	}));

	const commentThreads = createMemo(() => {
		const comments = commentsQuery.data ?? [];
		return buildStringCommentThreads(comments);
	});

	const handleDelete = async () => {
		const agent = auth.agent();
		const data = stringQuery.data;
		if (!agent || !data) return;

		if (!confirm(`are you sure you want to delete the string "${data.record.value.filename}"?`)) {
			return;
		}

		setDeleting(true);
		try {
			await deleteString(agent, params.rkey);
			const ownerId = data.owner.handle || data.owner.did;
			navigate(`/${ownerId}?tab=strings`);
		} catch (err) {
			alert(getErrorMessage(err));
		} finally {
			setDeleting(false);
		}
	};

	const handleStarToggle = async () => {
		const agent = auth.agent();
		const summary = starSummaryQuery.data;
		const record = stringQuery.data?.record;
		if (!agent || !summary || !record || starWorking()) return;

		setStarWorking(true);
		try {
			if (summary.isStarred && summary.currentUserStarRkey) {
				await deleteStringStar(agent, summary.currentUserStarRkey);
			} else {
				await createStringStar(agent, record.uri);
			}
			await queryClient.invalidateQueries({ queryKey: ['string-star-summary', record.uri] });
		} catch (err) {
			console.error('failed to toggle star', err);
		} finally {
			setStarWorking(false);
		}
	};

	const handleCommentSubmit = async (e: SubmitEvent) => {
		e.preventDefault();
		const agent = auth.agent();
		const record = stringQuery.data?.record;
		if (!agent || !record || !commentBody().trim() || commentWorking()) return;

		setCommentWorking(true);
		setCommentError(null);
		try {
			await createStringComment(agent, record.uri, record.cid, commentBody());
			setCommentBody('');
			await queryClient.invalidateQueries({ queryKey: ['string-comments', record.uri] });
		} catch (err) {
			setCommentError(getErrorMessage(err));
		} finally {
			setCommentWorking(false);
		}
	};

	const starBusy = () => starWorking() || starSummaryQuery.isLoading;

	return (
		<Switch>
			<Match when={stringQuery.isLoading}>
				<StringPageSkeleton />
			</Match>
			<Match when={stringQuery.error}>
				<div class="p-6 bg-white dark:bg-gray-800 rounded shadow-sm">
					<ErrorState message={getErrorMessage(stringQuery.error).toLowerCase()} />
				</div>
			</Match>
			<Match when={stringQuery.data}>
				{(data) => {
					const { owner, record } = data();
					const lineCount = () => record.value.contents.split('\n').length;
					const byteCount = () => new TextEncoder().encode(record.value.contents).length;
					const formattedSize = () => {
						const bytes = byteCount();
						if (bytes < 1024) return `${bytes} B`;
						const kb = bytes / 1024;
						if (kb < 1024) return `${kb.toFixed(1)} KB`;
						return `${(kb / 1024).toFixed(1)} MB`;
					};
					const timeAgo = () => formatRelativeTime(record.value.createdAt);

					const [rawUrl, setRawUrl] = createSignal<string>();
					createEffect(() => {
						const webBlob = new Blob([record.value.contents], { type: 'text/plain;charset=utf-8' });
						const url = URL.createObjectURL(webBlob);
						setRawUrl(url);
						onCleanup(() => URL.revokeObjectURL(url));
					});

					return (
						<div class="flex flex-col gap-4">
							<section id="string-header" class="mb-2 py-2 px-4 dark:text-white">
								<div class="text-lg flex flex-col sm:flex-row items-start gap-4 justify-between">
									<div class="flex flex-col gap-2">
										<div class="flex items-center gap-2 flex-wrap">
											<A href={`/${owner.handle || owner.did}`} class="flex items-center gap-1 hover:underline text-black dark:text-white font-medium">
												<Avatar did={owner.did} size="size-6" />
												<span>{owner.handle || owner.did}</span>
											</A>
											<span class="select-none text-gray-400">/</span>
											<A href={`/strings/${owner.handle || owner.did}/${record.rkey}`} class="font-bold hover:underline text-black dark:text-white">
												{record.value.filename}
											</A>
										</div>
										<span class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-600 dark:text-gray-300">
											<Show when={record.value.description} fallback={<span class="italic">this string has no description</span>}>
												{record.value.description}
											</Show>
										</span>
									</div>

									<div class="w-full sm:w-fit flex items-center gap-2 z-auto">
										<Show when={auth.currentDid() === owner.did}>
											<A
												class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group border border-gray-200 dark:border-gray-700 rounded-sm px-3 py-1.5 bg-white dark:bg-gray-800"
												href={`/strings/${owner.handle || owner.did}/${record.rkey}/edit`}
											>
												<Pencil class="w-4 h-4" />
												<span class="hidden md:inline">edit</span>
											</A>
											<button
												class="btn text-sm text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 flex items-center gap-2 group border border-gray-200 dark:border-gray-700 rounded-sm px-3 py-1.5 bg-white dark:bg-gray-800"
												onClick={handleDelete}
												disabled={deleting()}
											>
												<Show when={deleting()} fallback={<Trash2 class="w-4 h-4" />}>
													<LoaderCircle class="w-4 h-4 animate-spin" />
												</Show>
												<span class="hidden md:inline">delete</span>
											</button>
										</Show>
										<Show when={starSummaryQuery.data}>
											<div class="btn-group w-full sm:w-auto flex">
												<button
													class="btn-group-item active flex-1 gap-1 group flex items-center justify-center"
													disabled={starBusy()}
													onClick={handleStarToggle}
												>
													<Show when={starWorking()} fallback={
														<Star class={clsx("w-4 h-4 shrink-0", starSummaryQuery.data?.isStarred ? "fill-current text-yellow-500" : "")} />
													}>
														<LoaderCircle class="w-4 h-4 shrink-0 animate-spin" />
													</Show>
													<span>{starSummaryQuery.data?.isStarred ? 'unstar' : 'star'}</span>
												</button>
												<span class="btn-group-item cursor-default flex items-center justify-center px-3">
													{starSummaryQuery.data?.count ?? 0}
												</span>
											</div>
										</Show>
									</div>
								</div>
							</section>

							<section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full dark:text-white shadow-sm">
								<div class="flex flex-col md:flex-row md:justify-between md:items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
									<span>
										{record.value.filename}
										<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
										<span>
											{timeAgo()}
										</span>
									</span>
									<div>
										<span>{lineCount()} lines</span>
										<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
										<span>{formattedSize()}</span>
										<Show when={rawUrl()}>
											<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
											<a href={rawUrl()!} target="_blank" rel="noreferrer" class="hover:underline">view raw</a>
										</Show>
									</div>
								</div>
								<div class="overflow-x-auto overflow-y-hidden relative">
									<CodeView id="blob-contents" text={record.value.contents} wrap={false} path={record.value.filename} />
								</div>
							</section>

							{/* Comments section */}
							<div class="flex flex-col gap-4 px-2 md:px-0">
								<Show when={commentsQuery.data}>
									<CommentThreadsSection
										threads={commentThreads()}
										authorDid={owner.did}
										onReplySubmit={async (body, parent) => {
											const agent = auth.agent();
											if (!agent) return;
											await createStringComment(
												agent,
												record.uri,
												record.cid,
												body,
												{ uri: parent.uri as ResourceUri, cid: parent.cid ?? '' }
											);
											await queryClient.invalidateQueries({ queryKey: ['string-comments', record.uri] });
										}}
									/>
								</Show>

								<Show when={auth.agent()}>
									<CommentComposer
										value={commentBody()}
										onValueChange={setCommentBody}
										onSubmit={handleCommentSubmit}
										working={commentWorking()}
										error={commentError()}
										placeholder="add to the discussion. markdown is supported."
										submitLabel="comment"
									/>
								</Show>
							</div>
						</div>
					);
				}}
			</Match>
		</Switch>
	);
};

export const NewStringPage: Component = () => {
	const auth = useAuth();
	const navigate = useNavigate();
	const queryClient = useQueryClient();

	const [filename, setFilename] = createSignal('');
	const [description, setDescription] = createSignal('');
	const [contents, setContents] = createSignal('');
	const [saving, setSaving] = createSignal(false);
	const [saveError, setSaveError] = createSignal<string | null>(null);

	const lines = () => {
		const val = contents();
		return val === '' ? 0 : val.split('\n').length;
	};
	const bytes = () => {
		return new TextEncoder().encode(contents()).length;
	};

	const handlePublish = async (e: SubmitEvent) => {
		e.preventDefault();
		const agent = auth.agent();
		if (!agent) return;

		setSaving(true);
		setSaveError(null);
		try {
			const result = await createString(agent, {
				filename: filename(),
				description: description(),
				contents: contents(),
			});
			await queryClient.invalidateQueries({ queryKey: ['profile-strings', auth.currentDid()] });
			const ownerId = auth.currentDid();
			navigate(`/strings/${ownerId}/${result.rkey}`);
		} catch (err) {
			setSaveError(getErrorMessage(err));
		} finally {
			setSaving(false);
		}
	};

	return (
		<Show
			when={auth.currentDid()}
			fallback={
				<div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded p-6 relative flex gap-2 items-center dark:text-white">
					please log in to create a new string.
				</div>
			}
		>
			<div class="px-6 py-2 mb-4">
				<p class="text-xl font-bold dark:text-white mb-1">create a new string</p>
				<p class="text-gray-600 dark:text-gray-400 mb-1">store and share code snippets with ease.</p>
			</div>

			<form
				onSubmit={handlePublish}
				class="p-6 pb-4 dark:text-white flex flex-col gap-2 bg-white dark:bg-gray-800 drop-shadow-sm rounded border border-gray-200 dark:border-gray-700"
			>
				{saveError() && <p class="text-red-500 dark:text-red-400 text-sm mb-2">{saveError()}</p>}
				<div class="flex flex-col md:flex-row md:items-center gap-2">
					<input
						type="text"
						id="filename"
						name="filename"
						placeholder="filename"
						required
						value={filename()}
						onInput={(e) => setFilename(e.currentTarget.value)}
						class={clsx(inputStyles(), 'md:max-w-64')}
					/>
					<input
						type="text"
						id="description"
						name="description"
						value={description()}
						onInput={(e) => setDescription(e.currentTarget.value)}
						placeholder="description ..."
						class={clsx(inputStyles(), 'flex-1')}
					/>
				</div>
				<textarea
					name="content"
					id="content-textarea"
					wrap="off"
					class={clsx(textareaStyles(), 'font-mono text-sm')}
					rows={20}
					spellcheck={false}
					placeholder="paste your string here!"
					value={contents()}
					onInput={(e) => setContents(e.currentTarget.value)}
					required
				/>
				<div class="flex justify-between items-center">
					<div id="content-stats" class="text-sm text-gray-500 dark:text-gray-400">
						<span id="line-count">{lines()} line{lines() !== 1 ? 's' : ''}</span>
						<span class="select-none px-1 [&:before]:content-['·']"></span>
						<span id="byte-count">{bytes()} byte{bytes() !== 1 ? 's' : ''}</span>
					</div>
					<div id="actions" class="flex gap-2 items-center">
						<button
							type="submit"
							id="new-button"
							class="w-fit btn-create group inline-flex items-center gap-2 text-white"
							disabled={saving()}
						>
							<span class="inline-flex items-center gap-2">
								<ArrowUp class="w-4 h-4" />
								publish
							</span>
							<Show when={saving()}>
								<span class="pl-2 inline-block">
									<LoaderCircle class="w-4 h-4 animate-spin" />
								</span>
							</Show>
						</button>
					</div>
				</div>
			</form>
		</Show>
	);
};

export const EditStringPage: Component = () => {
	const params = useParams<{ actor: string; rkey: string }>();
	const auth = useAuth();
	const navigate = useNavigate();
	const queryClient = useQueryClient();

	const [filename, setFilename] = createSignal('');
	const [description, setDescription] = createSignal('');
	const [contents, setContents] = createSignal('');
	const [saving, setSaving] = createSignal(false);
	const [saveError, setSaveError] = createSignal<string | null>(null);

	const stringQuery = createQuery(() => ({
		queryKey: ['string-detail', params.actor, params.rkey],
		queryFn: () => getString(params.actor, params.rkey),
	}));

	createEffect(() => {
		const data = stringQuery.data;
		if (data) {
			setFilename(data.record.value.filename);
			setDescription(data.record.value.description ?? '');
			setContents(data.record.value.contents);
		}
	});

	const isOwner = () => {
		const data = stringQuery.data;
		return data && auth.currentDid() === data.owner.did;
	};

	const lines = () => {
		const val = contents();
		return val === '' ? 0 : val.split('\n').length;
	};
	const bytes = () => {
		return new TextEncoder().encode(contents()).length;
	};

	const handleSave = async (e: SubmitEvent) => {
		e.preventDefault();
		const agent = auth.agent();
		const data = stringQuery.data;
		if (!agent || !data) return;

		setSaving(true);
		setSaveError(null);
		try {
			await updateString(
				agent,
				params.rkey,
				{
					filename: filename(),
					description: description(),
					contents: contents(),
				},
				data.record.cid,
			);
			await queryClient.invalidateQueries({ queryKey: ['string-detail', params.actor, params.rkey] });
			navigate(`/strings/${params.actor}/${params.rkey}`);
		} catch (err) {
			setSaveError(getErrorMessage(err));
		} finally {
			setSaving(false);
		}
	};

	return (
		<Switch>
			<Match when={stringQuery.isLoading}>
				<RepoFormSkeleton titleWidth="w-32" />
			</Match>
			<Match when={stringQuery.error}>
				<div class="p-6 bg-white dark:bg-gray-800 rounded shadow-sm">
					<ErrorState message={getErrorMessage(stringQuery.error).toLowerCase()} />
				</div>
			</Match>
			<Match when={stringQuery.data && !isOwner()}>
				<div class="p-6 bg-white dark:bg-gray-800 rounded shadow-sm">
					<ErrorState message="you are not authorized to edit this string." />
				</div>
			</Match>
			<Match when={stringQuery.data && isOwner()}>
				<div>
					<div class="px-6 py-2 mb-4">
						<p class="text-xl font-bold dark:text-white">edit string</p>
					</div>

					<form
						onSubmit={handleSave}
						class="p-6 pb-4 dark:text-white flex flex-col gap-2 bg-white dark:bg-gray-800 drop-shadow-sm rounded border border-gray-200 dark:border-gray-700"
					>
						{saveError() && <p class="text-red-500 dark:text-red-400 text-sm mb-2">{saveError()}</p>}
						<div class="flex flex-col md:flex-row md:items-center gap-2">
							<input
								type="text"
								id="filename"
								name="filename"
								placeholder="filename"
								required
								value={filename()}
								onInput={(e) => setFilename(e.currentTarget.value)}
								class={clsx(inputStyles(), 'md:max-w-64')}
							/>
							<input
								type="text"
								id="description"
								name="description"
								value={description()}
								onInput={(e) => setDescription(e.currentTarget.value)}
								placeholder="description ..."
								class={clsx(inputStyles(), 'flex-1')}
							/>
						</div>
						<textarea
							name="content"
							id="content-textarea"
							wrap="off"
							class={clsx(textareaStyles(), 'font-mono text-sm')}
							rows={20}
							spellcheck={false}
							placeholder="paste your string here!"
							value={contents()}
							onInput={(e) => setContents(e.currentTarget.value)}
							required
						/>
						<div class="flex justify-between items-center">
							<div id="content-stats" class="text-sm text-gray-500 dark:text-gray-400">
								<span id="line-count">{lines()} line{lines() !== 1 ? 's' : ''}</span>
								<span class="select-none px-1 [&:before]:content-['·']"></span>
								<span id="byte-count">{bytes()} byte{bytes() !== 1 ? 's' : ''}</span>
							</div>
							<div id="actions" class="flex gap-2 items-center">
								<A
									class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
									href={`/strings/${params.actor}/${params.rkey}`}
								>
									<X class="size-4" />
									<span class="hidden md:inline">cancel</span>
								</A>
								<button
									type="submit"
									id="new-button"
									class="w-fit btn-create group inline-flex items-center gap-2 text-white"
									disabled={saving()}
								>
									<span class="inline-flex items-center gap-2">
										<ArrowUp class="w-4 h-4" />
										publish
									</span>
									<Show when={saving()}>
										<span class="pl-2 inline-block">
											<LoaderCircle class="w-4 h-4 animate-spin" />
										</span>
									</Show>
								</button>
							</div>
						</div>
					</form>
				</div>
			</Match>
		</Switch>
	);
};

export const RawStringPage: Component = () => {
	const params = useParams<{ actor: string; rkey: string }>();

	const stringQuery = createQuery(() => ({
		queryKey: ['string-detail', params.actor, params.rkey],
		queryFn: () => getString(params.actor, params.rkey),
	}));

	return (
		<Switch>
			<Match when={stringQuery.isLoading}>
				<div class="p-4 font-mono text-sm">loading raw content...</div>
			</Match>
			<Match when={stringQuery.error}>
				<div class="p-4 font-mono text-sm text-red-500">
					failed to load: {getErrorMessage(stringQuery.error).toLowerCase()}
				</div>
			</Match>
			<Match when={stringQuery.data}>
				{(data) => (
					<pre style="white-space: pre-wrap; word-wrap: break-word; font-family: monospace;" class="p-4 bg-white dark:bg-gray-950 dark:text-white">
						{data().record.value.contents}
					</pre>
				)}
			</Match>
		</Switch>
	);
};
