<script lang="ts" setup>
import { Btn, CheckInput, TextInput, ToggleInput, localRef, ListItem, Dropdown } from '@bagelink/vue'
import { computed, ref, watch, nextTick, onUnmounted } from 'vue'
import SpreadsheetTable from './SpreadsheetTable.vue'

// Define column configuration types
type ColumnFormat = 'text' | 'number' | 'currency' | 'date' | 'percentage' | 'image' | 'boolean'

interface ColumnConfig {
	key: string
	label?: string
	locked?: boolean
	format?: ColumnFormat
	sortable?: boolean
	width?: string
	fixed?: boolean
	hidden?: boolean
	defaultValue?: any
}

// Define props interface with column configuration
interface Props {
	modelValue: Array<{ [key: string]: any }>
	columnConfig?: ColumnConfig[]
	label?: string
	allowAddRow?: boolean
}

const { modelValue, columnConfig, label, allowAddRow = true } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])

// Helper function to flatten an object with dot notation
function flattenObject(obj: { [key: string]: any }, prefix = ''): { [key: string]: any } {
	const flattened: { [key: string]: any } = {}

	for (const key in obj) {
		if (Object.hasOwn(obj, key)) {
			const value = obj[key]
			const newKey = prefix ? `${prefix}.${key}` : key

			if (value && typeof value === 'object' && !Array.isArray(value)) {
				Object.assign(flattened, flattenObject(value, newKey))
			} else {
				flattened[newKey] = value
			}
		}
	}

	return flattened
}

// Helper function to unflatten an object from dot notation
function unflattenObject(obj: { [key: string]: any }): { [key: string]: any } {
	const result: { [key: string]: any } = {}

	for (const key in obj) {
		if (Object.hasOwn(obj, key)) {
			const keys = key.split('.')
			let current = result

			for (let i = 0; i < keys.length - 1; i++) {
				const k = keys[i]
				if (!current[k]) {
					current[k] = {}
				}
				current = current[k]
			}

			current[keys[keys.length - 1]] = obj[key]
		}
	}

	return result
}

// Create local copy of the modelValue with flattened objects
const localRows = ref<Array<{ [key: string]: any }>>(
	modelValue
		? modelValue.map(row => flattenObject({ ...row }))
		: []
)

// Update the watch handler to handle flattening
watch(() => modelValue, (newVal) => {
	localRows.value = newVal
		? newVal.map(row => flattenObject({ ...row }))
		: []
})

// Update the emit to unflatten the data
function emitUpdate() {
	emit('update:modelValue', localRows.value.map(row => unflattenObject({ ...row })))
}

// After other ref declarations but before the columns computed property
const visibleColumns = ref<string[]>([])

// Sort state
const sortColumn = ref<string | null>(null)
const sortDirection = ref<'asc' | 'desc'>('asc')

// Update the columns computed property
const columns = computed(() => {
	// Get all unique keys from the data
	const dataKeys = localRows.value.length
		? new Set(localRows.value.flatMap(row => Object.keys(row)))
		: new Set<string>()

	// Create a map of configured columns
	const configMap = new Map(
		(columnConfig || []).map(config => [config.key, config])
	)

	// Merge configured columns with data keys
	const allKeys = new Set([...dataKeys, ...(columnConfig?.map(c => c.key) || [])])

	return Array.from(allKeys).map((key) => {
		const configuredColumn = configMap.get(key)
		return {
			key,
			label: configuredColumn?.label ?? key,
			locked: configuredColumn?.locked ?? false,
			sortable: configuredColumn?.sortable ?? true,
			format: configuredColumn?.format ?? 'text',
			width: configuredColumn?.width,
			fixed: configuredColumn?.fixed ?? false,
			hidden: configuredColumn?.hidden ?? false,
			defaultValue: configuredColumn?.defaultValue
		} as ColumnConfig
	})
})

