import { Button, Spinner, Tooltip } from "@digdir/designsystemet-react";
import { ArrowCirclepathIcon, ArrowLeftIcon, ArrowRedoIcon, ArrowUndoIcon, CheckmarkIcon } from "@navikt/aksel-icons";
import { getLocalizedString } from "@olenbetong/appframe-core";
import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from "react";

import "./ImageEditor.css";
import { ResponsiveButton } from "./ResponsiveButton.js";
import type { ActionPlugin, ActionRegistry, EditAction, Plugin, ToolPlugin, ViewPlugin } from "./types.js";
import { useActiveTool } from "./useActiveTool.js";
import { useImageHistory } from "./useImageHistory.js";
import { useIsSmall } from "./useIsSmall.js";

export type ImageEditorRef = {
	/** Returns the current edited blob, or null if no edits have been applied since the last source image. */
	getCurrentBlob: () => Blob | null;
	/**
	 * Runs a registered action plugin (e.g. `"rotate-cw"`) against the current image.
	 * Use this instead of calling `plugin.execute()` directly so external toolbars and
	 * keyboard shortcuts share the editor's image state and undo history.
	 * Resolves once the resulting image has been applied.
	 */
	executeAction: (pluginId: string) => Promise<void>;
	/**
	 * Resolves once every queued action has been applied. Await this before `getCurrentBlob()`
	 * to be sure an action started moments earlier is included.
	 */
	whenIdle: () => Promise<void>;
};

interface Props {
	imageUrl: string;
	/** Called when the user clicks the Done button. When omitted, the Done button is not shown. */
	onConfirm?: (blob: Blob) => void;
	/** Called when the user wants to exit without saving. Shows a back button in the top bar. */
	onCancel?: () => void;
	plugins: Plugin[];
	/** Optional view plugin (e.g. zoom/pan) that wraps the canvas and contributes top-bar controls. */
	viewPlugin?: ViewPlugin;
}

/**
 * Full-screen image editor shell.
 * Does not know about specific tools — behaviour is entirely driven by the `plugins` prop.
 * Tool plugins contribute an overlay and toolbar; action plugins execute immediately.
 * Supports undo/redo: each applied transformation is recorded with its EditAction list.
 */
export const ImageEditor = forwardRef<ImageEditorRef, Props>(function ImageEditor(
	{ imageUrl, onConfirm, onCancel, plugins, viewPlugin }: Props,
	ref,
) {
	let registry = useMemo(() => buildActionRegistry(plugins), [plugins]);
	let history = useImageHistory(imageUrl, registry);
	let { activePlugin, activateTool, deactivateTool } = useActiveTool(plugins);
	let [busy, setBusy] = useState(false);
	let disabled = busy || history.replaying;
	let isSmall = useIsSmall();

	// Keeps async/imperative callers working against the latest history state instead of
	// the snapshot captured when their closure was created.
	let historyRef = useRef(history);
	historyRef.current = history;
	// Serialises every action (toolbar clicks and imperative calls alike) so rapid
	// invocations each build on the previous result instead of racing on a stale image.
	let actionQueueRef = useRef<Promise<void>>(Promise.resolve());

	useImperativeHandle(ref, () => ({
		getCurrentBlob: () => historyRef.current.getCurrentBlob(),
		whenIdle: () => actionQueueRef.current,
		executeAction: (pluginId: string) => {
			let plugin = plugins.find((p): p is ActionPlugin => p.type === "action" && p.id === pluginId);
			if (!plugin) {
				console.warn(`[ImageEditor] No action plugin registered with id "${pluginId}"`);
				return Promise.resolve();
			}
			return enqueueAction(plugin);
		},
	}));

	function enqueueAction(plugin: ActionPlugin) {
		let next = actionQueueRef.current.then(() => executeAction(plugin));
		actionQueueRef.current = next.catch(() => {});
		return next;
	}

	async function executeAction(plugin: ActionPlugin) {
		setBusy(true);
		try {
			let blob = await plugin.execute(historyRef.current.getCurrentImageUrl());
			let actions: EditAction[] = [{ type: plugin.id, params: {} }];
			historyRef.current.applyActions(blob, actions);
		} finally {
			setBusy(false);
		}
	}

	function handleToolApply(blob: Blob, actions: EditAction[]) {
		history.applyActions(blob, actions);
		deactivateTool();
	}

	function confirm() {
		if (!onConfirm) return;
		if (history.currentBlob) {
			onConfirm(history.currentBlob);
			return;
		}
		fetch(history.currentImageUrl)
			.then((r) => r.blob())
			.then(onConfirm);
	}

	let CanvasWrapper = viewPlugin?.CanvasWrapper;
	let TopBarControls = viewPlugin?.TopBarControls;

	let inner = (
		<div className="image-editor" data-color-scheme="dark">
			{/* Top toolbar — always visible; undo/redo/reset disabled while a tool is active */}
			<div className="image-editor__topbar">
				<div className="image-editor__toolbar-group">
					{onCancel && (
						<>
							<ResponsiveButton
								icon={<ArrowLeftIcon aria-hidden />}
								label={getLocalizedString("Back")}
								onClick={onCancel}
								isSmall={isSmall}
							/>
							<span className="image-editor__topbar-divider" aria-hidden />
						</>
					)}
					<ResponsiveButton
						icon={<ArrowUndoIcon aria-hidden />}
						label={getLocalizedString("Undo")}
						onClick={history.undo}
						disabled={disabled || !history.canUndo || !!activePlugin}
						isSmall={isSmall}
					/>
					<ResponsiveButton
						icon={<ArrowRedoIcon aria-hidden />}
						label={getLocalizedString("Redo")}
						onClick={history.redo}
						disabled={disabled || !history.canRedo || !!activePlugin}
						isSmall={isSmall}
					/>
					<ResponsiveButton
						icon={<ArrowCirclepathIcon aria-hidden />}
						label={getLocalizedString("Reset")}
						onClick={history.reset}
						disabled={disabled || !history.canUndo || !!activePlugin}
						isSmall={isSmall}
					/>
				</div>

				{TopBarControls && (
					<div className="image-editor__toolbar-group">
						<TopBarControls disabled={disabled} isSmall={isSmall} />
					</div>
				)}
			</div>

			{/* Canvas area */}
			<div className="image-editor__canvas">
				{CanvasWrapper ? (
					<CanvasWrapper panEnabled={!activePlugin}>
						{activePlugin ? (
							<ActiveOverlay plugin={activePlugin} />
						) : (
							<img
								src={history.currentImageUrl}
								alt={getLocalizedString("Image to edit")}
								className="image-editor__img"
							/>
						)}
					</CanvasWrapper>
				) : activePlugin ? (
					<ActiveOverlay plugin={activePlugin} />
				) : (
					<img src={history.currentImageUrl} alt={getLocalizedString("Image to edit")} className="image-editor__img" />
				)}

				{/* Done button floats over the canvas, bottom-right — hidden when a tool is active or no onConfirm */}
				{!activePlugin && onConfirm && (
					<Button
						className="image-editor__done"
						variant="primary"
						icon={isSmall}
						aria-label={isSmall ? getLocalizedString("Done") : undefined}
						onClick={confirm}
						disabled={disabled}
					>
						<CheckmarkIcon aria-hidden />
						{!isSmall && getLocalizedString("Done")}
					</Button>
				)}

				{/* Replay overlay covers the canvas and sits above the Done button */}
				{history.replaying && (
					<div className="image-editor__replay-overlay">
						<Spinner aria-label={getLocalizedString("Loading")} />
					</div>
				)}
			</div>

			{/* Bottom toolbar — tool/action plugin buttons */}
			<div className="image-editor__toolbar">
				{activePlugin ? (
					<ActiveToolbar plugin={activePlugin} />
				) : (
					<div className="image-editor__toolbar-group">
						{plugins.map((p) =>
							p.type === "tool" ? (
								<ToolButton
									key={p.id}
									plugin={p}
									busy={disabled}
									onActivate={() => activateTool(p.id)}
									isSmall={isSmall}
								/>
							) : (
								<ActionButton
									key={p.id}
									plugin={p}
									busy={disabled}
									onExecute={() => enqueueAction(p)}
									isSmall={isSmall}
								/>
							),
						)}
					</div>
				)}
			</div>
		</div>
	);

	// Wrap with the active tool's provider so Overlay and Toolbar share context.
	if (activePlugin) {
		let ActiveProvider = activePlugin.Provider;
		inner = (
			<ActiveProvider imageUrl={history.currentImageUrl} onApply={handleToolApply} onCancel={deactivateTool}>
				{inner}
			</ActiveProvider>
		);
	}

	// Wrap with the view plugin's provider (outermost) so both CanvasWrapper and TopBarControls share state.
	if (viewPlugin) {
		let ViewProvider = viewPlugin.Provider;
		return <ViewProvider>{inner}</ViewProvider>;
	}

	return inner;
});

