<script setup lang="ts" generic="T extends Record<string, any>">
import type { SchemaDefinition, FieldBuilder } from './form-flow'
import { computed, nextTick, ref, watch } from 'vue'
import ArrayInput from '../components/form/inputs/ArrayInput.vue'
import CheckInput from '../components/form/inputs/CheckInput.vue'
import CodeEditor from '../components/form/inputs/CodeEditor/Index.vue'
import ColorInput from '../components/form/inputs/ColorInput.vue'
import DateInput from '../components/form/inputs/DateInput.vue'
import EmailInput from '../components/form/inputs/EmailInput.vue'
import NumberInput from '../components/form/inputs/NumberInput.vue'
import PasswordInput from '../components/form/inputs/PasswordInput.vue'
import RadioGroup from '../components/form/inputs/RadioGroup.vue'
import RangeInput from '../components/form/inputs/RangeInput.vue'
import RichText from '../components/form/inputs/RichText/index.vue'
import SelectBtn from '../components/form/inputs/SelectBtn.vue'
import SelectInput from '../components/form/inputs/SelectInput.vue'
import TelInput from '../components/form/inputs/TelInput.vue'
import TextInput from '../components/form/inputs/TextInput.vue'
import ToggleInput from '../components/form/inputs/ToggleInput.vue'
import UploadInput from '../components/form/inputs/Upload/UploadInput.vue'
import { useI18n, resolveI18n } from '../i18n'
import { evaluateQuery } from '../utils/queryFilter'
import { EMAIL_REGEX } from '../utils/constants'

export type FormState = 'idle' | 'submitting' | 'success' | 'error'

export interface Props<T extends Record<string, any> = Record<string, any>> {
	schema: SchemaDefinition<T>
	modelValue?: T
	components?: Record<string, any>
	errors?: Record<string, string>
	/** Async submit handler — FormFlow awaits it and manages formState. */
	onSubmit?: (data: T) => Promise<void> | void
}

type FormFlowSlots<T> = {
	submit?: (_: {
		submit: (event?: Event) => Promise<boolean>
		validate: () => boolean
		formData: T
		errors: Record<string, string>
		formState: FormState
	}) => any
	success?: (_: { formData: T }) => any
	error?: (_: { formData: T }) => any
	errors?: (_: {
		errors: Record<string, string>
		errorCount: number
		visibleFields: { key: string, field: FieldBuilder }[]
	}) => any
} & Partial<Record<`field:${string}`, (_: {
	value: any
	field: FieldBuilder
	formData: T
	error: string
	onChange: (val: any) => void
}) => any>>

const props = defineProps<Props<T>>()

const emit = defineEmits<{
	'update:modelValue': [value: T]
}>()

defineSlots<FormFlowSlots<T>>()

const { $t } = useI18n()

function getNestedValue(obj: any, path: string): any {
	const keys = path.split('.')
	let value = obj
	for (const key of keys) {
		value = value?.[key]
	}
	return value
}

function setNestedValue(obj: any, path: string, value: any): void {
	const keys = path.split('.')
	let target = obj

	for (let i = 0; i < keys.length - 1; i++) {
		const key = keys[i]
		if (target[key] == null || typeof target[key] !== 'object') {
			target[key] = {}
		}
		target = target[key]
	}

	target[keys[keys.length - 1]] = value
}

function applyDefaults(modelValue: T | undefined): T {
	const data = { ...(modelValue ?? {}) } as T
	for (const [key, field] of Object.entries(props.schema._fields)) {
		const defaultValue = (field as FieldBuilder)._config.default
		if (defaultValue !== undefined && getNestedValue(data, key) === undefined) {
			setNestedValue(data, key, defaultValue)
		}
	}
	return data
}

const formData = ref(applyDefaults(props.modelValue)) as any
const validationErrors = ref<Record<string, string>>({})
const hasSubmitted = ref(false)
const formState = ref<FormState>('idle')
const formElement = ref<HTMLFormElement>()
let isInternalUpdate = false

watch(() => props.modelValue, (newVal) => {
	if (!isInternalUpdate) {
		formData.value = applyDefaults(newVal)
	}
}, { deep: true })

watch(formData, (newVal) => {
	isInternalUpdate = true
	emit('update:modelValue', newVal)
	nextTick(() => { isInternalUpdate = false })
}, { deep: true })

function shouldShowField(field: FieldBuilder): boolean {
	if (field._condition == null) return true
	return evaluateQuery(field._condition, formData.value)
}

const visibleFields = computed(() => {
	const fields: Array<{ key: string, field: FieldBuilder }> = []

	for (const [key, field] of Object.entries(props.schema._fields)) {
		if (shouldShowField(field)) {
			fields.push({ key, field })
		}
	}

	return fields
})

function updateField(key: string, value: any, field: FieldBuilder) {
	setNestedValue(formData.value, key, value)
	if (formState.value === 'success' || formState.value === 'error') {
		formState.value = 'idle'
	}
	if (hasSubmitted.value) {
		validateField(key, field)
	}
}

