package webui

import (
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"kjol/wasmruntime/vdom"
)

// Port of web/kit/CellGrid.tsx — an editable, sortable spreadsheet-style grid.
//
// Row model: rows are map[string]any (the clean Go analog of the TSX's `any`
// objects with string-keyed fields, so row[col.Key], row[IDField], and conflict
// lookups map directly). Column callbacks (Render, SortValue, CellClassFn,
// OnClick, Parse) take/return that map so no type assertions are needed.
//
// NOTE: Several CellGrid behaviors are browser-only and have no equivalent in the
// neutral runtime, so they are dropped (the render output and sort/edit/conflict
// wiring are preserved):
//   - Keyboard navigation (Enter/Tab/Arrows/Escape), cell focus & the `selected`
//     signal, and focusCell/moveSelection: all require live DOM focus()/refs.
//   - Row-reorder FLIP animation (getBoundingClientRect + Element.animate).
//   - The imperative CellGridApi `ref` (focusCell/selected/snapshot/animateRows).
//     Its one pure-logic member, dirty(), is offered as the CellGridDirty helper.
//   - onFocus/onBlur/blurStamp: conflict sets are recomputed on every render from
//     the current rows instead (a signal write already re-renders), so duplicate
//     highlighting stays live without the blur hook.
//   - localeCompare is approximated by byte-wise strings.Compare.
//   - The sort caret uses "caret-up"/"caret-down"; those aren't in the default
//     icon registry, so they render as an empty box unless the app RegisterIcon's
//     them (the triangle-exclamation conflict marker is registered by default).

// GridHeaderCls is the base <th> class for CellGrid headers (exported, matching
// the TSX GRID_HEADER_CLS).
const GridHeaderCls = "border-b border-r border-line-strong bg-surface-muted px-1.5 py-1.5 text-left text-ss font-bold uppercase text-ink whitespace-nowrap last:border-r-0"

var cgLeadingDigits = regexp.MustCompile(`^\d+`)
var cgLeadingFloat = regexp.MustCompile(`^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?`)

func cellGridStr(v any) string {
	if v == nil {
		return ""
	}
	return fmt.Sprint(v)
}

func cellGridColumnSize(width, minWidth string) string {
	if width != "" {
		return width
	}
	return minWidth
}

func cellGridLeadingInt(s string) (int, bool) {
	m := cgLeadingDigits.FindString(s)
	if m == "" {
		return 0, false
	}
	n, err := strconv.Atoi(m)
	if err != nil {
		return 0, false
	}
	return n, true
}

func cellGridParseFloat(v any) float64 {
	m := cgLeadingFloat.FindString(strings.TrimSpace(cellGridStr(v)))
	if m == "" || m == "+" || m == "-" || m == "." {
		return 0
	}
	f, err := strconv.ParseFloat(m, 64)
	if err != nil {
		return 0
	}
	return f
}

// CompareRowsGeneric compares a[key] and b[key] the way the TSX comparator does:
// empties sort last, "numeric" by leading integer then string tiebreak, "money"
// by parsed float, otherwise by string. Returns <0, 0, or >0.
func CompareRowsGeneric(a, b map[string]any, key, sortType string) int {
	av := a[key]
	bv := b[key]
	aEmpty := av == nil || av == ""
	bEmpty := bv == nil || bv == ""
	if aEmpty && bEmpty {
		return 0
	}
	if aEmpty {
		return 1
	}
	if bEmpty {
		return -1
	}
	switch sortType {
	case "numeric":
		as := cellGridStr(av)
		bs := cellGridStr(bv)
		an, aok := cellGridLeadingInt(as)
		bn, bok := cellGridLeadingInt(bs)
		if aok && bok {
			if an != bn {
				if an < bn {
					return -1
				}
				return 1
			}
			return strings.Compare(as, bs)
		}
		if aok {
			return -1
		}
		if bok {
			return 1
		}
		return strings.Compare(as, bs)
	case "money":
		af := cellGridParseFloat(av)
		bf := cellGridParseFloat(bv)
		if af < bf {
			return -1
		}
		if af > bf {
			return 1
		}
		return 0
	default:
		return strings.Compare(cellGridStr(av), cellGridStr(bv))
	}
}

