/**
 * Form Flow - Type-safe Form Schema Builder
 *
 * A declarative, type-safe way to define forms that work with:
 * - FormFlow (single-step forms)
 * - MultiStepForm (multi-step forms)
 * - DataTable (column definitions)
 * - Filter (filter field definitions)
 */

import type { Component } from 'vue'
import type { IconType } from '../types'
import { getOptionValue } from '../utils/options'

// ============================================================================
// Core Types
// ============================================================================

type Primitive = string | number | boolean | null | undefined | Date | ((...args: any[]) => any)

/** Option object format for select/radio/multiselect fields */
export interface SelectOption<T extends string = string> {
	label: string
	value: T
	icon?: IconType
}

/** Async options loader function */
export type AsyncOptionsLoader<T extends string = string> = (query: string) => Promise<(T | SelectOption<T>)[]>

/** Options can be string array, object array, or async loader function */
export type SelectOptions<T extends string = string>
	= | T[]
		| readonly T[]
		| SelectOption<T>[]
		| readonly SelectOption<T>[]
		| AsyncOptionsLoader<T>

/**
 * Extract all possible nested keys from a type, including:
 * - Dot notation paths: 'meta.bio'
 * - Array index paths: 'rules[0].name'
 *
 * @example
 * interface Form {
 *   name: string
 *   meta: { bio: string }
 *   rules: Array<{ field: string, value: number }>
 * }
 *
 * type Keys = NestedKeyOf<Form>
 * // 'name' | 'meta' | 'meta.bio' | 'rules' | 'rules[0]' | 'rules[0].field' | 'rules[0].value'
 */
export type NestedKeyOf<T> = T extends Primitive
	? never
	: {
		[K in keyof T & string]:
			| K
			| (T[K] extends Primitive
				? never
				: T[K] extends Array<infer U>
					? `${K}[${number}]` | (U extends Primitive ? never : `${K}[${number}].${NestedKeyOf<U>}`)
					: `${K}.${NestedKeyOf<T[K]>}`)
	}[keyof T & string]

interface BaseFieldConfig {
	label?: string
	required?: boolean
	placeholder?: string
	helpText?: string
	underlined?: boolean
	default?: any
	class?: string
}

interface TextFieldConfig {
	multiline?: boolean
	pattern?: string
	icon?: IconType
	rows?: number
	autofocus?: boolean
}

/**
 * Syntactic sugar for required: true
 * @example $.email({ required }) instead of $.email({ required: true })
 */
export const required = true

export interface FieldBuilder<TValue = any> {
	_type: string
	_config: BaseFieldConfig & Record<string, any>
	_condition?: string
	_validations?: Array<(value: TValue) => true | string>
	_onUpdate?: (value: TValue, formData: Record<string, any>) => void
	_class?: string
	_slots?: Array<SlotConfig>
	_component?: ComponentConfig

	// Chainable methods
	if: (condition: string) => FieldBuilder<TValue>
	validate: (fn: (value: TValue) => true | string) => FieldBuilder<TValue>
	onUpdate: (fn: (value: TValue, formData: Record<string, any>) => void) => FieldBuilder<TValue>
	class: (className: string) => FieldBuilder<TValue>
	required: (message?: string) => FieldBuilder<TValue>
	slot: {
		(component: Component | string, props?: Record<string, any>): FieldBuilder<TValue>
		(slotName: string, component: Component | string, props?: Record<string, any>): FieldBuilder<TValue>
	}
}

interface SlotConfig {
	name?: string
	component: Component | string
	props?: Record<string, any>
}

interface ComponentConfig {
	component: Component | string
	props?: Record<string, any>
}

export interface SchemaDefinition<T extends Record<string, any> = Record<string, any>> {
	/** Phantom type — not present at runtime, used for FormFlow generic inference */
	readonly __type: T
	_isSchema: true
	_label?: string
	_fields: Record<string, FieldBuilder>
	_condition?: string
	_class?: string

	if: (condition: string) => SchemaDefinition<T>
	class: (className: string) => SchemaDefinition<T>
	toJSONSchema: () => any
}

// ============================================================================
// Field Builder Base Class
// ============================================================================

