import { Attachment, denormalizeSystemInformation, type Piece, pieceCache, User } from "@/cache";
import { client } from "@/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useLoadingEffect } from "@/hooks/useLoading";
import { mapProp, Update, Updater } from "@/hooks/useStore";
import { timeout } from "@/lib/utils";
import { Label } from "@radix-ui/react-label";
import clsx from "clsx";
import { PieceId } from "common";
import { getMediaTypeForFilename } from "common/MediaType";
import { Cause, Effect, Option } from "effect";
import { constant } from "effect/Function";
import { Download, Loader2, Trash, UploadCloud } from "lucide-react";
import { DragEventHandler, FormEventHandler, MouseEvent, useCallback, useId, useState } from "react";
import { Link, useParams } from "react-router-dom";

export function Piece() {

	const id = PieceId(useParams().pieceId!);

	const { isLoading, error, data, setData } = useLoadingEffect(Effect.uninterruptible(pieceCache.get(id)));

	const setAttachments = useCallback((action: Update<readonly Attachment[]>) => {
		setData!(mapProp("attachments", action));
		Effect.runFork(pieceCache.invalidate(id));
	}, [setData]);

	if (isLoading) {
		return (
			<div className="w-full h-full overflow-hidden flex items-center justify-center">
				<div>Ładowanie…</div>
			</div>
		);
	}

	return (
		<div className="p-4 overflow-y-auto flex flex-wrap items-start gap-4">
			{error !== null ? (
				Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${error.value}`
			) : (<>
				<div className="flex flex-col gap-4 p-4 border rounded">
					<h3 className="font-bold text-lg">Utwór</h3>
					<PieceForm piece={data} />
					<h3 className="font-bold text-lg">Załączniki</h3>
					<AttachmentForm pieceId={id} setAttachments={setAttachments} />
				</div>
				<Attachments pieceId={id} attachments={data.attachments} setAttachments={setAttachments} />
			</>)}
		</div>
	);
}

namespace PieceForm {
	export interface Props {
		readonly piece: Piece;
	}
}

function PieceForm(props: PieceForm.Props) {

	const [name, setName] = useState(props.piece.name);
	const [composer, setComposer] = useState(() => Option.getOrElse(props.piece.composer, constant("")));
	const [lyricist, setLyricist] = useState(() => Option.getOrElse(props.piece.lyricist, constant("")));
	const [arranger, setArranger] = useState(() => Option.getOrElse(props.piece.arranger, constant("")));

	const nameId = useId();
	const composerId = useId();
	const lyricistId = useId();
	const arrangerId = useId();

	const [isLoading, setIsLoading] = useState(false);

	const onSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
		e.preventDefault();

		const delay = timeout(250);
		try {
			setIsLoading(true);

			const { error } = await client.piece({ pieceId: props.piece.pieceId }).put({
				name,
				composer: composer.length > 0 ? composer : null,
				lyricist: lyricist.length > 0 ? lyricist : null,
				arranger: arranger.length > 0 ? arranger : null,
			});

			if (error !== null) {
				console.error(error.value);
				return;
			}
		} finally {
			await delay;
			setIsLoading(false);
		}
	}

	return (
		<form className="flex flex-col gap-4" onSubmit={onSubmit}>
			<div className="grid items-baseline grid-cols-4 gap-4">
				<Label htmlFor={nameId} className="text-right">Tytuł</Label>
				<Input
					id={nameId}
					className="col-span-3"
					type="text"
					value={name}
					required
					onChange={(e) => setName(e.target.value)}
				/>
				<Label htmlFor={composerId} className="text-right">Kompozytor</Label>
				<Input
					id={composerId}
					className="col-span-3"
					type="text"
					value={composer}
					onChange={(e) => setComposer(e.target.value)}
				/>
				<Label htmlFor={lyricistId} className="text-right">Słowa</Label>
				<Input
					id={lyricistId}
					className="col-span-3"
					type="text"
					value={lyricist}
					onChange={(e) => setLyricist(e.target.value)}
				/>
				<Label htmlFor={arrangerId} className="text-right">Opracowanie</Label>
				<Input
					id={arrangerId}
					className="col-span-3"
					type="text"
					value={arranger}
					onChange={(e) => setArranger(e.target.value)}
				/>
			</div>
			<div className="flex flex-col-reverse items-end">
				<Button type="submit" disabled={isLoading}>
					{isLoading && <Loader2 className="animate-spin" />}
					Zapisz
				</Button>
			</div>
		</form>
	);
}

namespace Attachments {
	export interface Props {
		readonly pieceId: PieceId;
		readonly attachments: readonly Attachment[];
		readonly setAttachments: Updater<readonly Attachment[]>;
	}
}

function Attachments(props: Attachments.Props) {
	return (
		<div className="grow">
			<Table>
				<TableHeader>
					<TableRow>
						<TableHead>Nazwa pliku</TableHead>
						<TableHead>Typ</TableHead>
						<TableHead className="text-center">Dodano</TableHead>
						<TableHead className="text-center">Zmodyfikowano</TableHead>
						<TableHead className="text-center">Akcje</TableHead>
					</TableRow>
				</TableHeader>
				<TableBody>
					{props.attachments.map((attachment) => (
						<AttachmentRow
							key={attachment.attachmentId}
							attachment={attachment}
							setAttachments={props.setAttachments}
						/>
					))}
				</TableBody>
			</Table>
		</div>
	);
}

namespace AttachmentRow {
	export interface Props {
		readonly attachment: Attachment;
		readonly setAttachments: Updater<readonly Attachment[]>;
	}
}

function AttachmentRow(props: AttachmentRow.Props) {

	const download = useCallback(async () => {
		const { error, data } = await client
			.piece({ pieceId: props.attachment.pieceId })
			.attachment({ attachmentId: props.attachment.attachmentId })
			.get()

		if (error !== null) {
			console.error(error.value);
			return;
		}

		const url = URL.createObjectURL(data);
		const a = document.createElement("a");
		a.href = url;
		a.download = props.attachment.filename; // TODO Use `data.name` after Content-Disposition parser is implemented
		a.click();
		URL.revokeObjectURL(url);
	}, [props.attachment.attachmentId, props.attachment.pieceId]);

	const open = useCallback(async (event: MouseEvent<HTMLAnchorElement>) => {
		if (props.attachment.mediaType !== "application/pdf") {
			return;
		}

		event.preventDefault();

		const { error, data } = await client
			.piece({ pieceId: props.attachment.pieceId })
			.attachment({ attachmentId: props.attachment.attachmentId })
			.get();

		if (error !== null) {
			console.error(error.value);
			return;
		}

		const url = URL.createObjectURL(data);
		window.open(url, "_target");
		URL.revokeObjectURL(url);
	}, [props.attachment.mediaType, props.attachment.attachmentId, props.attachment.pieceId]);

	const doDelete = useCallback(async () => {

		const { error } = await client
			.piece({ pieceId: props.attachment.pieceId })
			.attachment({ attachmentId: props.attachment.attachmentId })
			.delete();

		if (error !== null) {
			console.error(error.value);
			return;
		}

		props.setAttachments((prev) => prev.filter((a) => a.attachmentId !== props.attachment.attachmentId));

	}, [props.attachment.attachmentId, props.attachment.pieceId]);

	return (
		<TableRow>
			<TableCell>
				{props.attachment.mediaType === "application/vnd.recordare.musicxml"
					|| props.attachment.mediaType === "application/vnd.recordare.musicxml+xml"
					|| props.attachment.mediaType === "application/pdf" ? (
					<Link className="underline" to={`attachment/${props.attachment.attachmentId}`} onClick={open}>
						{props.attachment.filename}
					</Link>
				) : (
					props.attachment.filename
				)}
			</TableCell>
			<TableCell>
				{props.attachment.mediaType}
			</TableCell>
			<TableCell className="text-center text-xs">
				{props.attachment.createdAt}
				{Option.isSome(props.attachment.createdBy) && <><br />przez {props.attachment.createdBy.value.username}</>}
			</TableCell>
			<TableCell className="text-center text-xs">
				{Option.isNone(props.attachment.modifiedAt) && Option.isNone(props.attachment.modifiedBy) ? "\u2014"
					: Option.isSome(props.attachment.modifiedAt) && Option.isNone(props.attachment.modifiedBy) ? props.attachment.modifiedAt.value
						: Option.isNone(props.attachment.modifiedAt) ? `przez ${(props.attachment.modifiedBy as Option.Some<User>).value.username}`
							: <>{props.attachment.modifiedAt.value}<br />przez {(props.attachment.modifiedBy as Option.Some<User>).value.username}</>}
			</TableCell>
			<TableCell className="text-center flex justify-center gap-4">
				<Button type="button" variant="ghost" size="icon" title="Pobierz" onClick={download}>
					<Download />
				</Button>
				<Button type="button" variant="ghost" size="icon" title="Usuń" onClick={doDelete}>
					<Trash />
				</Button>
			</TableCell>
		</TableRow>
	);
}

namespace AttachmentForm {
	export interface Props {
		readonly pieceId: PieceId;
		readonly setAttachments: Updater<readonly Attachment[]>;
	}
}

function AttachmentForm(props: AttachmentForm.Props) {

	const [isLoading, setIsLoading] = useState(false);

	const onDragOver: DragEventHandler<HTMLElement> = async (e) => {
		e.preventDefault();
		e.dataTransfer.dropEffect = "copy";
	};

	const onDrop: DragEventHandler<HTMLElement> = async (e) => {
		e.preventDefault();

		if (isLoading) {
			return;
		}

		const delay = timeout(250);
		try {
			setIsLoading(true);

			for (const file of e.dataTransfer.files) {
				const mediaType = getMediaTypeForFilename(file.name);
				if (mediaType === undefined) {
					continue;
				}

				const { data, error } = await client.piece({ pieceId: props.pieceId }).attachment.post({
					filename: file.name,
					mediaType,
					data: file,
				});

				if (error !== null) {
					console.error(error.value);
					continue;
				}

				const attachment = await Effect.runPromise(denormalizeSystemInformation(data));

				props.setAttachments((prev) => {
					const next = [...prev, attachment];
					next.sort((a, b) => a.filename.localeCompare(b.filename));
					return next;
				});
			}
		} finally {
			await delay;
			setIsLoading(false);
		}
	}

	return (
		<div
			className={clsx(
				"flex flex-col border rounded px-4 py-8 items-center select-none bg-stone-100",
				isLoading && "text-stone-400",
			)}
			onDragOver={onDragOver}
			onDrop={onDrop}
		>
			{isLoading ? (<>
				<Loader2 className="animate-spin" />
				<div>Wysyłanie załączników…</div>
			</>) : (<>
				<UploadCloud />
				<div>Przeciągnij i upuść tutaj załączniki</div>
			</>)}
		</div>
	);
}
