import type { BglFormSchemaT, Field, SchemaField } from '@bagelink/vue'
import type { DefaultPathsOptions } from 'type-fest/source/paths'
import type { MaybeRefOrGetter } from 'vue'
import type { SortDirectionsT } from '../../types/TableSchema'
import { useBglSchema, isDate, keyToLabel, formatDate } from '@bagelink/vue'
import { computed, ref, watch, toValue } from 'vue'

const NON_DIGIT_REGEX = /[^\d.-]/g

// Components that should receive their value as a child/slot instead of a prop
const SLOT_VALUE_COMPONENTS = new Set(['div', 'span', 'p'])

// Components that should receive their value as src attribute
const SRC_VALUE_COMPONENTS = new Set(['img', 'iframe'])

// Extend the base options interface to include computed refs
export interface UseTableDataOptions<T> {
	data: MaybeRefOrGetter<T[]>
	schema?: MaybeRefOrGetter<BglFormSchemaT<T> | undefined>
	columns?: MaybeRefOrGetter<string[] | undefined>
	useServerSort?: MaybeRefOrGetter<boolean | undefined>
	onSort?: (field: string, direction: SortDirectionsT) => void
}

interface TransformedDataBase {
	[key: `_transformed_${string}`]: any
	[key: `_slot_${string}`]: boolean
	[key: `_src_${string}`]: boolean
	[key: `_original_${string}`]: any
}

type TransformedData<TransDataT> = TransDataT & TransformedDataBase

function autoTransform<T>(field: Field<T, DefaultPathsOptions>): Field<T, DefaultPathsOptions> {
	if ((field.id === 'created_at' || field.id === 'updated_at') && !field.transform) {
		field.transform = (val?: any) => val ? formatDate(val) : val
	}
	return field
}

