<script lang="ts">
	import Label from './Label.svelte';
	import { Country, type ICountry } from 'country-state-city';
	import Combobox, { type ComboboxOption } from './Combobox.svelte';
	import StyledRawInput from './StyledRawInput.svelte';
	import { AsYouType, type PhoneNumber, type CountryCode } from 'libphonenumber-js';
	import { generateIdentifier } from './util';
	import type { ClassValue } from 'svelte/elements';
	import { untrack } from 'svelte';

	interface Props {
		name?: string;
		value?: PhoneNumber;
		defaultISO?: string;
		required?: boolean;
		label?: string;
		placeholder?: string;
		invalidMessage?: string;
		class?: ClassValue | null | undefined;
	}

	let {
		name,
		value = $bindable<PhoneNumber | undefined>(undefined),
		defaultISO,
		required = false,
		label,
		placeholder = 'Phone number',
		invalidMessage = 'Valid phone number is required',
		class: classValue
	}: Props = $props();

	const id = $derived(generateIdentifier('phone-input', name));
	let lastRawValue = $state('');
	let lastValue = $state<PhoneNumber | undefined>(value);
	let country = $state<ICountry | undefined>();
	let selectedCountryItem = $state<ComboboxOption | undefined>();
	let countriesOpen = $state<boolean>(false);
	let countriesValid = $state<boolean>(true);
	let numberValid = $state<boolean>(true);

	const countries = Country.getAllCountries();
	const countrycodeMap = countries.reduce(
		(acc, country) => {
			acc[country.isoCode] = country;
			return acc;
		},
		{} as Record<string, ICountry>
	);
	const options: ComboboxOption[] = countries.map((country) => ({
		value: country.isoCode,
		label: `${country.name} (+${country.phonecode})`,
		preview: `+${country.phonecode}`,
		icon: renderIcon
	}));

	let phonecode: string = $derived.by(() => {
		if (country) {
			return countrycodeMap[country.isoCode]?.phonecode ?? '';
		}
		return '';
	});

	// setCountryByISO sets the country to match a given ISO code
	export const setCountryByISO = (iso: string) => {
		if (iso in countrycodeMap) {
			countriesOpen = false;
			country = countrycodeMap[iso];
			selectedCountryItem = options.find((item) => item.value === country?.isoCode);
		}
	};

	// setCountryByIP queries ip-api.com for user location based on IP
	export const setCountryByIP = async (ip: string) => {
		const res = await fetch(`http://ip-api.com/json/${ip}`);
		const locationData = await res.json();
		let country = 'CA'; // default to Canada
		if (locationData.status !== 'fail') {
			const { countryCode } = locationData;
			country = countryCode;
		}
		setCountryByISO(country);
	};

	// set the default country based on the provided ISO code
	if (defaultISO) {
		setCountryByISO(defaultISO);
	}

	// if country is still blank (probably invalid defaultISO), set it to the first country
	if (!(() => country)()) {
		country = countries[0];

		if (defaultISO)
			console.warn(
				`PhoneInput: Invalid defaultISO "${defaultISO}", defaulting to ${(() => country)().name}`
			);
	}

	selectedCountryItem = options.find((item) => item.value === country?.isoCode);

	let formatterCountryCode: CountryCode | undefined = $derived.by(() => {
		if (country) return country.isoCode as CountryCode;
		return undefined;
	});

	let renderValue = value?.formatNational() ?? '';
	let renderInputElement = $state<HTMLInputElement | null>(null);

	$effect(() => {
		if (value) {
			untrack(() => {
				if (renderInputElement) {
					renderInputElement.value = value?.formatNational() ?? '';
				}
			});
		}
	});
</script>

{#snippet renderIcon(item: ComboboxOption)}
	{#if countrycodeMap[item.value]?.flag}
		{countrycodeMap[item.value].flag}
	{/if}
{/snippet}

<div class={classValue}>
	{#if label}
		<Label for={id}>{label}</Label>
	{/if}

	<!-- Hidden input stores international E.164 formatted number -->
	<input type="hidden" {name} {id} value={value?.number ?? ''} />

	<div class="flex gap-2">
		<div
			class={['transition-width', countriesOpen && 'w-full!']}
			style="width: {phonecode.length * 1.5 + 12.5}ch;"
		>
			<Combobox
				{options}
				bind:value={selectedCountryItem}
				bind:open={countriesOpen}
				{required}
				onchange={(item) => {
					country = countrycodeMap[item.value];
				}}
				onvalidate={(e) => {
					countriesValid = e.detail.valid;
				}}
				invalidMessage={null}
			/>
		</div>

		<div class="w-full">
			<StyledRawInput
				value={renderValue}
				bind:ref={renderInputElement}
				type="tel"
				{placeholder}
				validate={{
					required: required,
					func: () => !required || (value !== undefined && value.isValid())
				}}
				onvalidate={(e) => {
					numberValid = e.detail.valid;
				}}
				onkeydown={(e) => {
					if (e.ctrlKey || e.key.length > 1) {
						return;
					}

					if (/[0-9-()+]/.test(e.key)) {
						return;
					}

					e.preventDefault();
				}}
				onbeforeinput={(event: Event) => {
					const target = event.target as HTMLInputElement;
					if (target) {
						lastRawValue = target.value;
					}
				}}
				oninput={(event: Event) => {
					const e = event as InputEvent;
					if (!e.target) return;
					const input = e.target as HTMLInputElement;

					const formatter = new AsYouType(formatterCountryCode);
					const formatted = formatter.input(input.value);

					if (input.value.length >= lastRawValue.length) {
						setTimeout(() => {
							input.value = formatted;
						}, 1);
					}
					lastValue = formatter.getNumber();

					if (formatter.isValid() && formatter.isInternational() && value) {
						const country = formatter.getCountry();
						if (country) {
							setCountryByISO(country);
							setTimeout(() => {
								if (lastValue) input.value = lastValue.formatNational();
							}, 1);
						}
					}
				}}
				onblur={() => {
					value = lastValue;
				}}
			/>
		</div>
	</div>

	<div class={['opacity-0 transition-opacity', (!countriesValid || !numberValid) && 'opacity-100']}>
		<Label for={id} error>
			{invalidMessage}
		</Label>
	</div>
</div>