function ToolButton({
	plugin,
	busy,
	onActivate,
	isSmall,
}: {
	plugin: ToolPlugin;
	busy: boolean;
	onActivate: () => void;
	isSmall: boolean;
}) {
	let Icon = plugin.Icon;
	let label = getLocalizedString(plugin.label);

	if (isSmall) {
		return (
			<Tooltip content={label}>
				<Button icon variant="tertiary" data-size="sm" aria-label={label} onClick={onActivate} disabled={busy}>
					<Icon />
				</Button>
			</Tooltip>
		);
	}

	return (
		<Button variant="tertiary" data-size="sm" onClick={onActivate} disabled={busy}>
			<Icon />
			{label}
		</Button>
	);
}

function ActionButton({
	plugin,
	busy,
	onExecute,
	isSmall,
}: {
	plugin: ActionPlugin;
	busy: boolean;
	onExecute: () => void;
	isSmall: boolean;
}) {
	let Icon = plugin.Icon;
	let label = getLocalizedString(plugin.label);
	let icon = busy ? <Spinner aria-hidden data-size="xs" /> : <Icon />;

	if (isSmall) {
		return (
			<Tooltip content={label}>
				<Button icon variant="tertiary" data-size="sm" aria-label={label} onClick={onExecute} disabled={busy}>
					{icon}
				</Button>
			</Tooltip>
		);
	}

	return (
		<Button variant="tertiary" data-size="sm" onClick={onExecute} disabled={busy}>
			{icon}
			{label}
		</Button>
	);
}

function ActiveOverlay({ plugin }: { plugin: ToolPlugin }) {
	let Overlay = plugin.Overlay;
	return <Overlay />;
}

function ActiveToolbar({ plugin }: { plugin: ToolPlugin }) {
	let Toolbar = plugin.Toolbar;
	return <Toolbar />;
}

/** Builds the action registry from all registered plugins. */
function buildActionRegistry(plugins: Plugin[]): ActionRegistry {
	let registry: ActionRegistry = new Map();
	for (let plugin of plugins) {
		if (plugin.type === "action") {
			// Action plugins have no params — their execute function IS the replay handler.
			registry.set(plugin.id, (imageUrl, _params) => plugin.execute(imageUrl));
		} else {
			for (let action of plugin.actions) {
				registry.set(action.type, action.apply);
			}
		}
	}
	return registry;
}
