import "./ReportLauncher.css";

import { Button, Checkbox, Dialog, Dropdown, Heading, Tabs, Textfield } from "@digdir/designsystemet-react";
import { ChevronDownIcon, XMarkIcon } from "@navikt/aksel-icons";
import {
	convertFilter,
	createEmptyFilter,
	type FieldMetadata,
	type FilterTree,
	getLocalizedString,
	type ReportFormat,
	type ReportParameterValues,
	restoreParameterValues,
	type SavedFilter,
	serializeParameters,
} from "@olenbetong/appframe-core";
import { useFilterFields, useReport, useReportRender } from "@olenbetong/appframe-react";
import clsx from "clsx";
import { useEffect, useId, useMemo, useState } from "react";

import { FilterBuilder } from "../filter/FilterBuilder.js";
import type { FilterEditorSize, GetFieldOptions } from "../filter/types.js";
import { ReportParameters } from "./ReportParameters.js";

/** The label shown on the save button for each format. */
const FORMAT_LABELS: Record<ReportFormat, string> = {
	CSV: "Save as CSV",
	HTML: "Save as HTML",
	IMAGE: "Save as image",
	MHT: "Save as MHT",
	PDF: "Save as PDF",
	RTF: "Save as RTF",
	TEXT: "Save as text",
	XLS: "Save as XLS (Excel)",
	XLSX: "Save as XLSX (Excel 2007)",
};

export type ReportLauncherTab = "parameters" | "filter";

export type ReportLauncherProps = {
	/** The report to run, e.g. `arpt_Personnel_Persons`. */
	reportId: string;
	/**
	 * `"dialog"` wraps the launcher in a modal, `"inline"` renders it in place.
	 * @default "dialog"
	 */
	variant?: "dialog" | "inline";
	/** Whether the dialog is open. Ignored when `variant` is `"inline"`. */
	open?: boolean;
	/** Called when the launcher is dismissed. */
	onClose?: () => void;
	/**
	 * Overrides the filter the report was designed with, as a criteria string.
	 * An empty string starts the launcher with no filter at all.
	 */
	initialFilter?: string | null;
	/**
	 * The fields the filter can be built from. Loaded from the report's view
	 * when not supplied.
	 */
	fields?: FieldMetadata[];
	/**
	 * The tab shown first.
	 * @default "parameters"
	 */
	defaultTab?: ReportLauncherTab;
	getFieldOptions?: GetFieldOptions;
	className?: string;
	"data-size"?: FilterEditorSize;
};

/**
 * The web port of the Windows report launcher: the saved filters and criteria
 * editor of the filter builder, the report's parameters, the report options,
 * and the save/preview/print actions.
 *
 * Everything the launcher needs comes from the report itself, so a caller only
 * has to name it. A report that does not resolve is one the current user has no
 * access to — `sviw_WinClient_MyReports` is permission-resolved.
 *
 * @example
 * ```jsx
 * <ReportLauncher
 *   reportId="arpt_Production_ElementsDaily"
 *   open={open}
 *   initialFilter={`[ProjectID] = '${projectId}'`}
 *   onClose={() => setOpen(false)}
 * />
 * ```
 */