class Field<TValue = any> implements FieldBuilder<TValue> {
	_type: string
	_config: BaseFieldConfig & Record<string, any>
	_condition?: string
	_validations?: Array<(value: TValue) => true | string>
	_onUpdate?: (value: TValue, formData: Record<string, any>) => void
	_class?: string
	_slots?: Array<SlotConfig>
	_component?: ComponentConfig

	constructor(type: string, config: BaseFieldConfig & Record<string, any> = {}) {
		this._type = type
		this._config = config
	}

	if(condition: string): this {
		this._condition = condition
		return this
	}

	validate(fn: (value: TValue) => true | string): this {
		if (!this._validations) {
			this._validations = []
		}
		this._validations.push(fn)
		return this
	}

	onUpdate(fn: (value: TValue, formData: Record<string, any>) => void): this {
		this._onUpdate = fn
		return this
	}

	class(className: string): this {
		this._class = this._class ? `${this._class} ${className}` : className
		return this
	}

	required(message?: string): this {
		this._config.required = true
		if (message) {
			this._config.requiredMessage = message
		}
		return this
	}

	slot(componentOrSlotName: Component | string, componentOrProps?: Component | string | Record<string, any>, maybeProps?: Record<string, any>): this {
		if (!this._slots) {
			this._slots = []
		}

		// Overload 1: .slot(Component, props?)
		if (typeof componentOrSlotName !== 'string' || (componentOrProps !== undefined && typeof componentOrProps !== 'string')) {
			this._slots.push({
				component: componentOrSlotName as Component | string,
				props: componentOrProps as Record<string, any> | undefined
			})
		}
		// Overload 2: .slot('slotName', Component, props?)
		else {
			this._slots.push({
				name: componentOrSlotName,
				component: componentOrProps as Component | string,
				props: maybeProps
			})
		}

		return this
	}
}

// ============================================================================
// Field Builders ($)
// ============================================================================

// Helper to parse (label, config) or (config) signatures
function parseArgs<C extends BaseFieldConfig>(
	labelOrConfig?: string | C,
	maybeConfig?: C
): C {
	if (typeof labelOrConfig === 'string') {
		return { ...maybeConfig, label: labelOrConfig } as C
	}
	return (labelOrConfig ?? {}) as C
}

