import { denormalizePiece, pieceCache } from "@/cache";
import { client } from "@/client";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useCache } from "@/hooks/useCache";
import { useLoading } from "@/hooks/useLoading";
import { authors, created, DEBOUNCE, modified, SAVE_DELAY } from "@/snippets";
import clsx from "clsx";
import { PieceId, Updater } from "common";
import * as Body from "common/Body";
import { getMediaTypeForFilename } from "common/MediaType";
import { Array, Cause, Effect, Fiber, Iterable, Match, Option, Order, pipe, Predicate, Scope, SortedMap } from "effect";
import { Import, Loader2, Plus } from "lucide-react";
import { DragEventHandler, FormEventHandler, useId, useRef, useState } from "react";
import { Link, useNavigate } from "react-router-dom";

export function Pieces() {

	const [name, setName] = useState("");
	const [author, setAuthor] = useState("");

	const [importDialogOpen, setImportDialogOpen] = useState(false);

	const debounce = useRef(Effect.void);

	const { isLoading, error, data: pieceIds, refresh } = useLoading(Effect.gen(function* () {
		yield* debounce.current;
		const data = yield* client.queryPieces({
			name: name !== "" ? Option.some(name) : Option.none(),
			author: author !== "" ? Option.some(author) : Option.none(),
			offset: 0,
			limit: 100,
		});
		return data;
	}), [name, author]);

	return (
		<div className="p-4 overflow-y-auto flex flex-col items-start gap-4">
			<div className="flex flex-row gap-4">
				<Dialog>
					<DialogTrigger asChild>
						<Button variant="outline">
							<Plus />Dodaj utwór
						</Button>
					</DialogTrigger>
					<AddPieceDialogContent />
				</Dialog>
				<Dialog open={importDialogOpen} onOpenChange={setImportDialogOpen}>
					<DialogTrigger asChild>
						<Button variant="outline">
							<Import />Importuj utwory
						</Button>
					</DialogTrigger>
					<ImportPiecesDialogContent setDialogOpen={setImportDialogOpen} refresh={refresh} />
				</Dialog>
				<Input
					className="w-[32ch]"
					type="text"
					placeholder="Tytuł"
					value={name}
					onChange={(e) => {
						setName(e.target.value);
						debounce.current = DEBOUNCE;
					}}
				/>
				<Input
					className="w-[32ch]"
					type="text"
					placeholder="Twórcy"
					value={author}
					onChange={(e) => {
						setAuthor(e.target.value);
						debounce.current = DEBOUNCE;
					}}
				/>
			</div>
			<Table>
				<TableHeader className="bg-white sticky top-0">
					<TableRow>
						<TableHead>Tytuł</TableHead>
						<TableHead>Twórcy</TableHead>
						<TableHead className="text-center">Dodano</TableHead>
						<TableHead className="text-center">Zmodyfikowano</TableHead>
					</TableRow>
				</TableHeader>
				<TableBody>
					{isLoading ? (
						<TableRow>
							<TableCell colSpan={4} >
								<div className="flex items-center justify-center gap-2">
									<Loader2 className="animate-spin" />
									Ładowanie…
								</div>
							</TableCell>
						</TableRow>
					) : error !== null ? (
						<TableRow>
							<TableCell colSpan={4} className="text-center">
								{Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error)}`}
							</TableCell>
						</TableRow>
					) : (
						pieceIds.map((pieceId) => <PieceRow key={pieceId} pieceId={pieceId} />)
					)}
				</TableBody>
			</Table>
		</div>
	);
}

namespace PieceRow {
	export interface Props {
		readonly pieceId: PieceId;
	}
}

function PieceRow(props: PieceRow.Props) {

	const { isLoading, error, data: piece } = useCache(pieceCache, props.pieceId);

	if (isLoading) {
		return (
			<TableRow>
				<TableCell colSpan={4}>Ładowanie…</TableCell>
			</TableRow>
		);
	}

	if (error !== null) {
		return (
			<TableRow>
				<TableCell colSpan={4}>
					Wystąpił błąd: {Match.value(error).pipe(
						Match.tag("FetchError", () => "Nie można połączyć się z serwerem"),
						Match.tag("NotFound", () => "Utwór nie istnieje"),
						Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"),
						Match.tag("Unauthorized", () => "Nie posiadasz uprawnień"),
						Match.exhaustive,
					)}
				</TableCell>
			</TableRow>
		);
	}

	return (
		<TableRow>
			<TableCell>
				<Link className="underline" to={piece.pieceId}>{piece.name}</Link>
			</TableCell>
			<TableCell>
				{authors(piece)}
			</TableCell>
			<TableCell className="text-center text-xs">
				{created(piece)}
			</TableCell>
			<TableCell className="text-center text-xs">
				{modified(piece)}
			</TableCell>
		</TableRow>
	);
}

function AddPieceDialogContent() {

	const navigate = useNavigate();

	const [name, setName] = useState("");
	const [composer, setComposer] = useState("");
	const [lyricist, setLyricist] = useState("");
	const [arranger, setArranger] = useState("");

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

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

	const onSubmit: FormEventHandler<HTMLFormElement> = (e) => Effect.gen(function* () {
		e.preventDefault();

		yield* Effect.scopedWith((scope) => Effect.gen(function* () {

			yield* Scope.addFinalizer(scope, Effect.sync(() => setIsLoading(false)));

			setIsLoading(true);

			const { pieceId } = yield* pipe(
				client.createPiece({
					name,
					composer: composer.length > 0 ? Option.some(composer) : Option.none(),
					lyricist: lyricist.length > 0 ? Option.some(lyricist) : Option.none(),
					arranger: arranger.length > 0 ? Option.some(arranger) : Option.none(),
				}),
				Effect.flatMap(denormalizePiece),
				Effect.tap((piece) => pieceCache.create(piece.pieceId, piece)),
			);

			navigate(pieceId);
		}));
	}).pipe(Effect.runPromise);

	return (
		<DialogContent>
			<form onSubmit={onSubmit}>
				<DialogHeader>
					<DialogTitle>Dodaj utwór</DialogTitle>
				</DialogHeader>
				<div className="grid items-baseline grid-cols-4 gap-4 py-4">
					<Label htmlFor={nameId} className="text-right">Tytuł</Label>
					<Input
						id={nameId}
						className="col-span-3"
						type="text"
						value={name}
						autoFocus
						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>
				<DialogFooter>
					<Button type="submit" disabled={isLoading}>
						{isLoading && <Loader2 className="animate-spin" />}
						Dodaj
					</Button>
				</DialogFooter>
			</form>
		</DialogContent>
	);
}

namespace ImportPiecesDialogContent {
	export interface Props {
		readonly setDialogOpen: Updater<boolean>;
		readonly refresh: Effect.Effect<void>;
	}
}

function ImportPiecesDialogContent(props: ImportPiecesDialogContent.Props) {

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

	const [files, setFiles] = useState<SortedMap.SortedMap<string, readonly File[]>>(() => SortedMap.empty(Order.string));

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

	const onDrop: DragEventHandler<HTMLElement> = (e) => Effect.gen(function* () {
		e.preventDefault();

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

				const name = file.name.substring(0, file.name.lastIndexOf("."));

				const entry = pipe(
					value,
					SortedMap.get(name),
					Option.getOrElse(Array.empty),
					Array.append(file),
				);

				value = SortedMap.set(value, name, entry);
			}

			return value;
		});
	}).pipe(Effect.runPromise);

	const onSubmit: FormEventHandler<HTMLFormElement> = (e) => Effect.gen(function* () {
		e.preventDefault();

		if (isLoading) {
			return;
		}

		const delay = yield* Effect.fork(SAVE_DELAY);

		yield* Effect.scopedWith((scope) => Effect.gen(function* () {

			yield* Scope.addFinalizer(scope, Effect.gen(function* () {
				yield* Fiber.join(delay);
				setIsLoading(false);
			}));

			setIsLoading(true);

			yield* pipe(
				files,
				Iterable.map(([name, files]) => Effect.gen(function* () {

					const piece = yield* pipe(
						client.createPiece({
							name,
							composer: Option.none(),
							arranger: Option.none(),
							lyricist: Option.none(),
						}),
					);

					const attachments = yield* pipe(
						files,
						Array.map((file) => Effect.gen(function* () {
							const mediaType = getMediaTypeForFilename(file.name);
							if (mediaType === undefined) {
								return;
							}

							// NOTE Apparently, file.bytes is not a thing in this context
							const data = new Uint8Array(yield* Body.arrayBuffer(file));

							const attachment = yield* client.createAttachment({
								pieceId: piece.pieceId,
								data,
								filename: file.name,
								mediaType,
							});

							return attachment;
						})),
						Effect.allWith({ concurrency: "unbounded" }),
						Effect.map(Array.filter(Predicate.isNotUndefined)),
					);

					yield* pipe(
						{ ...piece, attachments },
						denormalizePiece,
						Effect.tap((piece) => pieceCache.create(piece.pieceId, piece)),
					);

					setFiles(SortedMap.remove(name));
				})),
				Effect.allWith({ concurrency: "unbounded" }),
			);

			yield* props.refresh;

			props.setDialogOpen(false);
		}));
	}).pipe(Effect.runPromise);

	return (
		<DialogContent>
			<form onSubmit={onSubmit}>
				<DialogHeader>
					<DialogTitle>Importuj utwory</DialogTitle>
				</DialogHeader>
				<div className="flex flex-col gap-4 py-4">
					{SortedMap.size(files) > 0 && (
						<div className="max-h-[50vh] overflow-y-auto">
							<Table>
								<TableHeader>
									<TableRow>
										<TableHead>Tytuł</TableHead>
										<TableHead className="text-center">Liczba plików</TableHead>
									</TableRow>
								</TableHeader>
								<TableBody>
									{pipe(
										files,
										SortedMap.entries,
										Iterable.map(([name, files]) => (
											<TableRow key={name}>
												<TableCell>{name}</TableCell>
												<TableCell className="text-center">{files.length}</TableCell>
											</TableRow>
										)),
									)}
								</TableBody>
							</Table>
						</div>
					)}
					<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}
					>
						<Import />
						<div>Przeciągnij i upuść tutaj załączniki</div>
					</div>
				</div>
				<DialogFooter>
					<Button type="submit" disabled={isLoading}>
						{isLoading && <Loader2 className="animate-spin" />}
						Importuj
					</Button>
				</DialogFooter>
			</form>
		</DialogContent>
	);
}