// Format cell value based on column configuration
function formatCellValue(value: any, format?: ColumnFormat): string {
	if (value === null || value === undefined) return ''

	switch (format) {
		case 'image':
			return String(value) // Return raw value for images
		case 'boolean':
			return '' // Return empty string since we're using CheckInput component
		case 'number':
			return Number(value).toLocaleString()
		case 'currency':
			return Number(value).toLocaleString(undefined, {
				style: 'currency',
				currency: 'USD'
			})
		case 'date':
			return value ? new Date(value).toLocaleDateString() : ''
		case 'percentage':
			return Number(value).toLocaleString(undefined, {
				style: 'percent',
				minimumFractionDigits: 2
			})
		default:
			return String(value)
	}
}

// Update the parseValueForFormat function to handle type conversions properly
function parseValueForFormat(value: string | boolean, format?: ColumnFormat): any {
	switch (format) {
		case 'boolean':
			return typeof value === 'boolean' ? value : value === 'true'
		case 'number':
		case 'percentage':
		case 'currency':
			if (typeof value === 'boolean') return null
			return value === '' ? null : Number(String(value).replace(/[^0-9.-]/g, ''))
		case 'date':
			if (typeof value === 'boolean') return null
			return value === '' ? null : new Date(String(value)).toISOString()
		default:
			return String(value)
	}
}

// Check if a cell is editable
function isCellEditable(columnKey: string): boolean {
	const column = columns.value.find(col => col.key === columnKey)
	return !(column?.locked ?? false)
}

// Update the sortByColumn function to preserve column visibility
function sortByColumn(columnKey: string) {
	const column = columns.value.find(col => col.key === columnKey)
	if (!column?.sortable) return

	if (sortColumn.value === columnKey) {
		sortDirection.value = sortDirection.value === 'desc' ? 'asc' : 'desc'
	} else {
		sortColumn.value = columnKey
		sortDirection.value = 'desc'
	}

	const sorted = [...localRows.value].sort((a, b) => {
		const aVal = a[columnKey]
		const bVal = b[columnKey]

		if (aVal === bVal) return 0
		if (aVal === null || aVal === undefined) return 1
		if (bVal === null || bVal === undefined) return -1

		const modifier = sortDirection.value === 'desc' ? 1 : -1
		return aVal < bVal ? -modifier : modifier
	})

	// Save the current visibleColumns state
	const currentVisibleColumns = [...visibleColumns.value]

	localRows.value = sorted
	emitUpdate()

	// Restore the visibleColumns state after the update
	nextTick(() => {
		visibleColumns.value = currentVisibleColumns
	})
}

// Variables to handle cell selection
interface CellPosition {
	row: number
	col: number
}
const isSelecting = ref(false)
const selectionStart = ref<CellPosition | null>(null)
const selectionEnd = ref<CellPosition | null>(null)
const wrapText = localRef('wrapText', false)

// Reactive variable to track the currently editing cell
const editingCell = ref<CellPosition | null>(null)

// Update the editInputRef to use a Map
const editInputRef = ref(new Map<string, HTMLInputElement>())

// Update the ref handling functions
function setInputRef(el: any, key: string) {
	if (el?.$el instanceof HTMLInputElement) {
		editInputRef.value.set(key, el.$el)
	} else if (el instanceof HTMLInputElement) {
		editInputRef.value.set(key, el)
	} else {
		editInputRef.value.delete(key)
	}
}

// // Determines if the given cell is within the currently selected range
// function isCellSelected(row: number, col: number): boolean {
// 	if (!selectionStart.value || !selectionEnd.value) return false
// 	const startRow = Math.min(selectionStart.value.row, selectionEnd.value.row)
// 	const endRow = Math.max(selectionStart.value.row, selectionEnd.value.row)
// 	const startCol = Math.min(selectionStart.value.col, selectionEnd.value.col)
// 	const endCol = Math.max(selectionStart.value.col, selectionEnd.value.col)
// 	return row >= startRow && row <= endRow && col >= startCol && col <= endCol
// }

// Mouse event handlers to manage the selection range
function handleMouseDown(row: number, col: number) {
	selectionStart.value = { row, col }
	selectionEnd.value = { row, col }
	isSelecting.value = true
}

function handleMouseOver(row: number, col: number) {
	if (isSelecting.value && selectionStart.value) {
		selectionEnd.value = { row, col }
	}
}

function handleMouseUp() {
	isSelecting.value = false
}