export const $ = {
	text(
		labelOrConfig?: string | (BaseFieldConfig & TextFieldConfig),
		config?: BaseFieldConfig & TextFieldConfig
	): FieldBuilder<string> {
		return new Field('text', parseArgs(labelOrConfig, config))
	},

	email(labelOrConfig?: string | BaseFieldConfig, config?: BaseFieldConfig): FieldBuilder<string> {
		return new Field('email', parseArgs(labelOrConfig, config))
	},

	password(labelOrConfig?: string | BaseFieldConfig, config?: BaseFieldConfig): FieldBuilder<string> {
		return new Field('password', parseArgs(labelOrConfig, config))
	},

	number(
		labelOrConfig?: string | (BaseFieldConfig & { min?: number, max?: number, step?: number }),
		config?: BaseFieldConfig & { min?: number, max?: number, step?: number }
	): FieldBuilder<number> {
		return new Field('number', parseArgs(labelOrConfig, config))
	},

	tel(
		labelOrConfig?: string | (BaseFieldConfig & { onlyCountries?: string[], defaultCountry?: string }),
		config?: BaseFieldConfig & { onlyCountries?: string[], defaultCountry?: string }
	): FieldBuilder<string> {
		return new Field('tel', parseArgs(labelOrConfig, config))
	},

	url(labelOrConfig?: string | BaseFieldConfig, config?: BaseFieldConfig): FieldBuilder<string> {
		return new Field('url', parseArgs(labelOrConfig, config))
	},

	date(
		labelOrConfig?: string | (BaseFieldConfig & { min?: string, max?: string }),
		config?: BaseFieldConfig & { min?: string, max?: string }
	): FieldBuilder<string> {
		return new Field('date', parseArgs(labelOrConfig, config))
	},

	time(labelOrConfig?: string | BaseFieldConfig, config?: BaseFieldConfig): FieldBuilder<string> {
		return new Field('time', parseArgs(labelOrConfig, config))
	},

	datetime(labelOrConfig?: string | BaseFieldConfig, config?: BaseFieldConfig): FieldBuilder<string> {
		return new Field('datetime', parseArgs(labelOrConfig, config))
	},

	textarea(
		labelOrConfig?: string | (BaseFieldConfig & { rows?: number, cols?: number }),
		config?: BaseFieldConfig & { rows?: number, cols?: number }
	): FieldBuilder<string> {
		return new Field('textarea', parseArgs(labelOrConfig, config))
	},

	richtext(
		labelOrConfig?: string | (BaseFieldConfig & { autoheight?: boolean, basic?: boolean }),
		config?: BaseFieldConfig & { autoheight?: boolean, basic?: boolean }
	): FieldBuilder<string> {
		return new Field('richtext', parseArgs(labelOrConfig, config))
	},

	json(
		labelOrConfig?: string | (BaseFieldConfig & { language?: string, height?: string }),
		config?: BaseFieldConfig & { language?: string, height?: string }
	): FieldBuilder<Record<string, any>> {
		const parsed = parseArgs(labelOrConfig, config)
		// Default language to 'json' for CodeEditor
		if (!parsed.language) parsed.language = 'json'
		return new Field('json', parsed)
	},

	checkbox(labelOrConfig?: string | BaseFieldConfig, config?: BaseFieldConfig): FieldBuilder<boolean> {
		return new Field('checkbox', parseArgs(labelOrConfig, config))
	},

	toggle(labelOrConfig?: string | BaseFieldConfig, config?: BaseFieldConfig): FieldBuilder<boolean> {
		return new Field('toggle', parseArgs(labelOrConfig, config))
	},

	radio<T extends string = string>(
		optionsOrLabel: SelectOptions<T> | string,
		labelOrOptionsOrConfig?: string | SelectOptions<T> | BaseFieldConfig,
		config?: BaseFieldConfig
	): FieldBuilder<T> {
		// Label-first: $.radio('Gender', ['male', 'female'])
		if (typeof optionsOrLabel === 'string') {
			const label = optionsOrLabel
			const options = labelOrOptionsOrConfig as SelectOptions<T>
			return new Field('radio', { ...config, label, options })
		}
		// Options-first: $.radio(['male', 'female'], 'Gender') or $.radio(['male', 'female'], { label: 'Gender' })
		return new Field('radio', { ...parseArgs(labelOrOptionsOrConfig as string | BaseFieldConfig, config), options: optionsOrLabel })
	},

	select<T extends string = string>(
		optionsOrLabel: SelectOptions<T> | string,
		labelOrOptionsOrConfig?: string | SelectOptions<T> | (BaseFieldConfig & { searchable?: boolean }),
		config?: BaseFieldConfig & { searchable?: boolean }
	): FieldBuilder<T> {
		// Label-first: $.select('Country', ['USA', 'Canada'])
		if (typeof optionsOrLabel === 'string') {
			const label = optionsOrLabel
			const options = labelOrOptionsOrConfig as SelectOptions<T>
			return new Field('select', { ...config, label, options })
		}
		// Options-first: $.select(['USA', 'Canada'], 'Country') or $.select(['USA', 'Canada'], { label: 'Country' })
		return new Field('select', { ...parseArgs(labelOrOptionsOrConfig as string | BaseFieldConfig, config), options: optionsOrLabel })
	},

	multiselect<T extends string = string>(
		optionsOrLabel: SelectOptions<T> | string,
		labelOrOptionsOrConfig?: string | SelectOptions<T> | (BaseFieldConfig & { searchable?: boolean }),
		config?: BaseFieldConfig & { searchable?: boolean }
	): FieldBuilder<T[]> {
		// Label-first: $.multiselect('Tags', ['tech', 'news'])
		if (typeof optionsOrLabel === 'string') {
			const label = optionsOrLabel
			const options = labelOrOptionsOrConfig as SelectOptions<T>
			return new Field('multiselect', { ...config, label, options, multiple: true })
		}
		// Options-first: $.multiselect(['tech', 'news'], 'Tags') or $.multiselect(['tech', 'news'], { label: 'Tags' })
		return new Field('multiselect', { ...parseArgs(labelOrOptionsOrConfig as string | BaseFieldConfig, config), options: optionsOrLabel })
	},

	color(labelOrConfig?: string | BaseFieldConfig, config?: BaseFieldConfig): FieldBuilder<string> {
		return new Field('color', parseArgs(labelOrConfig, config))
	},

	upload(
		labelOrConfig?: string | (BaseFieldConfig & {
			multiple?: boolean
			accept?: string
			capture?: boolean | 'user' | 'environment'
			dirPath?: string
			width?: string
			height?: string
			icon?: IconType
			theme?: 'dropzone' | 'basic'
			fill?: boolean
			oval?: boolean
			placeholder?: string
			noFilePlaceholder?: string
			btnPlaceholder?: string
			style?: string
		}),
		config?: BaseFieldConfig & {
			multiple?: boolean
			accept?: string
			capture?: boolean | 'user' | 'environment'
			dirPath?: string
			width?: string
			icon?: IconType
			height?: string
			theme?: 'dropzone' | 'basic'
			fill?: boolean
			oval?: boolean
			placeholder?: string
			noFilePlaceholder?: string
			btnPlaceholder?: string
			style?: string
		}
	): FieldBuilder<string | string[]> {
		return new Field('upload', parseArgs(labelOrConfig, config))
	},

	array(
		labelOrSchemaOrComponent?: string | SchemaDefinition | Component,
		schemaOrComponentOrConfig?: SchemaDefinition | Component | string | (BaseFieldConfig & {
			allowAdd?: boolean
			allowDelete?: boolean
			allowReorder?: boolean
			collapsible?: boolean
			min?: number
			max?: number
		}),
		maybeConfig?: BaseFieldConfig & {
			allowAdd?: boolean
			allowDelete?: boolean
			allowReorder?: boolean
			collapsible?: boolean
			min?: number
			max?: number
		}
	): FieldBuilder<any[]> {
		// Label-first: $.array('Items', schema) or $.array('Items', schema, { min: 1 })
		if (typeof labelOrSchemaOrComponent === 'string') {
			const label = labelOrSchemaOrComponent
			const schema = schemaOrComponentOrConfig as SchemaDefinition | Component | string | undefined
			const config = maybeConfig ?? {}
			return new Field('array', { ...config, label, schema })
		}
		// Schema-first: $.array(schema, 'Items') or $.array(schema, { label: 'Items' })
		const schema = labelOrSchemaOrComponent
		const parsed = parseArgs(schemaOrComponentOrConfig as string | BaseFieldConfig, maybeConfig)
		return new Field('array', { ...parsed, schema })
	},

	component(
		component: Component | string,
		props: Record<string, any> = {}
	): FieldBuilder {
		const field = new Field('component', props)
		field._component = { component, props }
		return field
	},

	/**
	 * Display-only component that receives formData but doesn't bind to a field value.
	 * Useful for summaries, previews, info panels, etc.
	 *
	 * @example
	 * $.display(Summary)
	 * $.display(Summary, { showPrice: true })
	 *
	 * // The component receives: { formData, ...props }
	 */
	display(
		component: Component | string,
		props: Record<string, any> = {}
	): FieldBuilder<void> {
		const field = new Field('display', props)
		field._component = { component, props }
		return field
	}
}