function isFieldEmpty(value: any, field: FieldBuilder): boolean {
	return value == null || value === ''
		|| (Array.isArray(value) && value.length === 0)
		|| (field._type === 'tel' && typeof value === 'string' && /^\+\d+$/.test(value))
}

function validateField(key: string, field: FieldBuilder): boolean {
	const value = getFieldValue(key)

	if (field._config.required === true && isFieldEmpty(value, field)) {
		validationErrors.value = { ...validationErrors.value, [key]: (field._config.requiredMessage as string | undefined) ?? $t('form.required') }
		return false
	}

	if (field._type === 'email' && value && !EMAIL_REGEX.test(value)) {
		validationErrors.value = { ...validationErrors.value, [key]: $t('form.invalidEmail') }
		return false
	}

	if (field._validations != null) {
		for (const validate of field._validations) {
			const result = validate(value)
			if (result !== true) {
				validationErrors.value = { ...validationErrors.value, [key]: result }
				return false
			}
		}
	}

	validationErrors.value = Object.fromEntries(
		Object.entries(validationErrors.value).filter(([k]) => k !== key)
	)
	return true
}

function getFieldValue(key: string): any {
	return getNestedValue(formData.value, key)
}

function generateLabel(key: string, field: FieldBuilder): string {
	if (field._config.label != null) return field._config.label

	// Convert camelCase or snake_case to Title Case
	return key
		.split(/[._-]/)
		.map(word => word.charAt(0).toUpperCase() + word.slice(1))
		.join(' ')
}

function getFieldComponent(fieldOrType: FieldBuilder | string) {
	// display: 'btn' on select/multiselect renders as SelectBtn
	if (typeof fieldOrType !== 'string' && (fieldOrType._type === 'select' || fieldOrType._type === 'multiselect') && fieldOrType._config.display === 'btn') {
		return SelectBtn
	}

	const type = typeof fieldOrType === 'string' ? fieldOrType : fieldOrType._type
	const componentMap: Record<string, any> = {
		text: TextInput,
		email: EmailInput,
		password: PasswordInput,
		number: NumberInput,
		tel: TelInput,
		url: TextInput, // Use TextInput for URL
		date: DateInput,
		time: TextInput, // TODO: Create TimeInput
		datetime: DateInput,
		textarea: TextInput, // TextInput supports multiline
		richtext: RichText, // TODO: Use RichTextEditor when available
		json: CodeEditor,
		checkbox: CheckInput,
		toggle: ToggleInput,
		radio: RadioGroup,
		select: SelectInput,
		multiselect: SelectInput,
		array: ArrayInput,
		upload: UploadInput,
		color: ColorInput,
		range: RangeInput,
	}

	return (componentMap[type] as typeof TextInput | undefined) ?? TextInput
}

const mergedErrors = computed(() => {
	const combined = { ...validationErrors.value, ...(props.errors || {}) }
	const resolved: Record<string, string> = {}
	for (const [key, val] of Object.entries(combined)) {
		resolved[key] = resolveI18n(val) ?? val
	}
	return resolved
})

function getFieldProps(field: FieldBuilder, key: string) {
	const label = generateLabel(key, field)
	const baseProps: any = {
		label,
		'modelValue': getFieldValue(key),
		'onUpdate:modelValue': (val: any) => { updateField(key, val, field) },
		'error': mergedErrors.value[key],
		...field._config,
		name: field._config.name ?? key,
	}

	if (field._type === 'textarea') {
		baseProps.multiline = true
	}

	if (field._type === 'multiselect') {
		baseProps.multiselect = true
	}

	if (field._type === 'json') {
		baseProps.language = baseProps.language ?? 'json'
	}

	if (field._type === 'array') {
		const configWithoutSchema = Object.fromEntries(
			Object.entries(field._config).filter(([k]) => k !== 'schema' && k !== 'itemType')
		)
		Object.assign(baseProps, configWithoutSchema)
		baseProps.allowAdd = baseProps.allowAdd ?? true
		baseProps.allowDelete = baseProps.allowDelete ?? true
	}
	return baseProps
}

function resolveComponent(componentRef: any): any {
	if (typeof componentRef === 'string') {
		if (props.components?.[componentRef] != null) {
			return props.components[componentRef]
		}
		return componentRef
	}
	return componentRef
}

function usesCustomComponent(field: FieldBuilder): boolean {
	return field._component != null
}

function validateAll(): boolean {
	let isValid = true
	const newErrors: Record<string, string> = {}

	for (const { key, field } of visibleFields.value) {
		const value = getFieldValue(key)

		if (field._config.required === true && isFieldEmpty(value, field)) {
			newErrors[key] = (field._config.requiredMessage as string | undefined) ?? $t('form.required')
			isValid = false
			continue
		}

		if (field._type === 'email' && value && !EMAIL_REGEX.test(value)) {
			newErrors[key] = $t('form.invalidEmail')
			isValid = false
			continue
		}

		if (field._validations != null) {
			for (const validate of field._validations) {
				const result = validate(value)
				if (result !== true) {
					newErrors[key] = result
					isValid = false
					break
				}
			}
		}
	}

	validationErrors.value = newErrors
	return isValid
}