// Add types and state for undo/redo functionality
interface SpreadsheetChange {
	type: 'cell' | 'row' | 'paste'
	data: {
		rows: Array<{ [key: string]: any }>
		selection?: { start: CellPosition, end: CellPosition }
	}
}

const undoStack = ref<SpreadsheetChange[]>([])
const redoStack = ref<SpreadsheetChange[]>([])

// Function to save state before making changes
function saveState(type: SpreadsheetChange['type']) {
	undoStack.value.push({
		type,
		data: {
			rows: JSON.parse(JSON.stringify(localRows.value)),
			selection: selectionStart.value && selectionEnd.value
				? {
					start: { ...selectionStart.value },
					end: { ...selectionEnd.value }
				}
				: undefined
		}
	})
	// Clear redo stack when new changes are made
	redoStack.value = []
}

// Undo/Redo functions
function undo() {
	const change = undoStack.value.pop()
	if (change) {
		// Save current state to redo stack
		redoStack.value.push({
			type: change.type,
			data: {
				rows: JSON.parse(JSON.stringify(localRows.value)),
				selection: selectionStart.value && selectionEnd.value
					? {
						start: { ...selectionStart.value },
						end: { ...selectionEnd.value }
					}
					: undefined
			}
		})

		// Restore previous state
		localRows.value = JSON.parse(JSON.stringify(change.data.rows))
		if (change.data.selection) {
			selectionStart.value = { ...change.data.selection.start }
			selectionEnd.value = { ...change.data.selection.end }
		}
		emitUpdate()
	}
}

function redo() {
	const change = redoStack.value.pop()
	if (change) {
		// Save current state to undo stack
		undoStack.value.push({
			type: change.type,
			data: {
				rows: JSON.parse(JSON.stringify(localRows.value)),
				selection: selectionStart.value && selectionEnd.value
					? {
						start: { ...selectionStart.value },
						end: { ...selectionEnd.value }
					}
					: undefined
			}
		})

		// Restore next state
		localRows.value = JSON.parse(JSON.stringify(change.data.rows))
		if (change.data.selection) {
			selectionStart.value = { ...change.data.selection.start }
			selectionEnd.value = { ...change.data.selection.end }
		}
		emitUpdate()
	}
}

// Update updateCell to use undo stack
function updateCell(rowIndex: number, key: string, newValue: string | boolean) {
	const column = columns.value.find(col => col.key === key)
	if (column?.locked) return

	saveState('cell')
	const parsedValue = parseValueForFormat(newValue, column?.format)
	// If the parsed value is null/undefined, use the default value
	localRows.value[rowIndex][key] = parsedValue ?? column?.defaultValue ?? (column?.format === 'boolean' ? false : '')
	emitUpdate()
}

// After other ref declarations but before computed properties
// const visibleColumns = ref<string[]>([])

// After the columns computed property
// Initialize visibleColumns with all columns when columns change
watch(() => columns.value, (newColumns) => {
	if (newColumns.length > 0 && visibleColumns.value.length === 0) {
		visibleColumns.value = newColumns.filter(col => !col.hidden).map(col => col.key)
	}
})

// Update the fixedColumns computed property
const fixedColumns = computed(() => {
	return columns.value.filter(col => col.fixed && !col.hidden && visibleColumns.value.includes(col.key))
})

// Update the scrollableColumns computed property
const scrollableColumns = computed(() => {
	return columns.value.filter(col => !col.fixed && !col.hidden && visibleColumns.value.includes(col.key))
})

// Update createEmptyRow to use defaultValue from column config
function createEmptyRow(): { [key: string]: any } {
	const newRow: { [key: string]: any } = {}
	columns.value.forEach((col) => {
		// Use defaultValue if provided, otherwise use format-specific defaults
		if (col.defaultValue !== undefined) {
			newRow[col.key] = col.defaultValue
		} else {
			newRow[col.key] = col.format === 'boolean' ? false : ''
		}
	})
	return newRow
}

// Update addRow to use the createEmptyRow function
function addRow() {
	if (columns.value.length) {
		saveState('row')
		localRows.value.push(createEmptyRow())
		emitUpdate()
	}
}