// ============================================================================
// Schema Definition
// ============================================================================

class Schema<T extends Record<string, any> = Record<string, any>> implements SchemaDefinition<T> {
	declare readonly __type: T
	_isSchema = true as const
	_label?: string
	_fields: Record<string, FieldBuilder>
	_condition?: string
	_class?: string

	constructor(fields: Record<string, FieldBuilder>, label?: string) {
		this._fields = fields
		this._label = label
	}

	if(condition: string): this {
		this._condition = condition
		return this
	}

	class(className: string): this {
		this._class = this._class ? `${this._class} ${className}` : className
		return this
	}

	toJSONSchema(): any {
		const properties: Record<string, any> = {}
		const requiredFields: string[] = []

		for (const [key, field] of Object.entries(this._fields)) {
			const config = field._config
			const jsonSchemaType = Schema.getJSONSchemaType(field._type)

			// Build standard JSONSchema properties
			const prop: Record<string, any> = {
				type: jsonSchemaType,
			}

			// Map standard properties
			if (config.label) prop.title = config.label
			if (config.helpText) prop.description = config.helpText
			if (config.default !== undefined) prop.default = config.default
			if (config.options) {
				const enumValues = (config.options as any[]).map(o => getOptionValue(o) ?? o)
				if (field._type === 'multiselect') {
					prop.items = { type: 'string', enum: enumValues }
				} else {
					prop.enum = enumValues
				}
			}

			// Numeric constraints
			if (config.min !== undefined) {
				prop[jsonSchemaType === 'string' ? 'minLength' : 'minimum'] = config.min
			}
			if (config.max !== undefined) {
				prop[jsonSchemaType === 'string' ? 'maxLength' : 'maximum'] = config.max
			}

			// String format hints
			if (field._type === 'email') prop.format = 'email'
			if (field._type === 'url') prop.format = 'uri'
			if (field._type === 'date') prop.format = 'date'
			if (field._type === 'time') prop.format = 'time'
			if (field._type === 'datetime') prop.format = 'date-time'

			// Build x-ui extension object for non-standard hints
			const xUi: Record<string, any> = {}

			// Field type (useful for rendering)
			if (field._type !== 'text') xUi.fieldType = field._type

			// UI-specific hints
			if (config.placeholder) xUi.placeholder = config.placeholder
			if (config.disabled) xUi.disabled = config.disabled
			if (config.readonly) xUi.readonly = config.readonly
			if (config.autoheight) xUi.autoheight = config.autoheight
			if (config.rows) xUi.rows = config.rows
			if (config.cols) xUi.cols = config.cols

			// Layout hints
			if (field._class) xUi.class = field._class

			// Conditional visibility
			if (field._condition) xUi.condition = field._condition

			// Array field options
			if (field._type === 'array') {
				if (config.allowAdd !== undefined) xUi.allowAdd = config.allowAdd
				if (config.allowDelete !== undefined) xUi.allowDelete = config.allowDelete
			}

			// Component hint (for custom components)
			if (field._component) {
				xUi.component = typeof field._component.component === 'string'
					? field._component.component
					: field._component.component.name || 'CustomComponent'
				if (field._component.props) xUi.componentProps = field._component.props
			}

			// Only add x-ui if it has properties
			if (Object.keys(xUi).length > 0) {
				prop['x-ui'] = xUi
			}

			properties[key] = prop

			// Track required fields
			if (config.required === true) {
				requiredFields.push(key)
			}
		}

		const schema: Record<string, any> = {
			type: 'object',
			properties,
		}

		if (requiredFields.length > 0) {
			schema.required = requiredFields
		}

		// Add schema-level metadata
		if (this._label) schema.title = this._label

		// Add schema-level x-ui if needed
		const schemaXUi: Record<string, any> = {}
		if (this._condition) schemaXUi.condition = this._condition
		if (this._class) schemaXUi.class = this._class
		if (Object.keys(schemaXUi).length > 0) {
			schema['x-ui'] = schemaXUi
		}

		return schema
	}

