import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { expect, type Page } from "@playwright/test";
import type { z } from "zod";
import { getFormFieldMetadata } from "~/form/fields";
import type { FormField } from "~/form/types";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

function loadTranslations(): Record<string, Record<string, string>> {
	const localesPath = path.resolve(__dirname, "../../locales/en");
	return {
		forms: JSON.parse(
			fs.readFileSync(path.join(localesPath, "forms.json"), "utf-8"),
		),
		common: JSON.parse(
			fs.readFileSync(path.join(localesPath, "common.json"), "utf-8"),
		),
	};
}

const translations = loadTranslations();

function resolveTranslation(key: string): string {
	// Handle keys like "common:forms.name" or "team:newTeam.header"
	const [namespace, translationPath] = key.includes(":")
		? key.split(":")
		: ["common", key];
	const nsTranslations = translations[namespace];

	if (!nsTranslations) {
		return key;
	}

	const value = nsTranslations[translationPath as keyof typeof nsTranslations];
	return typeof value === "string" ? value : key;
}

type Inferred<T extends z.ZodRawShape> = z.infer<z.ZodObject<T>>;

type FillableKeys<T extends z.ZodRawShape> = {
	[K in keyof Inferred<T>]-?: string extends Inferred<T>[K] ? K : never;
}[keyof Inferred<T>];

type CheckableKeys<T extends z.ZodRawShape> = {
	[K in keyof Inferred<T>]-?: Inferred<T>[K] extends boolean ? K : never;
}[keyof Inferred<T>];

type SelectableKeys<T extends z.ZodRawShape> = {
	[K in keyof Inferred<T>]-?: Inferred<T>[K] extends string | null | undefined
		? K
		: never;
}[keyof Inferred<T>];

type FormFieldHelpers<T extends z.ZodRawShape> = {
	fill: (name: FillableKeys<T>, value: string) => Promise<void>;
	check: (name: CheckableKeys<T>) => Promise<void>;
	uncheck: (name: CheckableKeys<T>) => Promise<void>;
	checkItems: (name: keyof Inferred<T>, itemValues: string[]) => Promise<void>;
	select: (name: SelectableKeys<T>, optionText: string) => Promise<void>;
	selectUser: (name: keyof Inferred<T>, userName: string) => Promise<void>;
	selectWeapons: (
		name: keyof Inferred<T>,
		weaponNames: string[],
	) => Promise<void>;
	setDateTime: (name: keyof Inferred<T>, date: Date) => Promise<void>;
	setDate: (name: keyof Inferred<T>, date: Date) => Promise<void>;
	setImage: (name: keyof Inferred<T>, filePath: string) => Promise<void>;
	submit: () => Promise<void>;
	getLabel: <K extends keyof Inferred<T>>(name: K) => string;
	getItemLabel: (name: keyof Inferred<T>, itemValue: string) => string;
};