// Copy function using Navigator Clipboard API
async function copySelection() {
	if (!selectionStart.value || !selectionEnd.value) return

	const startRow = Math.min(selectionStart.value.row, selectionEnd.value.row)
	const endRow = Math.max(selectionStart.value.row, selectionEnd.value.row)
	const startCol = Math.min(selectionStart.value.col, selectionEnd.value.col)
	const endCol = Math.max(selectionStart.value.col, selectionEnd.value.col)

	const selectedData = []
	for (let i = startRow; i <= endRow; i++) {
		const rowData = []
		for (let j = startCol; j <= endCol; j++) {
			const columnKey = columns.value[j].key
			const value = localRows.value[i][columnKey]
			rowData.push(formatCellValue(value, columns.value[j].format))
		}
		selectedData.push(rowData)
	}

	// Convert to TSV format for clipboard
	const tsvContent = selectedData.map(row => row.join('\t')).join('\n')

	try {
		await navigator.clipboard.writeText(tsvContent)
	} catch (err) {
		console.error('Failed to copy to clipboard:', err)
	}
}

// Add a ref for the search input
const searchInputRef = ref<any>(null)

// Add a method to check if the search input is focused
function isSearchFocused(): boolean {
	const inputElement = searchInputRef.value?.$el?.querySelector('input')
	return document.activeElement === inputElement
}

// Update the pasteSelection function to handle search focus
async function pasteSelection() {
	if (!selectionStart.value) return

	try {
		const clipboardText = await navigator.clipboard.readText()
		const rows = clipboardText.split('\n').map(row => row.split('\t'))

		saveState('paste')

		const startRow = selectionStart.value.row
		const startCol = selectionStart.value.col

		// Calculate how many new rows we need to add
		const neededRows = startRow + rows.length - localRows.value.length
		if (neededRows > 0) {
			// Add the required number of new rows
			for (let i = 0; i < neededRows; i++) {
				localRows.value.push(createEmptyRow())
			}
		}

		// Update the data
		rows.forEach((rowData, rowIndex) => {
			const targetRow = startRow + rowIndex
			rowData.forEach((cellValue, colIndex) => {
				const targetCol = startCol + colIndex
				if (targetCol >= columns.value.length) return

				const columnKey = columns.value[targetCol].key
				if (!isCellEditable(columnKey)) return

				const { format } = columns.value[targetCol]
				localRows.value[targetRow][columnKey] = parseValueForFormat(cellValue, format)
			})
		})

		emitUpdate()
	} catch (err) {
		console.error('Failed to paste from clipboard:', err)
	}
}

// Add a variable to track the original value before editing
const editingOriginalValue = ref<string | null>(null)

// Update the startEditing function to handle focus properly
function startEditing(row: number, col: number, initialKey?: string) {
	const columnKey = columns.value[col]?.key
	if (!columnKey) return

	// Only check editability when we're actually going to edit
	if (initialKey !== undefined && !isCellEditable(columnKey)) return

	editingCell.value = { row, col }
	editingOriginalValue.value = localRows.value[row][columnKey]?.toString() ?? ''

	if (initialKey !== undefined) {
		updateCell(row, columnKey, initialKey)
	}

	// Focus the input on the next tick after Vue has updated the DOM
	nextTick(() => {
		const inputKey = `cell-${row}-${col}`
		const input = editInputRef.value.get(inputKey)
		if (input) {
			input.focus()
			// If we have an initial key, set the selection to the end
			if (initialKey !== undefined) {
				input.value = initialKey
			}
		}
	})
}

// Update the stopEditing function to handle cancellation
function stopEditing(cancelled = false) {
	if (cancelled && editingCell.value && editingOriginalValue.value !== null) {
		const { row, col } = editingCell.value
		const columnKey = columns.value[col]?.key
		if (columnKey) {
			localRows.value[row][columnKey] = editingOriginalValue.value
		}
	}
	editingCell.value = null
	editingOriginalValue.value = null
}

// Update the handleStopEditingAndBlur function
function handleStopEditingAndBlur(cancelled = false) {
	nextTick(() => {
		stopEditing(cancelled)
		nextTick(() => {
			// Force focus back to the main spreadsheet container
			const spreadsheet = document.querySelector('.spreadsheet') as HTMLElement
			if (spreadsheet) {
				spreadsheet.focus()
			}
		})
	})
}