	private static getJSONSchemaType(fieldType: string): string {
		const typeMap: Record<string, string> = {
			text: 'string',
			email: 'string',
			password: 'string',
			number: 'number',
			tel: 'string',
			url: 'string',
			date: 'string',
			time: 'string',
			datetime: 'string',
			textarea: 'string',
			richtext: 'string',
			json: 'object',
			checkbox: 'boolean',
			toggle: 'boolean',
			radio: 'string',
			select: 'string',
			multiselect: 'array',
			array: 'array',
		}
		return typeMap[fieldType] || 'string'
	}
}

// ============================================================================
// Public API
// ============================================================================

/**
 * Define a single-step form schema or a step in a multi-step form
 *
 * Supports both direct keys and nested paths (dot notation):
 * - Direct: { email: $.email() }
 * - Nested: { 'meta.bio': $.text() }
 *
 * @example
 * // Single-step form
 * const userSchema = defineSchema<User>({
 *   email: $.email({ required: true }),
 *   firstName: $.text({ required: true }),
 *   'meta.bio': $.richtext() // Nested path - type-checked!
 * })
 *
 * @example
 * // Multi-step form step (with label)
 * const step = defineSchema('Personal Info', {
 *   firstName: $.text({ required: true }),
 *   lastName: $.text({ required: true })
 * })
 */