export function ReportLauncher({
	reportId,
	variant = "dialog",
	open = true,
	onClose,
	initialFilter: initialFilterOverride,
	fields: fieldsOverride,
	defaultTab = "parameters",
	getFieldOptions,
	className,
	"data-size": size = "sm",
}: ReportLauncherProps) {
	let { report, parameters, initialFilter, defaultValues, loading, error } = useReport(reportId, {
		initialFilter: initialFilterOverride,
	});
	let viewName = report?.viewName ?? undefined;
	let { fields: loadedFields } = useFilterFields(fieldsOverride ? null : viewName);
	let fields = fieldsOverride ?? loadedFields;
	let { open: openReport, print, download, isRendering, progress, error: renderError } = useReportRender(reportId);

	let [filter, setFilter] = useState<FilterTree>(createEmptyFilter);
	let [filterString, setFilterString] = useState("");
	let [values, setValues] = useState<ReportParameterValues>({});
	let [reportTitle, setReportTitle] = useState("");
	let [headerText, setHeaderText] = useState("");
	let [hideFilterString, setHideFilterString] = useState(false);
	let [tab, setTab] = useState<ReportLauncherTab>(defaultTab);
	let panelId = useId();

	// The report's own defaults arrive in two rounds — the literal ones first,
	// then the ones a stored procedure produces — so they are copied into the
	// form whenever they change rather than only once.
	useEffect(() => {
		setValues(defaultValues);
	}, [defaultValues]);

	useEffect(() => {
		setReportTitle(report?.title ?? "");
	}, [report?.title]);

	/*
	 * The initial filter is a criteria string, so it has to be parsed into a
	 * tree before the editor can show it.
	 */
	useEffect(() => {
		if (!initialFilter) {
			setFilter(createEmptyFilter());

			return;
		}

		let controller = new AbortController();

		convertFilter({ filterString: initialFilter, viewName }, controller.signal)
			.then((result) => {
				if (!controller.signal.aborted) setFilter(result.filterObject ?? createEmptyFilter());
			})
			.catch(() => {
				// An initial filter the converter rejects is better dropped than
				// blocking the launcher; the user can rebuild it.
			});

		return () => {
			controller.abort();
		};
	}, [initialFilter, viewName]);

	/** Selecting a saved filter restores the report options stored with it. */
	function handleSelectFilter(saved: SavedFilter | null) {
		if (!saved) {
			setValues(defaultValues);
			setHeaderText("");
			setHideFilterString(false);
			setReportTitle(report?.title ?? "");

			return;
		}

		setValues(restoreParameterValues(parameters, saved.parameters));
		setHeaderText(saved.headerText ?? "");
		setHideFilterString(saved.hideCriteria);
		setReportTitle(saved.reportTitle ?? report?.title ?? "");
	}

	let saveValues = useMemo(
		() => ({
			headerText: headerText || null,
			hideCriteria: hideFilterString,
			parameters: parameters.length > 0 ? serializeParameters(parameters, values) : null,
		}),
		[headerText, hideFilterString, parameters, values],
	);

	let formats = report?.formats ?? [];
	// PDF is the only format worth previewing or printing in place; the rest are
	// downloads whatever the browser is asked to do with them.
	let canPreview = formats.includes("PDF");

	function getRenderOptions(format: ReportFormat) {
		return {
			format,
			fileName: reportTitle || report?.title || reportId,
			filter: filterString,
			parameters: values,
			headerText: report?.hasHeader ? headerText : null,
			hideFilterString,
		};
	}

	function run(action: (options: ReturnType<typeof getRenderOptions>) => Promise<void>, format: ReportFormat) {
		void action(getRenderOptions(format));
	}

	/*
	 * The parameters and the criteria are too much to show at once, so they get
	 * a tab each. The report options stay below the tabs: they apply to the
	 * report whichever tab is open, and the hide-criteria flag in particular
	 * reads as belonging to both.
	 */
	let options = (
		<div className="ObReportLauncher-options">
			{report?.hasTitle && (
				<Textfield
					data-size={size}
					label={getLocalizedString("Report name")}
					description={getLocalizedString("Used as the file name when the report is saved.")}
					value={reportTitle}
					onChange={(event) => setReportTitle(event.target.value)}
				/>
			)}
			{report?.hasHeader && (
				<Textfield
					data-size={size}
					multiline
					rows={2}
					label={getLocalizedString("Optional text (at the top of every page)")}
					value={headerText}
					onChange={(event) => setHeaderText(event.target.value)}
				/>
			)}
			<Checkbox
				data-size={size}
				label={getLocalizedString("Do not show the filter criteria at the top of the pages")}
				checked={hideFilterString}
				onChange={(event) => setHideFilterString(event.currentTarget.checked)}
			/>
		</div>
	);

	let actions = (
		<div className="ObReportLauncher-actions">
			{isRendering && (
				<span className="ObReportLauncher-progress">
					{progress?.bytesReceived
						? `${Math.round(progress.bytesReceived / 1024)} kB`
						: getLocalizedString("Rendering...")}
				</span>
			)}
			{formats.length > 0 && (
				<div className="ObReportLauncher-saveGroup">
					<Button type="button" data-size={size} disabled={isRendering} onClick={() => run(download, formats[0])}>
						{getLocalizedString(FORMAT_LABELS[formats[0]])}
					</Button>
					{formats.length > 1 && (
						<Dropdown.TriggerContext>
							<Dropdown.Trigger
								data-size={size}
								icon
								disabled={isRendering}
								aria-label={getLocalizedString("More formats")}
							>
								<ChevronDownIcon aria-hidden />
							</Dropdown.Trigger>
							<Dropdown>
								<Dropdown.List>
									{formats.slice(1).map((format) => (
										<Dropdown.Item key={format}>
											<Dropdown.Button onClick={() => run(download, format)}>
												{getLocalizedString(FORMAT_LABELS[format])}
											</Dropdown.Button>
										</Dropdown.Item>
									))}
								</Dropdown.List>
							</Dropdown>
						</Dropdown.TriggerContext>
					)}
				</div>
			)}
			<Button
				type="button"
				variant="secondary"
				data-size={size}
				disabled={!canPreview || isRendering}
				onClick={() => run(openReport, "PDF")}
			>
				{getLocalizedString("Preview")}
			</Button>
			<Button
				type="button"
				variant="secondary"
				data-size={size}
				disabled={!canPreview || isRendering}
				onClick={() => run(print, "PDF")}
			>
				{getLocalizedString("Print")}
			</Button>
			<Button type="button" variant="tertiary" data-size={size} onClick={onClose}>
				{getLocalizedString("Close")}
			</Button>
		</div>
	);

	let content = (
		<div className={clsx("ObReportLauncher-root", className)} data-size={size}>
			{loading && <p className="ObReportLauncher-status">{getLocalizedString("Loading...")}</p>}
			{!loading && !report && (
				<p className="ObReportLauncher-error" role="alert">
					{error
						? error.message
						: getLocalizedString("The report does not exist, or you do not have access to run it.")}
				</p>
			)}
			{report && (
				<FilterBuilder
					fields={fields}
					viewName={viewName}
					reportId={reportId}
					getFieldOptions={getFieldOptions}
					variant="inline"
					value={filter}
					data-size={size}
					actions={actions}
					saveValues={saveValues}
					onChange={setFilter}
					onFilterStringChange={setFilterString}
					onSelect={handleSelectFilter}
					renderMain={(editor) => (
						<div className="ObReportLauncher-main">
							<Tabs
								value={tab}
								data-size={size}
								className="ObReportLauncher-tabs"
								onChange={(value) => setTab(value as ReportLauncherTab)}
							>
								<Tabs.List>
									<Tabs.Tab value="parameters" aria-controls={`${panelId}-parameters`}>
										{parameters.length > 0 ? getLocalizedString("Parameters") : getLocalizedString("Report")}
									</Tabs.Tab>
									<Tabs.Tab value="filter" aria-controls={`${panelId}-filter`}>
										{getLocalizedString("Filter")}
									</Tabs.Tab>
								</Tabs.List>
							</Tabs>
							{/*
							 * The panels are rendered outside `Tabs` rather than as
							 * `Tabs.Panel`, so the criteria editor gets a flex parent
							 * it can fill and scroll inside of.
							 */}
							{tab === "parameters" ? (
								<div
									role="tabpanel"
									id={`${panelId}-parameters`}
									className="ObReportLauncher-panel ObReportLauncher-panel--scroll"
								>
									{parameters.length > 0 ? (
										// No legend: the tab is labelled "Parameters" already.
										<ReportParameters
											parameters={parameters}
											values={values}
											onChange={setValues}
											legend={null}
											data-size={size}
										/>
									) : (
										<p className="ObReportLauncher-status">{getLocalizedString("The report has no parameters.")}</p>
									)}
								</div>
							) : (
								<div role="tabpanel" id={`${panelId}-filter`} className="ObReportLauncher-panel">
									{editor}
								</div>
							)}
							{options}
						</div>
					)}
				/>
			)}
			{renderError && (
				<p className="ObReportLauncher-error" role="alert">
					{renderError.message}
				</p>
			)}
		</div>
	);

	if (variant === "inline") return content;

	return (
		<Dialog
			open={open}
			closedby="closerequest"
			closeButton={false}
			data-size={size}
			className="ObReportLauncher-dialog"
			onClose={onClose}
		>
			{/* As in the filter builder, `Dialog`'s floated close button does not
			    survive a flex layout, so the header renders its own. */}
			<Dialog.Block className="ObReportLauncher-dialogHeader">
				<Heading level={2} data-size="xs">
					{getLocalizedString("Report")}: {report?.title ?? reportId}
					{/* The report number is what users refer to a report by — it is
					    printed in the footer of every report. */}
					{report?.shortId !== null && report?.shortId !== undefined && ` (ID: ${report.shortId})`}
				</Heading>
				<Button
					type="button"
					variant="tertiary"
					data-color="neutral"
					data-size={size}
					icon
					aria-label={getLocalizedString("Close")}
					onClick={onClose}
				>
					<XMarkIcon aria-hidden />
				</Button>
			</Dialog.Block>
			<Dialog.Block className="ObReportLauncher-dialogBody">{content}</Dialog.Block>
		</Dialog>
	);
}
