/**
 * The contact form, ported from `contact-form.ts` / `lead-api.ts` of landing-astro: validates the
 * fields and posts them as JSON to the management new message API (NEW_MESSAGE_API_URL).
 *
 * The API requires `name`, `email`, `phone` and `message`; the form has no message field, so the
 * message is built from the company name. `company_name` and `source` (APP_NAME) are sent as
 * well.
 *
 * Markup contract: see ContactForm.astro; the `[data-contact-form]` JSON value is the form config.
 * Field errors are shown in `[data-error-for="<field name>"]` elements; the matching controls get
 * `aria-invalid`.
 */
import { NEW_MESSAGE_API_URL } from 'astro:env/client';

// Same patterns as landing-astro; the API validates the phone number with the same one.
const EMAIL_PATTERN =
	/^((?:[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~]|(?<=^|\.)"|"(?=$|\.|@)|(?<=".*)[ .](?=.*")|(?<!\.)\.){1,64})(@)((?:[A-Za-z0-9.\-])*(?:[A-Za-z0-9])\.(?:[A-Za-z0-9]){2,})$/;
const HU_PHONE_PATTERN = /^(\+36|0036|06)\s?(1|20|30|31|70|[2-9]\d)\s?\d{3}\s?\d{3,4}$/;

type Config = {
	source: string;
	/** `{companyName}` is replaced with the field's value. */
	message: string;
	validation: Record<
		'nameRequired' | 'companyNameRequired' | 'emailRequired' | 'emailInvalid' | 'phoneRequired' | 'phoneInvalid',
		string
	>;
};

type SubmitResult =
	| { status: 'sent' }
	| { status: 'invalid'; message?: string; errors: Record<string, string> }
	| { status: 'failed' };

async function postMessage(payload: Record<string, string>): Promise<SubmitResult> {
	if (!NEW_MESSAGE_API_URL) return { status: 'failed' };

	try {
		const res = await fetch(NEW_MESSAGE_API_URL, {
			method: 'POST',
			headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
			body: JSON.stringify(payload),
		});

		if (res.status === 422) {
			const data: { message?: string; errors?: Record<string, string[]> } = await res.json();
			const errors: Record<string, string> = {};
			for (const [field, messages] of Object.entries(data.errors ?? {})) {
				if (messages[0]) errors[field] = messages[0];
			}

			return { status: 'invalid', message: data.message, errors };
		}

		return res.ok ? { status: 'sent' } : { status: 'failed' };
	} catch {
		return { status: 'failed' };
	}
}

/** Shows or clears a field's error; returns whether the form has a place to show it. */
function setFieldError(form: HTMLFormElement, field: string, message: string | null) {
	const error = form.querySelector<HTMLElement>(`[data-error-for="${field}"]`);
	if (error) {
		error.textContent = message ?? '';
		error.hidden = !message;
	}
	for (const control of form.querySelectorAll(`[name="${field}"]`)) {
		if (message) control.setAttribute('aria-invalid', 'true');
		else control.removeAttribute('aria-invalid');
	}

	return Boolean(error);
}

function init() {
	const wrapper = document.querySelector<HTMLElement>('[data-contact-form]');
	const form = wrapper?.querySelector('form');
	if (!wrapper || !form) return;

	const config: Config = JSON.parse(wrapper.dataset.contactForm ?? '{}');
	const v = config.validation;
	const fieldset = form.querySelector('fieldset')!;
	const generalError = form.querySelector<HTMLElement>('[data-contact-error]')!;

	function readFields() {
		const data = new FormData(form!);
		const text = (name: string) => String(data.get(name) ?? '').trim();

		return { name: text('name'), company_name: text('company_name'), phone: text('phone'), email: text('email') };
	}

	/** Shows the field errors and reports whether the form is valid. */
	function validate(fields: ReturnType<typeof readFields>) {
		const errors: Record<string, string> = {};

		if (!fields.name) errors.name = v.nameRequired;
		if (!fields.company_name) errors.company_name = v.companyNameRequired;
		if (!fields.phone) errors.phone = v.phoneRequired;
		else if (!HU_PHONE_PATTERN.test(fields.phone)) errors.phone = v.phoneInvalid;
		if (!fields.email) errors.email = v.emailRequired;
		else if (!EMAIL_PATTERN.test(fields.email.toLowerCase())) errors.email = v.emailInvalid;

		for (const [field, message] of Object.entries(errors)) setFieldError(form!, field, message);

		return Object.keys(errors).length === 0;
	}

	function clearErrors() {
		for (const error of form!.querySelectorAll<HTMLElement>('[data-error-for]')) {
			setFieldError(form!, error.dataset.errorFor!, null);
		}
		generalError.hidden = true;
	}

	function showGeneralError(message?: string) {
		generalError.textContent = message || generalError.dataset.fallback || '';
		generalError.hidden = false;
	}

	function setLoading(loading: boolean) {
		fieldset.disabled = loading;
		form!.querySelector<HTMLElement>('[data-contact-ready]')!.hidden = loading;
		form!.querySelector<HTMLElement>('[data-contact-loading]')!.hidden = !loading;
	}

	async function submit() {
		clearErrors();

		// Read before disabling the fieldset, as disabled controls are left out of FormData.
		const fields = readFields();
		if (!validate(fields)) return;

		setLoading(true);
		const result = await postMessage({
			...fields,
			message: config.message.replace('{companyName}', fields.company_name),
			source: config.source,
		});
		setLoading(false);

		if (result.status === 'invalid') {
			// Errors of fields the form doesn't show (e.g. `message`) go to the general error.
			const unshown = Object.entries(result.errors)
				.filter(([field, message]) => !setFieldError(form!, field, message))
				.map(([, message]) => message);
			if (unshown.length) showGeneralError(unshown.join(' '));
			else if (Object.keys(result.errors).length === 0) showGeneralError(result.message);
		} else if (result.status === 'failed') {
			showGeneralError();
		} else {
			form!.hidden = true;
			wrapper!.querySelector<HTMLElement>('[data-contact-success]')!.hidden = false;
		}
	}

	// Clears a field's error as soon as it is edited.
	form.addEventListener('input', (event) => {
		const name = (event.target as HTMLInputElement).name;
		if (name) setFieldError(form, name, null);
	});
	form.addEventListener('submit', (event) => {
		event.preventDefault();
		void submit();
	});
}

init();