export function defineSchema<T extends Record<string, any>>(
	labelOrFields: string | Partial<Record<NestedKeyOf<T>, FieldBuilder>>,
	maybeFields?: Partial<Record<NestedKeyOf<T>, FieldBuilder>>
): SchemaDefinition<T> {
	if (typeof labelOrFields === 'string') {
		if (!maybeFields) {
			throw new Error('Fields are required when providing a label')
		}
		return new Schema(maybeFields as Record<string, FieldBuilder>, labelOrFields)
	}
	return new Schema(labelOrFields as Record<string, FieldBuilder>)
}

/**
 * Multi-step schema type with embedded form data type
 * The __type property is a phantom type for TypeScript inference
 */
export type MultiStepSchemaDefinition<T = any> = Record<string, SchemaDefinition> & {
	/** Phantom type for form data inference (not present at runtime) */
	__type?: T
}

/**
 * Define a multi-step form schema
 *
 * @param T - The final merged form data type across all steps
 *
 * @example
 * interface OnboardingData {
 *   userType: 'individual' | 'business'
 *   firstName: string
 *   lastName: string
 *   companyName?: string
 * }
 *
 * const onboardingFlow = multiStepSchema<OnboardingData>({
 *   user_type: schema('Who are you?', {
 *     userType: $.radio(['individual', 'business'], { required: true })
 *   }),
 *   personal_info: schema('Personal Information', {
 *     firstName: $.text({ required: true }),
 *     lastName: $.text({ required: true })
 *   }),
 *   business_info: schema('Company Details', {
 *     companyName: $.text({ required: true })
 *   }).if('userType eq "business"')
 * })
 *
 * // Later, extract the type:
 * type FormData = NonNullable<typeof onboardingFlow.__type>
 */
export function multiStepSchema<T extends Record<string, any> = Record<string, any>>(
	steps: Record<string, SchemaDefinition>
): MultiStepSchemaDefinition<T> {
	return steps as MultiStepSchemaDefinition<T>
}

// ============================================================================
// JSONSchema Import
// ============================================================================

interface JSONSchemaProperty {
	'type'?: string
	'title'?: string
	'description'?: string
	'default'?: any
	'enum'?: any[]
	'format'?: string
	'minimum'?: number
	'maximum'?: number
	'minLength'?: number
	'maxLength'?: number
	'$ref'?: string
	'items'?: JSONSchemaProperty
	'anyOf'?: JSONSchemaProperty[]
	'oneOf'?: JSONSchemaProperty[]
	'allOf'?: JSONSchemaProperty[]
	'additionalProperties'?: boolean | JSONSchemaProperty
	'x-ui'?: {
		fieldType?: string
		placeholder?: string
		disabled?: boolean
		readonly?: boolean
		autoheight?: boolean
		rows?: number
		cols?: number
		class?: string
		condition?: string
		allowAdd?: boolean
		allowDelete?: boolean
		component?: string
		componentProps?: Record<string, any>
	}
}

interface JSONSchemaObject {
	'type'?: string
	'title'?: string
	'properties'?: Record<string, JSONSchemaProperty>
	'required'?: string[]
	'$defs'?: Record<string, JSONSchemaProperty>
	'definitions'?: Record<string, JSONSchemaProperty>
	'x-ui'?: {
		condition?: string
		class?: string
	}
}

/**
 * Create a schema from a JSONSchema object
 *
 * This allows you to save schemas to a database and load them later.
 *
 * @example
 * // Save to database
 * const jsonSchema = mySchema.toJSONSchema()
 * await db.schemas.insert({ name: 'user-form', schema: jsonSchema })
 *
 * // Load from database
 * const saved = await db.schemas.findOne({ name: 'user-form' })
 * const loadedSchema = fromJSONSchema(saved.schema)
 *
 * // Use in component
 * <FormFlow v-model="data" :schema="loadedSchema" />
 */