// CellGridColumn defines one grid column. The `cellClass: string | (row)=>string`
// TSX union becomes CellClass (static) + CellClassFn (dynamic; wins when set).
type CellGridColumn struct {
	Key         string
	Label       string
	SortKey     string
	SortType    string
	SortValue   func(row map[string]any) any
	Width       string
	MinWidth    string
	HeaderClass string
	Editable    bool
	ReadOnly    bool
	Render      func(row map[string]any) *vdom.VNode
	CellClass   string
	CellClassFn func(row map[string]any) string
	OnClick     func(row map[string]any)
	InputMode   string
	Placeholder string
	Parse       func(value string) any
}

// CellGridProps configures CellGrid. SortKey/SortDesc are the current sort state;
// SetSortKey/SetSortDesc are called by the header click handler (which toggles
// direction when the same column is re-clicked).
type CellGridProps struct {
	Columns        []CellGridColumn
	Rows           []map[string]any
	IDField        string
	OnCellChange   func(rowID any, field string, value any)
	SortKey        string
	SetSortKey     func(key string)
	SortDesc       bool
	SetSortDesc    func(desc bool)
	ConflictFields []string
	Dense          bool
}

// SortableHeaderProps configures a clickable, sort-indicating header cell.
type SortableHeaderProps struct {
	Label    string
	SortKey  string
	Width    string
	MinWidth string
	Current  string // currently active sort key ("" == none)
	Desc     bool
	OnSort   func(key string)
}

// SortableHeader renders a <th> that shows a caret when it is the active sort
// column and calls OnSort(SortKey) on click.
func SortableHeader(p SortableHeaderProps) *vdom.VNode {
	isActive := p.SortKey != "" && p.Current == p.SortKey
	cls := GridHeaderCls + " cursor-pointer select-none hover:bg-surface-strong"
	if sz := cellGridColumnSize(p.Width, p.MinWidth); sz != "" {
		cls += " " + sz
	}

	inner := []vdom.Mod{
		vdom.Attr("class", "flex items-center gap-0.5 min-w-0"),
		vdom.Span(vdom.Attr("class", "truncate min-w-0 flex-1"), vdom.Text(p.Label)),
	}
	if isActive {
		icon := "caret-up"
		if p.Desc {
			icon = "caret-down"
		}
		inner = append(inner, vdom.Span(vdom.Attr("class", "shrink-0"), Icon(icon, 10, "")))
	}

	mods := []vdom.Mod{vdom.Attr("class", cls)}
	if p.OnSort != nil {
		key := p.SortKey
		mods = append(mods, vdom.On(vdom.EVENT_CLICK, func() { p.OnSort(key) }))
	}
	mods = append(mods, vdom.Div(inner...))
	return vdom.Th(mods...)
}

func cellGridConflictSets(p CellGridProps) map[string]map[string]bool {
	result := map[string]map[string]bool{}
	if len(p.ConflictFields) == 0 {
		return result
	}
	for _, field := range p.ConflictFields {
		counts := map[string]int{}
		for _, row := range p.Rows {
			v := row[field]
			if v == nil || v == "" {
				continue
			}
			counts[cellGridStr(v)]++
		}
		set := map[string]bool{}
		for v, c := range counts {
			if c > 1 {
				set[v] = true
			}
		}
		result[field] = set
	}
	return result
}