async function handleSubmit(event?: Event): Promise<boolean> {
	if (event) {
		event.preventDefault()
	}

	hasSubmitted.value = true
	const isValid = validateAll()

	if (!isValid) {
		event?.stopPropagation()

		const firstErrorKey = visibleFields.value.find(({ key }) => validationErrors.value[key])?.key

		if (firstErrorKey != null && formElement.value) {
			const fieldElement
				= formElement.value.querySelector(`[name="${firstErrorKey}"]`)
					|| formElement.value.querySelector(`[id="${firstErrorKey}"]`)
					|| formElement.value.querySelector(`[data-field="${firstErrorKey}"]`)
					|| formElement.value.querySelector('input, select, textarea')

			if (fieldElement) {
				fieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
				setTimeout(() => {
					if (fieldElement instanceof HTMLElement) fieldElement.focus()
				}, 300)
			}
		}
	} else if (props.onSubmit) {
		formState.value = 'submitting'
		try {
			await props.onSubmit(formData.value)
			formState.value = 'success'
		} catch {
			formState.value = 'error'
		}
	}

	return isValid
}

defineExpose({
	validate: validateAll,
	submit: handleSubmit,
	formState
})
</script>

<template>
	<form ref="formElement" class="grid-form" :class="schema._class" @submit="handleSubmit">
		<template v-if="formState === 'success'">
			<slot name="success" :form-data="formData" />
		</template>
		<template v-else>
			<div v-for="{ key, field } in visibleFields" :key="key" :class="[field._class, field._config.class]">
				<!-- Custom slot for specific field -->
				<slot
					:name="`field:${key}`"
					:value="getFieldValue(key)"
					:field="field"
					:form-data="formData"
					:error="validationErrors[key]"
					:on-change="(val: any) => { updateField(key, val, field) }"
				>
					<!-- Array field with nested schema -->
					<template v-if="field._type === 'array' && field._config.schema?._isSchema">
						<ArrayInput v-bind="getFieldProps(field, key)">
							<template #default="{ item }">
							<!-- @vue-ignore -->
							<FormFlow
								:model-value="(item.value as any)"
								:schema="(field._config.schema as any)"
								:components="components"
								@update:model-value="(val: any) => { item.value = val }"
							/>
							</template>
						</ArrayInput>
					</template>

					<!-- Array field with primitive string item type e.g. $.array('Label', 'text') -->
					<template v-else-if="field._type === 'array' && typeof field._config.schema === 'string'">
						<ArrayInput v-bind="getFieldProps(field, key)">
							<template #default="{ item }">
								<component
									:is="getFieldComponent(field._config.schema as string)"
									:model-value="(item.value as string | number | undefined)"
									@update:model-value="(val: any) => { item.value = val }"
								/>
							</template>
						</ArrayInput>
					</template>

					<!-- Array field with custom component -->
					<template v-else-if="field._type === 'array' && field._config.schema">
						<ArrayInput v-bind="getFieldProps(field, key)">
							<template #default="{ item }">
								<component :is="resolveComponent(field._config.schema)" v-model="item.value" />
							</template>
						</ArrayInput>
					</template>

					<!-- Array field with primitive item type (e.g. from JSON Schema type: array with items: { type: string }) -->
					<template v-else-if="field._type === 'array' && field._config.itemType">
						<ArrayInput v-bind="getFieldProps(field, key)">
							<template #default="{ item }">
								<component
									:is="getFieldComponent(field._config.itemType)"
									:model-value="(item.value as string | number | undefined)"
									@update:model-value="(val: any) => { item.value = val }"
								/>
							</template>
						</ArrayInput>
					</template>

					<!-- Custom component rendering -->
					<template v-else-if="usesCustomComponent(field)">
						<component
							:is="resolveComponent(field._component!.component)"
							:model-value="getFieldValue(key)"
							v-bind="field._component!.props"
							@update:model-value="(val: any) => updateField(key, val, field)"
						/>
					</template>

					<!-- Default field rendering -->
					<component :is="getFieldComponent(field)" v-else v-bind="getFieldProps(field, key)" />
				</slot>
			</div>

			<!-- Form-level errors summary slot -->
			<slot
				name="errors"
				:errors="mergedErrors"
				:error-count="Object.keys(mergedErrors).filter(k => mergedErrors[k]).length"
				:visible-fields="visibleFields"
			/>

			<!-- Error slot renders above submit, form remains visible -->
			<slot v-if="formState === 'error'" name="error" :form-data="formData" />

			<!-- Submit slot -->
			<slot
				name="submit"
				:submit="handleSubmit"
				:validate="validateAll"
				:form-data="formData"
				:errors="mergedErrors"
				:form-state="formState"
			/>
		</template>
	</form>
</template>