export function useTableData<T extends { [key: string]: any }>(options: UseTableDataOptions<T>) {
	// Sorting state
	const sortField = ref('')
	const sortDirection = ref<SortDirectionsT>('ASC')
	// Schema loading state
	const schemaState = ref<'loading' | 'loaded' | 'error'>('loading')
	const resolvedSchema = ref<any[]>([])

	// Function to resolve schema
	function resolveSchema() {
		try {
			schemaState.value = 'loading'

			// Get the data safely
			const dataValue = toValue(options.data) || []

			// Get the schema from useBglSchema
			const schema = useBglSchema<T>({
				schema: toValue(options.schema),
				columns: toValue(options.columns),
				data: dataValue,
			})

			// If we have a valid schema with fields, filter out fields without an ID
			if (Array.isArray(schema) && schema.length > 0) {
				resolvedSchema.value = (schema as SchemaField<T>[])
					.filter(field => field)
					.map(field => field as Field<T>)
					.map(autoTransform)
			} else if (Array.isArray(dataValue) && dataValue.length > 0) {
				// If no schema is provided or it's empty, generate a default schema from the data
				const firstItem = dataValue[0]

				// Create a schema based on the keys of the first item
				resolvedSchema.value = Object.keys(firstItem || {})
					.filter(key => key !== 'id' && !key.startsWith('_')) // Exclude id and internal fields
					.map(key => ({
						id: key as any, // Cast to any to resolve type issues with Path<T>
						label: keyToLabel(key),
						$el: 'div',
						transform: (val?: any) => {
							// Handle date fields
							const dateFields = ['created_at', 'updated_at']
							if (dateFields.includes(key)) { return val ? new Date(val).toLocaleString() : val }
							return val
						}
					}))
			} else {
				// Return an empty array if no data or schema
				resolvedSchema.value = []
			}

			schemaState.value = 'loaded'
		} catch (error) {
			console.error('Error resolving schema:', error)
			schemaState.value = 'error'
			resolvedSchema.value = []
		}
	}

	// Watch for changes in schema or columns and resolve schema
	watch([
		() => toValue(options.schema),
		() => toValue(options.columns),
		options.data,
	], () => {
		resolveSchema()
	}, { immediate: true })

	// Create a computed property for the schema
	const computedSchema = computed(() => resolvedSchema.value)

	function transform(rowData: T): TransformedData<T> {
		// TODO: only use type casting in the return statement
		// TODO: replace assignments with Object.assign(transformed, {[key]: value})

		const transformed = { ...rowData } as TransformedData<T>
		const schemaFields = computedSchema.value.filter((f: any) => f.id)

		for (const field of schemaFields) {
			const fieldId = field.id as keyof T
			const fieldData = rowData[fieldId]
			const transformKey = `_transformed_${String(fieldId)}` as keyof TransformedDataBase
			const slotKey = `_slot_${String(fieldId)}` as keyof TransformedDataBase
			const srcKey = `_src_${String(fieldId)}` as keyof TransformedDataBase
			const originalKey = `_original_${String(fieldId)}` as keyof TransformedDataBase

			// Store the original value
			;(transformed as TransformedDataBase)[originalKey] = fieldData

			// Determine if this component should receive value as slot or src
			const isSlotValueComponent = typeof field.$el === 'string' && SLOT_VALUE_COMPONENTS.has(field.$el)
			const isSrcValueComponent = typeof field.$el === 'string' && SRC_VALUE_COMPONENTS.has(field.$el)

			;(transformed as TransformedDataBase)[slotKey] = isSlotValueComponent
			;(transformed as TransformedDataBase)[srcKey] = isSrcValueComponent

			if (field.transform) {
				const newFieldVal = field.transform(fieldData, rowData)
				// Store transformed value in _transformed_ key but keep original in the main field
				;(transformed as TransformedDataBase)[transformKey] = newFieldVal
			} else {
				;(transformed as TransformedDataBase)[transformKey] = fieldData
			}
		}
		return transformed
	}

	// Helper function to clean up transformed data by removing all added properties
	function cleanTransformedData<T extends { [key: string]: any }>(data: TransformedData<T>): T {
		const cleanData = { ...data } as T

		// Remove all keys that start with underscore (these are added by the transform function)
		Object.keys(cleanData).forEach((key) => {
			if (key.startsWith('_')) {
				delete cleanData[key]
			}
		})

		return cleanData
	}

	const computedSortField = computed(() => sortField.value ? `_transformed_${sortField.value}` : '')

	const computedData = computed(() => {
		// Get the data safely
		const currentData = toValue(options.data) || []

		// If there's no data, return an empty array
		if (!Array.isArray(currentData) || currentData.length === 0) {
			return []
		}

		if (!sortField.value || toValue(options.useServerSort) === true) {
			return currentData.map(transform)
		}

		return currentData
			.map(transform)
			.sort((a, z) => {
				const aValue = (a as any)[computedSortField.value] ?? ''
				const bValue = (z as any)[computedSortField.value] ?? ''

				if (isDate(aValue) && isDate(bValue)) {
					return sortDirection.value === 'ASC'
						? new Date(aValue).getTime() - new Date(bValue).getTime()
						: new Date(bValue).getTime() - new Date(aValue).getTime()
				}

				const numAValue = Number.parseInt(`${aValue}`.replaceAll(NON_DIGIT_REGEX, ''), 10)
				const numBValue = Number.parseInt(`${bValue}`.replaceAll(NON_DIGIT_REGEX, ''), 10)

				if (!Number.isNaN(numAValue) && !Number.isNaN(numBValue)) {
					return sortDirection.value === 'ASC' ? numAValue - numBValue : numBValue - numAValue
				}

				if (typeof aValue === 'string') {
					return sortDirection.value === 'ASC' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue)
				}

				return sortDirection.value === 'ASC' ? (aValue < bValue ? -1 : 1) : (aValue < bValue ? 1 : -1)
			})
	})

	function toggleSort(fieldname: string) {
		if (sortField.value === fieldname) {
			if (sortDirection.value === 'ASC') {
				sortDirection.value = 'DESC'
			} else {
				sortField.value = ''
			}
		} else {
			sortField.value = fieldname
			sortDirection.value = 'ASC'
		}
		options.onSort?.(sortField.value, sortDirection.value)
	}

	return {
		computedSchema,
		computedData,
		transform,
		sortField: computed(() => sortField.value),
		sortDirection: computed(() => sortDirection.value),
		toggleSort,
		cleanTransformedData,
		schemaState: computed(() => schemaState.value),
	}
}
