import { Box, Button, DialogActions, LinearProgress, Link, Typography } from "@mui/material";
import { getLocalizedString } from "@olenbetong/appframe-core";
import { ResponsiveDialog } from "@olenbetong/appframe-mui";
import { useEffect, useRef, useState } from "react";

export type MergeProgressDialogProps = {
	open: boolean;
	onClose: () => void;
	/** Called once when the dialog opens; should return the merger ID for the start route */
	getMergerId: () => Promise<string | number>;
	mergeType?: "pdf" | "zip";
};

type MergePhase =
	| { kind: "preparing" }
	| { kind: "processing"; completed: number; total: number; currentFile: string; startedAt: number }
	| { kind: "completed"; jobId: string; fileName: string }
	| { kind: "failed"; error: string };

function formatRemaining(ms: number) {
	if (ms < 60_000) return `~${Math.ceil(ms / 1000)}s`;
	let mins = Math.floor(ms / 60_000);
	let secs = Math.ceil((ms % 60_000) / 1000);
	return secs > 0 ? `~${mins}m ${secs}s` : `~${mins}m`;
}

export function MergeProgressDialog({ open, onClose, getMergerId, mergeType = "pdf" }: MergeProgressDialogProps) {
	let [phase, setPhase] = useState<MergePhase>({ kind: "preparing" });
	let [now, setNow] = useState(Date.now);
	let esRef = useRef<EventSource | null>(null);
	let doneRef = useRef(false);

	// Tick every second while processing to recompute the time estimate
	useEffect(() => {
		if (phase.kind !== "processing") return;
		let timer = setInterval(() => setNow(Date.now()), 1000);
		return () => clearInterval(timer);
	}, [phase.kind]);

	// Start the merge job each time the dialog opens
	// oxlint-disable-next-line react-hooks/exhaustive-deps -- intentionally only re-runs on open change
	useEffect(() => {
		if (!open) return;

		setPhase({ kind: "preparing" });
		doneRef.current = false;

		let cancelled = false;
		let es: EventSource | null = null;

		async function start() {
			try {
				let mergerId = await getMergerId();
				if (cancelled) return;

				let response = await fetch(`/api/ob/merger/start/${mergerId}/${mergeType}`, { method: "POST" });
				if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
				let { jobId } = await response.json();
				if (cancelled) return;

				es = new EventSource(`/api/ob/merger/events/${jobId}`);
				esRef.current = es;

				es.addEventListener("progress", (e) => {
					let d = JSON.parse((e as MessageEvent).data);
					setPhase((prev) => ({
						kind: "processing",
						completed: d.completed,
						total: d.total,
						currentFile: d.currentFile ?? "",
						startedAt: prev.kind === "processing" ? prev.startedAt : Date.now(),
					}));
					setNow(Date.now());
				});

				es.addEventListener("done", (e) => {
					doneRef.current = true;
					es?.close();
					let d = JSON.parse((e as MessageEvent).data);
					setPhase({ kind: "completed", jobId, fileName: d.fileName ?? "" });
					window.open(`/api/ob/merger/download/${jobId}`);
				});

				// Catches both server-sent "event: error" and connection errors.
				// After a "done" event the server closes the stream, which also fires error —
				// doneRef guards against treating that as a failure.
				es.addEventListener("error", (e) => {
					if (doneRef.current) return;
					es?.close();
					let message = getLocalizedString("Unknown error");
					let data = (e as MessageEvent).data;
					if (data) {
						try {
							message = JSON.parse(data).message ?? message;
						} catch {}
					}
					setPhase({ kind: "failed", error: message });
				});
			} catch (err: unknown) {
				if (!cancelled) {
					setPhase({ kind: "failed", error: (err as Error)?.message ?? String(err) });
				}
			}
		}

		start();

		return () => {
			cancelled = true;
			es?.close();
			esRef.current = null;
		};
	}, [open]);

	function handleClose() {
		esRef.current?.close();
		esRef.current = null;
		onClose();
	}

	let pct = 0;
	let remaining: string | null = null;

	if (phase.kind === "processing" && phase.total > 0) {
		pct = Math.round((phase.completed / phase.total) * 100);
		if (phase.completed > 0) {
			let elapsed = now - phase.startedAt;
			let msPerFile = elapsed / phase.completed;
			let remainingMs = msPerFile * (phase.total - phase.completed);
			remaining = formatRemaining(remainingMs);
		}
	}

	let title =
		phase.kind === "completed"
			? getLocalizedString("Merge complete")
			: phase.kind === "failed"
				? getLocalizedString("Merge failed")
				: getLocalizedString("Merging drawings");

	let canClose = phase.kind === "completed" || phase.kind === "failed";

	return (
		<ResponsiveDialog
			open={open}
			onClose={canClose ? handleClose : undefined}
			title={title}
			maxWidth="sm"
			fullWidth
			slotProps={{
				transition: {
					mountOnEnter: true,
					unmountOnExit: true,
				},
			}}
		>
			<Box sx={{ px: 3, pb: 1 }}>
				{(phase.kind === "preparing" || phase.kind === "processing") && (
					<LinearProgress
						variant={phase.kind === "preparing" || phase.total === 0 ? "indeterminate" : "determinate"}
						value={pct}
						sx={{ mb: 1.5 }}
					/>
				)}

				{phase.kind === "preparing" && (
					<Typography
						variant="body2"
						sx={{
							color: "text.secondary",
						}}
					>
						{getLocalizedString("Starting merge job…")}
					</Typography>
				)}

				{phase.kind === "processing" && (
					<>
						<Typography variant="body2">
							{phase.completed} / {phase.total} {getLocalizedString("files")}
							{remaining && (
								<Typography
									component="span"
									variant="body2"
									sx={{
										color: "text.secondary",
									}}
								>
									{" "}
									— {remaining}
								</Typography>
							)}
						</Typography>
						{phase.currentFile && (
							<Typography
								variant="body2"
								noWrap
								title={phase.currentFile}
								sx={{
									color: "text.secondary",
									mt: 0.5,
								}}
							>
								{phase.currentFile}
							</Typography>
						)}
					</>
				)}

				{phase.kind === "completed" && (
					<>
						<Typography
							variant="body2"
							sx={{
								color: "success.main",
							}}
						>
							{getLocalizedString("Download started. You can close this dialog.")}
						</Typography>
						<Typography
							variant="body2"
							sx={{
								color: "text.secondary",
								mt: 1,
							}}
						>
							{getLocalizedString("If the file did not open automatically, ")}{" "}
							<Link href={`/api/ob/merger/download/${phase.jobId}`} target="_blank" rel="noopener noreferrer">
								{getLocalizedString("click here to open it")}
							</Link>
							.
						</Typography>
					</>
				)}

				{phase.kind === "failed" && (
					<Typography variant="body2" color="error">
						{phase.error}
					</Typography>
				)}
			</Box>
			<DialogActions>
				<Button onClick={handleClose} disabled={!canClose}>
					{getLocalizedString("Close")}
				</Button>
			</DialogActions>
		</ResponsiveDialog>
	);
}
