import { type TransactionStatusCode, transactionStatusLabels } from "@olenbetong/appframe-updater";
import { Box, Text } from "ink";
import { type ReactNode, useMemo } from "react";

import type { Server } from "../lib/Server.js";
import { FullScreenBox } from "./FullScreenBox.js";
import { STATUS_DIRTY_INDICATOR } from "./tableFormatting.js";
import { useCommand } from "./useCommand.js";
import { useScreenSize } from "./useScreenSize.js";
import { useScrollableText } from "./useScrollableText.js";
import { useTransactionEdits } from "./useTransactionEdits.js";
import { useTransactions } from "./useTransactions.js";
import { useTransactionText } from "./useTransactionText.js";
import { useVirtualText } from "./useVirtualText.js";

const PREVIEW_SAVE_SHORTCUTS = ["ctrl+s", "meta+s"];
const NO_TRANSACTION_FILTER = "1 = 0";

function escapePrimKey(primKey: string) {
	return primKey.replace(/'/g, "''");
}

function createPrimKeyFilter(primKey: string | null) {
	if (!primKey) {
		return NO_TRANSACTION_FILTER;
	}

	return `PrimKey = '${escapePrimKey(primKey)}'`;
}

export type TransactionsPreviewDialogProps = {
	server: Server;
	namespace: string | number;
	frameWidth: number;
	focusedPrimKey: string | null;
	focusNext: () => void;
	focusPrevious: () => void;
};

function Shortcut({ keyboard, command }: { keyboard: string; command: string }) {
	return (
		<Text>
			[{keyboard}] {command}
		</Text>
	);
}

export function TransactionsPreviewDialog({
	server,
	namespace,
	frameWidth,
	focusedPrimKey,
	focusNext,
	focusPrevious,
}: TransactionsPreviewDialogProps) {
	let transactionFilter = useMemo(() => createPrimKeyFilter(focusedPrimKey), [focusedPrimKey]);
	let { loading, error, refresh, normalizedTransactions, transactionMap } = useTransactions({
		server,
		namespace,
		filter: transactionFilter,
	});

	let selectedPrimKeys = useMemo(
		() => (focusedPrimKey ? new Set([focusedPrimKey]) : new Set<string>()),
		[focusedPrimKey],
	);

	let { pendingStatuses, cycleStatus, save } = useTransactionEdits({
		normalizedTransactions,
		transactionRecordMap: transactionMap,
		selectedPrimKeys,
		server,
		refresh,
	});

	let record = focusedPrimKey ? transactionMap.get(focusedPrimKey) : undefined;
	let pendingStatus = focusedPrimKey ? pendingStatuses[focusedPrimKey] : undefined;

	let statusPreviewValue = useMemo(() => {
		if (!record) {
			return "";
		}

		let originalStatus = record.Status;
		let numericOriginal =
			typeof originalStatus === "number"
				? originalStatus
				: typeof originalStatus === "string"
					? Number(originalStatus)
					: null;

		let baseText: string;
		if (pendingStatus !== undefined) {
			baseText = String(pendingStatus);
		} else if (originalStatus === null) {
			baseText = "null";
		} else if (originalStatus === undefined) {
			baseText = "";
		} else {
			baseText = String(originalStatus);
		}

		let labelSource: number | null = null;
		if (pendingStatus !== undefined) {
			labelSource = pendingStatus;
		} else if (numericOriginal !== null && Number.isFinite(numericOriginal)) {
			labelSource = numericOriginal;
		}

		if (labelSource !== null) {
			let label = transactionStatusLabels[labelSource as TransactionStatusCode];
			if (label) {
				baseText = baseText.length > 0 ? `${baseText} (${label})` : `(${label})`;
			}
		}

		let isDirty = false;
		if (pendingStatus !== undefined) {
			isDirty = numericOriginal === null || !Number.isFinite(numericOriginal) || pendingStatus !== numericOriginal;
		}

		if (isDirty) {
			baseText = `${baseText}${STATUS_DIRTY_INDICATOR}`;
		}

		return baseText;
	}, [pendingStatus, record]);

	let { text: transactionText, dialogWidth } = useTransactionText({
		transaction: record,
		statusPreviewValue,
		frameWidth,
	});

	let { height: screenHeight } = useScreenSize();

	let headerLineCount = 3 + (record?.Name ? 1 : 0);
	let marginLines = 1; // Box marginTop between header and content
	let baseNonContentLines = headerLineCount + marginLines;
	let contentLineCount = transactionText.length > 0 ? transactionText.length : 1;

	let computeLayout = (extraNonContentLines: number) => {
		let nonContentLines = baseNonContentLines + extraNonContentLines;
		let naturalDialogHeight = nonContentLines + contentLineCount;
		let maxDialogHeight = Math.max(screenHeight - 4, nonContentLines + 1);
		maxDialogHeight = Math.min(maxDialogHeight, Math.max(screenHeight - 2, 1));
		let dialogHeightValue = Math.min(naturalDialogHeight, maxDialogHeight);
		let contentHeightValue = Math.max(dialogHeightValue - nonContentLines, 1);
		return { dialogHeight: dialogHeightValue, contentHeight: contentHeightValue, nonContentLines };
	};

	let { dialogHeight, contentHeight } = computeLayout(0);
	let showScrollSummary = record && transactionText.length > contentHeight;
	if (showScrollSummary) {
		({ dialogHeight, contentHeight } = computeLayout(2));
	}

	let previewSignature = useMemo(
		() => `${focusedPrimKey ?? ""}|${transactionText.join("|")}`,
		[focusedPrimKey, transactionText],
	);

	let { scrollOffset } = useScrollableText({
		textLength: transactionText.length,
		viewportHeight: contentHeight,
		signature: previewSignature,
	});

	let { visibleText, totalLines, offset } = useVirtualText({
		text: transactionText,
		viewportHeight: contentHeight,
		scrollOffset,
	});

	useCommand("left", focusPrevious, true);
	useCommand("right", focusNext, true);
	useCommand(
		PREVIEW_SAVE_SHORTCUTS,
		() => {
			if (focusedPrimKey) {
				save({ primKey: focusedPrimKey });
			}
		},
		focusedPrimKey !== null,
	);
	useCommand(
		"s",
		() => {
			if (focusedPrimKey) {
				cycleStatus(focusedPrimKey, { exclusive: true });
			}
		},
		focusedPrimKey !== null,
	);

	let summaryText = useMemo(() => {
		if (totalLines === 0) {
			return "";
		}

		let start = offset + 1;
		let end = Math.min(offset + contentHeight, totalLines);
		return `Showing ${start}–${end} of ${totalLines}`;
	}, [contentHeight, offset, totalLines]);

	let bodyContent: ReactNode;
	if (!focusedPrimKey) {
		bodyContent = <Text color="gray">No transaction selected.</Text>;
	} else if (error) {
		bodyContent = <Text color="red">{error.message}</Text>;
	} else if (!record) {
		bodyContent = loading ? (
			<Text color="gray">Loading transaction…</Text>
		) : (
			<Text color="yellow">Transaction not found.</Text>
		);
	} else if (transactionText.length === 0) {
		bodyContent = <Text color="gray">No data available.</Text>;
	} else {
		bodyContent = visibleText.map((line, index) => (
			<Text key={`${offset + index}`}>{line.length > 0 ? line : " "}</Text>
		));
	}

	return (
		<FullScreenBox position="absolute" justifyContent="center" alignItems="center">
			<Box
				flexDirection="column"
				borderStyle="round"
				paddingX={1}
				paddingY={1}
				width={dialogWidth}
				height={dialogHeight}
				backgroundColor="black"
			>
				<Text bold>Transaction preview</Text>
				{record?.Name ? <Text>{record.Name}</Text> : null}
				<Text color="gray">
					<Shortcut keyboard="esc" command="Close preview" /> · <Shortcut keyboard="←/→" command="Change focus" /> ·{" "}
					<Shortcut keyboard="↑/↓" command="Scroll" /> · <Shortcut keyboard="PgUp/PgDn" command="Scroll faster" /> ·{" "}
					<Shortcut keyboard="s" command="Cycle status" /> · <Shortcut keyboard="Ctrl+S" command="Save" />
				</Text>
				<Box marginTop={1} flexDirection="column" height={contentHeight}>
					{bodyContent}
				</Box>
				{showScrollSummary ? (
					<Box marginTop={1} flexDirection="column">
						<Text color="gray">{summaryText}</Text>
					</Box>
				) : null}
			</Box>
		</FullScreenBox>
	);
}