func cellGridSortedRows(p CellGridProps) []map[string]any {
	rows := make([]map[string]any, len(p.Rows))
	copy(rows, p.Rows)
	sk := p.SortKey
	if sk == "" {
		return rows
	}

	var col *CellGridColumn
	for i := range p.Columns {
		if p.Columns[i].SortKey == sk {
			col = &p.Columns[i]
			break
		}
	}
	st := "string"
	getVal := func(r map[string]any) any { return r[sk] }
	if col != nil {
		if col.SortType != "" {
			st = col.SortType
		}
		if col.SortValue != nil {
			getVal = col.SortValue
		}
	}

	// Sort ascending stably, then reverse for desc (matches the TSX, which
	// reverses after a stable ascending sort rather than flipping ties).
	sort.SliceStable(rows, func(i, j int) bool {
		a := map[string]any{"v": getVal(rows[i])}
		b := map[string]any{"v": getVal(rows[j])}
		return CompareRowsGeneric(a, b, "v", st) < 0
	})
	if p.SortDesc {
		for i, j := 0, len(rows)-1; i < j; i, j = i+1, j-1 {
			rows[i], rows[j] = rows[j], rows[i]
		}
	}
	return rows
}

// CellGridDirty reports whether any editable field differs from initialRows (the
// pure-logic half of the dropped CellGridApi.dirty()). A length mismatch or a nil
// baseline counts as dirty.
func CellGridDirty(columns []CellGridColumn, rows, initialRows []map[string]any) bool {
	if initialRows == nil || len(rows) != len(initialRows) {
		return true
	}
	var fields []string
	for _, c := range columns {
		if c.Editable {
			fields = append(fields, c.Key)
		}
	}
	for i := range rows {
		for _, f := range fields {
			if cellGridStr(rows[i][f]) != cellGridStr(initialRows[i][f]) {
				return true
			}
		}
	}
	return false
}