export function fromJSONSchema(jsonSchema: JSONSchemaObject): SchemaDefinition {
	const fields: Record<string, FieldBuilder> = {}
	const requiredSet = new Set(jsonSchema.required || [])

	if (jsonSchema.properties) {
		for (const [key, prop] of Object.entries(jsonSchema.properties)) {
			fields[key] = createFieldFromProperty(key, prop, requiredSet.has(key), jsonSchema)
		}
	}

	const schema = new Schema(fields, jsonSchema.title)

	// Apply schema-level x-ui properties
	if (jsonSchema['x-ui']?.condition) {
		schema.if(jsonSchema['x-ui'].condition)
	}
	if (jsonSchema['x-ui']?.class) {
		schema.class(jsonSchema['x-ui'].class)
	}

	return schema
}

/**
 * Resolve a $ref pointer to its definition
 */
function resolveRef(ref: string, rootSchema: JSONSchemaObject): JSONSchemaProperty | undefined {
	// Handle local refs like "#/$defs/WaLanguage" or "#/definitions/WaLanguage"
	const match = ref.match(/^#\/(\$defs|definitions)\/(.+)$/)
	if (!match) return undefined

	const [, defsKey, defName] = match
	const defs = defsKey === '$defs' ? rootSchema.$defs : rootSchema.definitions
	return defName ? defs?.[defName] : undefined
}

/**
 * Resolve anyOf/oneOf to a primary type (ignoring null for nullable fields)
 */
function resolveUnionType(prop: JSONSchemaProperty, _rootSchema: JSONSchemaObject): JSONSchemaProperty {
	const unionTypes = prop.anyOf || prop.oneOf
	if (!unionTypes || unionTypes.length === 0) return prop

	// Filter out null types to find the "real" type
	const nonNullTypes = unionTypes.filter(t => t.type !== 'null')

	if (nonNullTypes.length === 1) {
		// Single non-null type - merge with parent prop (preserving title, default, etc.)
		const resolved = nonNullTypes[0]
		return {
			...resolved,
			'title': prop.title || resolved.title,
			'description': prop.description || resolved.description,
			'default': prop.default !== undefined ? prop.default : resolved.default,
			'x-ui': { ...resolved['x-ui'], ...prop['x-ui'] },
		}
	}

	// Multiple non-null types - try to pick the most useful one
	// Prefer object > array > string > number > boolean
	const typeOrder = ['object', 'array', 'string', 'number', 'integer', 'boolean']
	const sorted = nonNullTypes.sort((a, b) => {
		const aIdx = typeOrder.indexOf(a.type || 'string')
		const bIdx = typeOrder.indexOf(b.type || 'string')
		return aIdx - bIdx
	})

	const resolved = sorted[0]
	return {
		...resolved,
		'title': prop.title || resolved.title,
		'description': prop.description || resolved.description,
		'default': prop.default !== undefined ? prop.default : resolved.default,
		'x-ui': { ...resolved['x-ui'], ...prop['x-ui'] },
	}
}

/**
 * Create a field builder from a JSONSchema property
 */
function createFieldFromProperty(
	key: string,
	prop: JSONSchemaProperty,
	isRequired: boolean,
	rootSchema: JSONSchemaObject
): FieldBuilder {
	// Resolve $ref if present
	let resolvedProp = prop
	if (prop.$ref) {
		const refDef = resolveRef(prop.$ref, rootSchema)
		if (refDef) {
			// Merge ref definition with property (property values take precedence)
			resolvedProp = {
				...refDef,
				'type': prop.type ?? refDef.type,
				'enum': prop.enum ?? refDef.enum,
				'title': prop.title ?? refDef.title,
				'description': prop.description ?? refDef.description,
				'default': prop.default !== undefined ? prop.default : refDef.default,
				'x-ui': { ...refDef['x-ui'], ...prop['x-ui'] },
			}
		}
	}

	// Resolve anyOf/oneOf
	if (resolvedProp.anyOf || resolvedProp.oneOf) {
		resolvedProp = resolveUnionType(resolvedProp, rootSchema)
	}

	const xUi = resolvedProp['x-ui'] || {}
	const fieldType = xUi.fieldType || inferFieldType(resolvedProp)

	// Build base config
	const config: BaseFieldConfig & Record<string, any> = {}

	if (resolvedProp.title) config.label = resolvedProp.title
	if (resolvedProp.description) config.helpText = resolvedProp.description
	if (resolvedProp.default !== undefined) config.default = resolvedProp.default
	if (isRequired) config.required = true

	// UI hints from x-ui
	if (xUi.placeholder) config.placeholder = xUi.placeholder
	if (xUi.disabled) config.disabled = xUi.disabled
	if (xUi.readonly) config.readonly = xUi.readonly
	if (xUi.autoheight) config.autoheight = xUi.autoheight
	if (xUi.rows) config.rows = xUi.rows
	if (xUi.cols) config.cols = xUi.cols

	// Numeric/string constraints
	if (resolvedProp.minimum !== undefined) config.min = resolvedProp.minimum
	if (resolvedProp.maximum !== undefined) config.max = resolvedProp.maximum
	if (resolvedProp.minLength !== undefined) config.min = resolvedProp.minLength
	if (resolvedProp.maxLength !== undefined) config.max = resolvedProp.maxLength

	// Create the appropriate field builder
	let field: FieldBuilder

	switch (fieldType) {
		case 'email':
			field = $.email(config)
			break
		case 'password':
			field = $.password(config)
			break
		case 'number':
			field = $.number(config)
			break
		case 'tel':
			field = $.tel(config)
			break
		case 'url':
			field = $.url(config)
			break
		case 'date':
			field = $.date(config)
			break
		case 'time':
			field = $.time(config)
			break
		case 'datetime':
			field = $.datetime(config)
			break
		case 'textarea':
			field = $.textarea(config)
			break
		case 'richtext':
			field = $.richtext(config)
			break
		case 'json':
			field = $.json(config)
			break
		case 'checkbox':
			field = $.checkbox(config)
			break
		case 'toggle':
			field = $.toggle(config)
			break
		case 'radio':
			field = $.radio(resolvedProp.enum || [], config)
			break
		case 'select':
			field = $.select(resolvedProp.enum || [], config)
			break
		case 'multiselect':
			field = $.multiselect(resolvedProp.enum || [], config)
			break
		case 'array': {
			const arrayConfig: Record<string, any> = { ...config }
			if (xUi.allowAdd !== undefined) arrayConfig.allowAdd = xUi.allowAdd
			if (xUi.allowDelete !== undefined) arrayConfig.allowDelete = xUi.allowDelete
			if (resolvedProp.items?.type) {
				arrayConfig.itemType = inferFieldType(resolvedProp.items)
			}
			field = $.array(resolvedProp.title || key, undefined, arrayConfig)
			break
		}
		case 'display':
			// Display fields with custom components can't be fully restored
			// without the actual component reference
			field = $.display(xUi.component || 'div', xUi.componentProps || {})
			break
		case 'component':
			// Custom components can't be fully restored without the actual component
			field = $.component(xUi.component || 'div', xUi.componentProps || {})
			break
		default:
			field = $.text(config)
	}

	// Apply class
	if (xUi.class) {
		field.class(xUi.class)
	}

	// Apply condition
	if (xUi.condition) {
		field.if(xUi.condition)
	}

	return field
}

/**
 * Infer field type from JSONSchema property
 */
function inferFieldType(prop: JSONSchemaProperty): string {
	// Check format first
	if (prop.format) {
		switch (prop.format) {
			case 'email': return 'email'
			case 'uri': return 'url'
			case 'date': return 'date'
			case 'time': return 'time'
			case 'date-time': return 'datetime'
		}
	}

	// Check type + enum
	if (prop.enum) {
		return 'select'
	}

	// Check base type
	switch (prop.type) {
		case 'boolean': return 'checkbox'
		case 'number':
		case 'integer': return 'number'
		case 'array': return 'array'
		case 'object': return 'json' // Objects rendered with CodeEditor
		default: return 'text'
	}
}

// ============================================================================
// Type Helpers
// ============================================================================

/**
 * Infer the TypeScript type from a schema definition
 */
export type InferSchemaType<S extends SchemaDefinition> = {
	[K in keyof S['_fields']]: InferFieldType<S['_fields'][K]>
}

type InferFieldType<F> = F extends FieldBuilder<infer T> ? T : any