window.addEventListener('mouseup', handleMouseUp)

// First add a function to handle row selection
function selectEntireRow(rowIndex: number) {
	selectionStart.value = { row: rowIndex, col: 0 }
	selectionEnd.value = { row: rowIndex, col: columns.value.length - 1 }
}

// Add back the handleCellKeyDown function
function handleCellKeyDown(event: KeyboardEvent, row: number, col: number) {
	// If this cell is not already in edit mode
	if (!(editingCell.value && editingCell.value.row === row && editingCell.value.col === col)) {
		// Start editing if a printable character or Enter is pressed
		if ((event.key.length === 1 && !event.ctrlKey && !event.metaKey) || event.key === 'Enter') {
			event.preventDefault()
			startEditing(row, col, event.key.length === 1 ? event.key : undefined)
		}
	}
}

// Update keyboard shortcuts to include undo/redo
function handleSpreadsheetKeyDown(event: KeyboardEvent) {
	// Don't intercept keyboard shortcuts when editing a cell
	if (editingCell.value || isSearchFocused()) return
	console.log('handleSpreadsheetKeyDown', event)
	const isCtrlOrCmd = event.ctrlKey || event.metaKey

	if (isCtrlOrCmd) {
		switch (event.key.toLowerCase()) {
			case 'c':
				event.preventDefault()
				copySelection()
				break
			case 'v':
				event.preventDefault()
				pasteSelection()
				break
			case 'z':
				event.preventDefault()
				if (event.shiftKey) {
					redo()
				} else {
					undo()
				}
				break
			case 'y':
				event.preventDefault()
				redo()
				break
		}
	}
}

// Add computed properties for undo/redo stack state
const canUndo = computed(() => undoStack.value.length > 0)
const canRedo = computed(() => redoStack.value.length > 0)

// Add after other ref declarations
const search = ref('')

// Add the filteredRows computed property after the columns computed
const filteredRows = computed(() => {
	if (!search.value) return localRows.value

	const searchTerm = search.value.toLowerCase()
	return localRows.value.filter((row) => {
		// Check all values in the row, including hidden columns
		return Object.values(row).some((value) => {
			if (value === null || value === undefined) return false
			return String(value).toLowerCase().includes(searchTerm)
		})
	})
})

// Add after other ref declarations
const columnWidths = ref<Map<string, number>>(new Map())
const isResizing = ref(false)
const resizingColumn = ref<string | null>(null)
const startX = ref<number>(0)
const startWidth = ref<number>(0)

// Add after other function declarations
function handleResizeStart(e: MouseEvent, columnKey: string) {
	isResizing.value = true
	resizingColumn.value = columnKey
	startX.value = e.pageX

	// Find the column header element
	const columnHeader = (e.target as HTMLElement).closest('th')
	if (!columnHeader) return

	// Get the actual computed width of the column
	const computedWidth = columnHeader.getBoundingClientRect().width
	const currentWidth = columnWidths.value.get(columnKey) || computedWidth
	startWidth.value = currentWidth

	// Add event listeners for mousemove and mouseup
	document.addEventListener('mousemove', handleResizeMove)
	document.addEventListener('mouseup', handleResizeEnd)
}

function handleResizeMove(e: MouseEvent) {
	if (!isResizing.value || !resizingColumn.value) return

	e.preventDefault()

	const diff = e.pageX - startX.value
	const newWidth = Math.max(80, startWidth.value + diff) // Keep minimum width of 80px

	columnWidths.value.set(resizingColumn.value, newWidth)
}

function handleResizeEnd() {
	isResizing.value = false
	resizingColumn.value = null

	// Remove event listeners
	document.removeEventListener('mousemove', handleResizeMove)
	document.removeEventListener('mouseup', handleResizeEnd)
}

// Clean up event listeners when component is unmounted
onUnmounted(() => {
	document.removeEventListener('mousemove', handleResizeMove)
	document.removeEventListener('mouseup', handleResizeEnd)
})

// Add a computed property for visible column options
const columnOptions = computed(() => {
	return columns.value.filter(col => !col.hidden)
})
</script>