export function createFormHelpers<T extends z.ZodRawShape>(
	page: Page,
	schema: z.ZodObject<T>,
	options?: { submitTestId?: string },
): FormFieldHelpers<T> {
	const submitTestId = options?.submitTestId ?? "submit-button";
	const getFieldMetadata = (name: string): FormField | undefined => {
		const fieldSchema = schema.shape[name];
		if (!fieldSchema) return undefined;
		return getFormFieldMetadata(fieldSchema as z.ZodType);
	};

	const getLabel = (name: string): string => {
		const metadata = getFieldMetadata(name);
		if (!metadata || !("label" in metadata) || !metadata.label) {
			throw new Error(`No label found for field: ${name}`);
		}
		return resolveTranslation(metadata.label);
	};

	// match the whole label (allowing the trailing space and optional required " *" the
	// Label component always renders) so a short label like "Name" doesn't also pick up
	// "Bracket's name" or "Require in-game names"
	const byLabel = (label: string) => {
		const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
		return page.getByLabel(new RegExp(`^${escaped} *\\*?$`, "i"));
	};

	const getItemLabel = (name: string, itemValue: string): string => {
		const metadata = getFieldMetadata(name);
		if (!metadata || !("items" in metadata) || !Array.isArray(metadata.items)) {
			throw new Error(`No items found for field: ${name}`);
		}
		const item = metadata.items.find(
			(i: { value: string }) => i.value === itemValue,
		);
		if (!item || typeof item.label !== "string") {
			throw new Error(`No item found with value: ${itemValue}`);
		}
		return resolveTranslation(item.label);
	};

	return {
		getLabel(name) {
			return getLabel(String(name));
		},

		getItemLabel(name, itemValue) {
			return getItemLabel(String(name), itemValue);
		},

		async fill(name, value) {
			const label = getLabel(String(name));
			await byLabel(label).fill(value);
		},

		async check(name) {
			const label = getLabel(String(name));
			const locator = byLabel(label);
			const isChecked = await locator.isChecked();
			if (!isChecked) {
				await locator.click({ force: true });
			}
		},

		async uncheck(name) {
			const label = getLabel(String(name));
			const locator = byLabel(label);
			const isChecked = await locator.isChecked();
			if (isChecked) {
				await locator.click({ force: true });
			}
		},

		async checkItems(name, itemValues) {
			const metadata = getFieldMetadata(String(name));
			if (
				!metadata ||
				!("items" in metadata) ||
				!Array.isArray(metadata.items)
			) {
				throw new Error(`No items found for field: ${String(name)}`);
			}

			for (const item of metadata.items as Array<{
				value: string;
				label: string;
			}>) {
				const itemLabelText = resolveTranslation(item.label);
				const locator = page.getByLabel(itemLabelText);
				const isChecked = await locator.isChecked();
				const shouldBeChecked = itemValues.includes(item.value);

				if (shouldBeChecked && !isChecked) {
					await locator.click();
				} else if (!shouldBeChecked && isChecked) {
					await locator.click();
				}
			}
		},

		async select(name, optionValue) {
			const label = getLabel(String(name));
			const locator = byLabel(label);
			const tagName = await locator.evaluate((el) => el.tagName.toLowerCase());

			const metadata = getFieldMetadata(String(name));
			let resolvedOptionText = optionValue;
			if (metadata && "items" in metadata && Array.isArray(metadata.items)) {
				const item = metadata.items.find(
					(i: { value: string }) => i.value === optionValue,
				);
				if (item && typeof item.label === "string") {
					resolvedOptionText = resolveTranslation(item.label);
				}
			}

			if (tagName === "select") {
				await locator.selectOption(resolvedOptionText);
			} else {
				await locator.click();
				await page.getByRole("option", { name: resolvedOptionText }).click();
			}
		},

		async selectUser(name, userName) {
			const label = getLabel(String(name));
			const comboboxButton = page.getByLabel(label, { exact: true });
			const searchInput = page.getByTestId("user-search-input");
			const option = page.getByTestId("user-search-item").first();

			await expect(comboboxButton).not.toBeDisabled();
			await comboboxButton.click();
			await searchInput.fill(userName);
			await expect(option).toBeVisible();
			await page.keyboard.press("Enter");
		},

		async selectWeapons(_name, weaponNames) {
			for (const weaponName of weaponNames) {
				await page.getByTestId("weapon-select").click();
				await page.getByPlaceholder("Search weapons...").fill(weaponName);
				await page
					.getByRole("listbox", { name: "Suggestions" })
					.getByTestId(`weapon-select-option-${weaponName}`)
					.click();
			}
		},

		async setDateTime(name, date) {
			const label = getLabel(String(name));
			const hours = date.getHours();

			const fillSpinbutton = async (spinName: string, value: string) => {
				await page
					.getByRole("spinbutton", {
						name: new RegExp(`^${spinName}, ${label}`),
					})
					.fill(value);
			};

			await fillSpinbutton("year", date.getFullYear().toString());
			await fillSpinbutton("month", (date.getMonth() + 1).toString());
			await fillSpinbutton("day", date.getDate().toString());
			await fillSpinbutton("hour", String(hours % 12 || 12));
			await fillSpinbutton(
				"minute",
				date.getMinutes().toString().padStart(2, "0"),
			);
			await fillSpinbutton("AM/PM", hours >= 12 ? "PM" : "AM");
		},

		async setDate(name, date) {
			const label = getLabel(String(name));

			const fillSpinbutton = async (spinName: string, value: string) => {
				await page
					.getByRole("spinbutton", {
						name: new RegExp(`^${spinName}, ${label}`),
					})
					.fill(value);
			};

			await fillSpinbutton("year", date.getFullYear().toString());
			await fillSpinbutton("month", (date.getMonth() + 1).toString());
			await fillSpinbutton("day", date.getDate().toString());
		},

		async setImage(name, filePath) {
			const label = getLabel(String(name));
			await page.getByLabel(label).setInputFiles(filePath);
		},

		async submit() {
			await page.getByTestId(submitTestId).click();
		},
	};
}