// CellGrid renders the sortable/editable grid. See the file NOTE for the
// browser-only behaviors that are intentionally omitted.
func CellGrid(p CellGridProps) *vdom.VNode {
	idf := p.IDField
	if idf == "" {
		idf = "id"
	}
	rowHCls := "h-8"
	if p.Dense {
		rowHCls = "h-6"
	}
	readonlyTdCls := "border-b border-r border-line-strong bg-black/5 px-2 text-ink-soft align-middle " + rowHCls
	const editableTdCls = "border-b border-r border-line-strong p-0 relative align-middle"
	const inputCls = "absolute inset-0 w-full border-none outline-none bg-transparent px-2 placeholder:text-ink-faint focus:bg-red-50 dark:bg-red-950 focus:shadow-[inset_0_0_0_2px_var(--color-red-500)]"

	conflicts := cellGridConflictSets(p)
	isConflict := func(field string, value any) bool {
		if value == nil || value == "" {
			return false
		}
		set := conflicts[field]
		return set != nil && set[cellGridStr(value)]
	}
	fieldInConflictList := func(key string) bool {
		for _, f := range p.ConflictFields {
			if f == key {
				return true
			}
		}
		return false
	}

	handleSort := func(key string) {
		if p.SortKey == key {
			if p.SetSortDesc != nil {
				p.SetSortDesc(!p.SortDesc)
			}
			return
		}
		if p.SetSortKey != nil {
			p.SetSortKey(key)
		}
		if p.SetSortDesc != nil {
			p.SetSortDesc(false)
		}
	}

	renderHeader := func(col CellGridColumn) *vdom.VNode {
		if col.SortKey != "" {
			return SortableHeader(SortableHeaderProps{
				Label:    col.Label,
				SortKey:  col.SortKey,
				Width:    col.Width,
				MinWidth: col.MinWidth,
				Current:  p.SortKey,
				Desc:     p.SortDesc,
				OnSort:   handleSort,
			})
		}
		cls := GridHeaderCls
		if col.HeaderClass != "" {
			cls += " " + col.HeaderClass
		}
		if sz := cellGridColumnSize(col.Width, col.MinWidth); sz != "" {
			cls += " " + sz
		}
		return vdom.Th(vdom.Attr("class", cls), vdom.Text(col.Label))
	}

	renderCell := func(row map[string]any, col CellGridColumn) *vdom.VNode {
		rowID := row[idf]
		sizeCls := ""
		if sz := cellGridColumnSize(col.Width, col.MinWidth); sz != "" {
			sizeCls = " " + sz
		}
		cellClass := func() string {
			if col.CellClassFn != nil {
				return col.CellClassFn(row) + sizeCls
			}
			base := col.CellClass
			if base == "" {
				base = readonlyTdCls
			}
			return base + sizeCls
		}

		// Custom-rendered, non-editable cell.
		if col.Render != nil && !col.Editable {
			mods := []vdom.Mod{vdom.Attr("class", cellClass())}
			if col.OnClick != nil {
				mods = append(mods, vdom.On(vdom.EVENT_CLICK, func() { col.OnClick(row) }))
			}
			mods = append(mods, col.Render(row))
			return vdom.Td(mods...)
		}

		// Read-only cell.
		if col.ReadOnly {
			mods := []vdom.Mod{vdom.Attr("class", cellClass())}
			if col.OnClick != nil {
				mods = append(mods, vdom.On(vdom.EVENT_CLICK, func() { col.OnClick(row) }))
			}
			if col.Render != nil {
				mods = append(mods, col.Render(row))
			} else {
				mods = append(mods, vdom.Text(cellGridStr(row[col.Key])))
			}
			return vdom.Td(mods...)
		}

		// Editable cell.
		inList := fieldInConflictList(col.Key)
		hasConflict := isConflict(col.Key, row[col.Key])
		tdCls := editableTdCls + sizeCls
		if inList {
			tdCls += " relative"
			if hasConflict {
				tdCls += " bg-amber-100 dark:bg-amber-900"
			}
		}
		im := col.InputMode
		if im == "" {
			im = "text"
		}
		input := vdom.Input(vdom.Attr("class", inputCls),
			vdom.Attr("type", "text"),
			vdom.Attr("inputmode", im),
			vdom.Attr("placeholder", col.Placeholder),
			vdom.Prop("value", cellGridStr(row[col.Key])),
			vdom.OnEvent(vdom.EVENT_INPUT, func(e vdom.Event) {
				var v any = e.Value()
				if col.Parse != nil {
					v = col.Parse(e.Value())
				}
				if p.OnCellChange != nil {
					p.OnCellChange(rowID, col.Key, v)
				}
			}),
		)
		mods := []vdom.Mod{vdom.Attr("class", tdCls), input}
		if inList && hasConflict {
			mods = append(mods, vdom.Span(vdom.Attr("class", "pointer-events-none absolute right-0.5 top-1/2 -translate-y-1/2 text-amber-600 dark:text-amber-400"),
				vdom.Attr("title", "Duplicate value"),
				Icon("triangle-exclamation", 12, ""),
			))
		}
		return vdom.Td(mods...)
	}

	var headerCells []*vdom.VNode
	for _, col := range p.Columns {
		headerCells = append(headerCells, renderHeader(col))
	}
	thead := vdom.Thead(vdom.Tr(kids(nil, headerCells)...))

	var bodyRows []*vdom.VNode
	for _, row := range cellGridSortedRows(p) {
		var cells []*vdom.VNode
		for _, col := range p.Columns {
			cells = append(cells, renderCell(row, col))
		}
		bodyRows = append(bodyRows, vdom.Tr(kids([]vdom.Mod{vdom.Attr("class", "odd:bg-surface even:bg-surface-raised")}, cells)...))
	}
	tbody := vdom.Tbody(kids(nil, bodyRows)...)

	tableCls := "min-w-full w-max border-collapse text-sm"
	if p.Dense {
		tableCls = "min-w-full w-max border-collapse text-ss"
	}
	return vdom.Div(vdom.Attr("class", "relative max-w-full overflow-x-auto border border-line-strong rounded-default bg-surface tabular-nums"),
		vdom.Table(vdom.Attr("class", tableCls), thead, tbody),
	)
}