<template>
	<div class="w-100p" tabindex="-1" @keydown="handleSpreadsheetKeyDown">
		<div class="flex gap-05 py-05 justify-content-end m_flex-wrap">
			<label v-if="label" class="label me-auto">{{ label }}</label>
			<div class="flex gap-075">
				<TextInput ref="searchInputRef" v-model="search" icon="search" placeholder="Search" class="m-0 max-w200px" />
				<Dropdown flat thin icon="more_vert">
					<ListItem title="Paste" icon="paste" @click="pasteSelection" />
					<ListItem title="copy" icon="copy" @click="copySelection" />
					<ListItem title="Undo" icon="undo" :disabled="!canUndo" @click="undo" />
					<ListItem title="Redo" icon="redo" :disabled="!canRedo" @click="redo" />
					<ToggleInput v-model="wrapText" label="Wrap Text" />
					<Dropdown placement="auto-end">
						<template #trigger="{ show }">
							<ListItem title="Column Visibility" icon="view_column" @click="show()" />
						</template>
						<div class="p-05">
							<div class="flex space-between">
								<Btn flat thin small value="Select All" @click="visibleColumns = columnOptions.map(col => col.key)" />
								<Btn flat thin small value="Clear All" @click="visibleColumns = []" />
							</div>
							<CheckInput
								v-for="col in columnOptions"
								:key="col.key"
								v-model="visibleColumns"
								:value="col.key"
								:label="col.label || col.key"
								@update:modelValue="val => undefined"
							/>
						</div>
					</Dropdown>
				</Dropdown>
			</div>
		</div>
		<div class="spreadsheet" @mouseup="handleMouseUp">
			<div class="flex w-100p relative">
				<!-- Fixed Columns -->
				<SpreadsheetTable
					v-if="fixedColumns.length"
					:columns="fixedColumns"
					:rows="filteredRows"
					:is-fixed="true"
					:show-row-numbers="true"
					:column-widths="columnWidths"
					:sort-column="sortColumn"
					:sort-direction="sortDirection"
					:selection-start="selectionStart"
					:selection-end="selectionEnd"
					:editing-cell="editingCell"
					:base-column-index="0"
					:wrap-text="wrapText"
					class="sticky z-2 start-0 bg-white"
					@sortColumn="sortByColumn"
					@resizeStart="handleResizeStart"
					@selectRow="selectEntireRow"
					@cellMouseDown="handleMouseDown"
					@cellMouseOver="handleMouseOver"
					@cellEditStart="startEditing"
					@cellKeyDown="handleCellKeyDown"
					@updateCell="updateCell"
					@stopEditing="handleStopEditingAndBlur"
					@setInputRef="setInputRef"
				/>

				<!-- Scrollable Columns -->
				<div class="flex-shrink flex-grow">
					<SpreadsheetTable
						:columns="scrollableColumns"
						:rows="filteredRows"
						:is-fixed="false"
						:show-row-numbers="!fixedColumns.length"
						:column-widths="columnWidths"
						:sort-column="sortColumn"
						:sort-direction="sortDirection"
						:selection-start="selectionStart"
						:selection-end="selectionEnd"
						:editing-cell="editingCell"
						:base-column-index="fixedColumns.length"
						:wrap-text="wrapText"
						@sortColumn="sortByColumn"
						@resizeStart="handleResizeStart"
						@selectRow="selectEntireRow"
						@cellMouseDown="handleMouseDown"
						@cellMouseOver="handleMouseOver"
						@cellEditStart="startEditing"
						@cellKeyDown="handleCellKeyDown"
						@updateCell="updateCell"
						@stopEditing="handleStopEditingAndBlur"
						@setInputRef="setInputRef"
					/>
				</div>
			</div>
			<Btn v-if="allowAddRow" outline thin round icon="add" value="Add Row" class="mt-05" @click="addRow" />
		</div>
	</div>
</template>

<style>
/* Spreadsheet container styles */
.spreadsheet {
	user-select: none;
	outline: 1px solid var(--border-color);
}

body>div:has(.spreadsheet) ::-webkit-scrollbar-track {
	background: var(--input-bg) !important;
}
</style>
