package webui

import (
	"sort"
	"strconv"
	"strings"
	"unicode"

	"kjol/wasmruntime/vdom"
)

// Port of web/kit/FuzzyMatch.tsx.
//
// The fuzzy-scoring/matching logic (Forrest Smith's fts_fuzzy_match, Sublime-
// style subsequence scoring) is pure and is ported directly to Go below. The
// component keeps the API + structure + Tailwind and models query / open /
// highlighted state as plain value props + callbacks.
//
// NOTE: the exported matcher `fuzzyMatch` would collide (case-folded) with the
// `FuzzyMatch` component in Go, so it is renamed to FuzzyMatchOne here. The
// other matchers keep their names (Go-cased): FuzzyMatchTypoTolerant,
// FuzzySegments, RankFuzzyMatches.
// NOTE: positions are rune indices (the TSX used UTF-16 code-unit indices);
// these agree for BMP text and are what FuzzySegments consumes here.

// ============================================================================
// Fuzzy matching  (Sublime-style subsequence scoring)
// ============================================================================

// FuzzyMatchResult is a scored match with the matched character positions.
type FuzzyMatchResult struct {
	Score     int
	Positions []int
}

// FuzzySegment is a run of text flagged matched or unmatched (for highlighting).
type FuzzySegment struct {
	Text  string
	Match bool
}

// FuzzyRankedItem is one ranked option: its value, score, and highlight segments.
type FuzzyRankedItem struct {
	Value    string
	Score    int
	Segments []FuzzySegment
}

const (
	fuzzySequentialBonus   = 15
	fuzzySeparatorBonus    = 30
	fuzzyCamelBonus        = 30
	fuzzyFirstLetterBonus  = 15
	fuzzyLeadingPenalty    = -5
	fuzzyMaxLeadingPenalty = -15
	fuzzyUnmatchedPenalty  = -1
	fuzzyRecursionLimit    = 10
	fuzzyTransposePenalty  = -20
	fuzzyExactSubstrBonus  = 100
)

func fuzzyIsLower(c rune) bool     { return c >= 'a' && c <= 'z' }
func fuzzyIsUpper(c rune) bool     { return c >= 'A' && c <= 'Z' }
func fuzzyIsSeparator(c rune) bool { return c == ' ' || c == '_' || c == '-' }

func fuzzyMax(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func fuzzyScore(target []rune, matches []int) int {
	score := 100
	score += fuzzyMax(fuzzyMaxLeadingPenalty, fuzzyLeadingPenalty*matches[0])
	score += fuzzyUnmatchedPenalty * (len(target) - len(matches))

	for i := 0; i < len(matches); i++ {
		curr := matches[i]
		if i > 0 && curr == matches[i-1]+1 {
			score += fuzzySequentialBonus
		}
		if curr == 0 {
			score += fuzzyFirstLetterBonus
		} else {
			prev := target[curr-1]
			if fuzzyIsLower(prev) && fuzzyIsUpper(target[curr]) {
				score += fuzzyCamelBonus
			}
			if fuzzyIsSeparator(prev) {
				score += fuzzySeparatorBonus
			}
		}
	}
	return score
}

func fuzzyRecurse(query, target []rune, qi, ti int, matches []int, rec *int) []int {
	*rec++
	if *rec >= fuzzyRecursionLimit {
		return nil
	}

	var best []int
	for qi < len(query) && ti < len(target) {
		if unicode.ToLower(query[qi]) == unicode.ToLower(target[ti]) {
			skipped := fuzzyRecurse(query, target, qi, ti+1, append([]int(nil), matches...), rec)
			if skipped != nil && (best == nil || fuzzyScore(target, skipped) > fuzzyScore(target, best)) {
				best = skipped
			}
			matches = append(matches, ti)
			qi++
		}
		ti++
	}

	if qi < len(query) {
		return best // query not fully consumed -> this path failed
	}
	if best == nil || fuzzyScore(target, matches) > fuzzyScore(target, best) {
		return matches
	}
	return best
}

// FuzzyMatchOne scores query against target, or returns nil if the query is
// empty or is not a subsequence of target. (Port of the TSX `fuzzyMatch`.)
func FuzzyMatchOne(query, target string) *FuzzyMatchResult {
	if query == "" {
		return nil
	}
	qr := []rune(query)
	tr := []rune(target)
	rec := 0
	matches := fuzzyRecurse(qr, tr, 0, 0, nil, &rec)
	if matches == nil {
		return nil
	}
	score := fuzzyScore(tr, matches)
	// A contiguous substring hit ("bankof" in "Bankof") should outrank a
	// word-boundary match split across tokens ("Bank of America"). The bonus is
	// constant per query/target, so it lives here rather than in the per-
	// alignment scorer the recursion uses to pick match positions.
	if strings.Contains(strings.ToLower(target), strings.ToLower(query)) {
		score += fuzzyExactSubstrBonus
	}
	return &FuzzyMatchResult{Score: score, Positions: matches}
}

// FuzzyMatchTypoTolerant also tries every single adjacent-swap variant of the
// query (subsequence matching can't tolerate a transposed typo like "teh" vs
// "the"), penalizing transposed hits so exact matches still rank first.
func FuzzyMatchTypoTolerant(query, target string) *FuzzyMatchResult {
	best := FuzzyMatchOne(query, target)
	qr := []rune(query)
	for i := 0; i < len(qr)-1; i++ {
		swapped := string(qr[:i]) + string(qr[i+1]) + string(qr[i]) + string(qr[i+2:])
		m := FuzzyMatchOne(swapped, target)
		if m == nil {
			continue
		}
		score := m.Score + fuzzyTransposePenalty
		if best == nil || score > best.Score {
			best = &FuzzyMatchResult{Score: score, Positions: m.Positions}
		}
	}
	return best
}

// FuzzySegments splits text into alternating matched / unmatched runs for
// highlighting, using the given (rune-index) positions.
func FuzzySegments(text string, positions []int) []FuzzySegment {
	matched := make(map[int]bool, len(positions))
	for _, p := range positions {
		matched[p] = true
	}
	tr := []rune(text)
	var segments []FuzzySegment
	buf := ""
	bufMatch := matched[0]
	for i := 0; i < len(tr); i++ {
		isMatch := matched[i]
		if isMatch != bufMatch {
			if buf != "" {
				segments = append(segments, FuzzySegment{Text: buf, Match: bufMatch})
			}
			buf = ""
			bufMatch = isMatch
		}
		buf += string(tr[i])
	}
	if buf != "" {
		segments = append(segments, FuzzySegment{Text: buf, Match: bufMatch})
	}
	return segments
}

// RankFuzzyMatches ranks options against query, best score first, with
// highlight segments. Returns nil for an empty query. maxResults <= 0 means no
// limit. This is the headless entry point.
func RankFuzzyMatches(query string, options []string, maxResults int) []FuzzyRankedItem {
	q := strings.TrimSpace(query)
	if q == "" {
		return nil
	}
	var out []FuzzyRankedItem
	for _, value := range options {
		m := FuzzyMatchTypoTolerant(q, value)
		if m != nil {
			out = append(out, FuzzyRankedItem{Value: value, Score: m.Score, Segments: FuzzySegments(value, m.Positions)})
		}
	}
	sort.SliceStable(out, func(i, j int) bool { return out[i].Score > out[j].Score })
	if maxResults > 0 && len(out) > maxResults {
		out = out[:maxResults]
	}
	return out
}

// ============================================================================
// Component
// ============================================================================

// FuzzyMatchDisplay selects how FuzzyMatch renders its results.
type FuzzyMatchDisplay = string

const (
	FuzzyDisplayList     = "list"     // inline highlighted results below the input (default)
	FuzzyDisplayDropdown = "dropdown" // ComboBox-style autocomplete popover
	FuzzyDisplayNone     = "none"     // render only the input (headless)
)

const fuzzyInputCls = "bg-surface block w-full border border-line-strong rounded-default shadow-xs text-sm p-2 h-[38px] focus:outline-2 focus:outline-offset-1 focus:outline-sky-500"

// Mirror FormCombobox's dropdown styling: neutral hover / highlight.
const fuzzyDropdownCls = "bg-surface border border-line-strong rounded-default shadow-lg max-h-60 overflow-auto"
const fuzzyDropdownOptionCls = "w-full text-left p-2 text-sm cursor-pointer flex items-center justify-between gap-2 bg-transparent border-none hover:bg-surface-raised whitespace-nowrap"
const fuzzyDropdownOptionHighlightCls = "bg-surface-raised"

// FuzzyMatchProps configures the FuzzyMatch component. Query / Open / Highlighted
// are caller-held state (read at the call site); the On* callbacks report changes.
type FuzzyMatchProps struct {
	Options     []string
	Display     FuzzyMatchDisplay // default FuzzyDisplayList
	ShowScores  bool
	MaxResults  int
	Placeholder string
	Class       string
	ListClass   string

	Query       string // current query text
	Open        bool   // dropdown open (Display == dropdown)
	Highlighted int    // highlighted result index (dropdown keyboard selection)

	OnSelect      func(value string, item FuzzyRankedItem)
	OnQueryChange func(query string)
	OnResults     func(results []FuzzyRankedItem) // see NOTE: not auto-invoked
}

// fuzzyHighlight renders a segment list as sky-highlighted / plain spans.
func fuzzyHighlight(segments []FuzzySegment) []*vdom.VNode {
	out := make([]*vdom.VNode, 0, len(segments))
	for _, seg := range segments {
		if seg.Match {
			out = append(out, vdom.Span(vdom.Attr("class", "text-sky-700 dark:text-sky-400 font-semibold"), vdom.Text(seg.Text)))
		} else {
			out = append(out, vdom.Span(vdom.Text(seg.Text)))
		}
	}
	return out
}

// fuzzyScoreBadge renders the debug score badge, or nil when disabled.
func fuzzyScoreBadge(show bool, score int) *vdom.VNode {
	if !show {
		return nil
	}
	return vdom.Span(vdom.Attr("class", "ml-3 shrink-0 text-ss text-ink-faint"), vdom.Text(strconv.Itoa(score)))
}

// fuzzyListView renders the inline "list" display. Before the user types, all
// options show unranked so the searchable set is visible up front.
func fuzzyListView(p FuzzyMatchProps, results []FuzzyRankedItem) *vdom.VNode {
	var items []FuzzyRankedItem
	if strings.TrimSpace(p.Query) != "" {
		items = results
	} else {
		for _, v := range p.Options {
			items = append(items, FuzzyRankedItem{Value: v, Score: 0, Segments: []FuzzySegment{{Text: v, Match: false}}})
		}
	}

	outer := []vdom.Mod{vdom.Attr("class", cx("mt-3", pick(p.ListClass, "h-72 overflow-y-auto")))}
	if len(items) > 0 {
		ul := []vdom.Mod{vdom.Attr("class", "flex flex-col gap-0.5")}
		for _, r := range items {
			r := r
			li := []vdom.Mod{
				vdom.Attr("class", "flex items-center justify-between gap-3 px-2 py-1 rounded-default cursor-pointer hover:bg-surface-raised"),
			}
			if p.OnSelect != nil {
				li = append(li, vdom.On(vdom.EVENT_CLICK, func() { p.OnSelect(r.Value, r) }))
			}
			li = append(li, vdom.Span(kids([]vdom.Mod{vdom.Attr("class", "text-sm text-ink")}, fuzzyHighlight(r.Segments))...))
			if badge := fuzzyScoreBadge(p.ShowScores, r.Score); badge != nil {
				li = append(li, badge)
			}
			ul = append(ul, vdom.Li(li...))
		}
		outer = append(outer, vdom.Ul(ul...))
	} else if strings.TrimSpace(p.Query) != "" {
		outer = append(outer, vdom.P(vdom.Attr("class", "text-sm text-ink-muted italic"),
			vdom.Text(`No matches for "`+p.Query+`".`)))
	}
	return vdom.Div(outer...)
}

// fuzzyDropdownView renders the "dropdown" display's option panel.
//
// NOTE: the TSX portaled this to document.body with `position: fixed` coords
// from getBoundingClientRect + a resize/scroll effect; here it's a statically
// `absolute` panel anchored under the input (the container is `relative`).
func fuzzyDropdownView(p FuzzyMatchProps, results []FuzzyRankedItem) *vdom.VNode {
	mods := []vdom.Mod{
		vdom.Attr("data-floating-content", "true"),
		vdom.Attr("class", cx("absolute left-0 right-0 top-full mt-1 z-[200]", fuzzyDropdownCls)),
	}
	for i, r := range results {
		r := r
		cls := fuzzyDropdownOptionCls
		if i == p.Highlighted {
			cls = cx(fuzzyDropdownOptionCls, fuzzyDropdownOptionHighlightCls)
		}
		btnMods := []vdom.Mod{
			vdom.Attr("type", "button"),
			vdom.Attr("class", cls),
		}
		if p.OnSelect != nil {
			btnMods = append(btnMods, vdom.On(vdom.EVENT_CLICK, func() { p.OnSelect(r.Value, r) }))
		}
		btnMods = append(btnMods, vdom.Span(kids([]vdom.Mod{vdom.Attr("class", "text-ink")}, fuzzyHighlight(r.Segments))...))
		if badge := fuzzyScoreBadge(p.ShowScores, r.Score); badge != nil {
			btnMods = append(btnMods, badge)
		}
		mods = append(mods, vdom.Button(btnMods...))
	}
	return vdom.Div(mods...)
}

// FuzzyMatch renders a fuzzy-search input with inline ("list"), autocomplete
// ("dropdown"), or headless ("none") result display.
//
// NOTE: keyboard navigation (ArrowUp/Down/Enter/Escape) is dropped — the vdom
// Event exposes no key. In dropdown mode, updating Query and closing Open on
// select, opening on focus/typing, and outside-click dismissal are the caller's
// responsibility (report them via the On* callbacks). OnResults is retained for
// API parity but is not auto-invoked; headless callers should call
// RankFuzzyMatches directly.
func FuzzyMatch(p FuzzyMatchProps) *vdom.VNode {
	display := p.Display
	if display == "" {
		display = FuzzyDisplayList
	}

	results := RankFuzzyMatches(p.Query, p.Options, p.MaxResults)

	inputMods := []vdom.Mod{
		vdom.Attr("type", "text"),
		vdom.Attr("class", fuzzyInputCls),
		vdom.Prop("value", p.Query),
		vdom.Attr("placeholder", pick(p.Placeholder, "Search...")),
	}
	if p.OnQueryChange != nil {
		inputMods = append(inputMods, vdom.OnEvent(vdom.EVENT_INPUT, func(e vdom.Event) { p.OnQueryChange(e.Value()) }))
	}

	mods := []vdom.Mod{
		vdom.Attr("class", cx("relative", p.Class)),
		vdom.Input(inputMods...),
	}

	switch display {
	case FuzzyDisplayList:
		mods = append(mods, fuzzyListView(p, results))
	case FuzzyDisplayDropdown:
		if p.Open && len(results) > 0 {
			mods = append(mods, fuzzyDropdownView(p, results))
		}
	}
	return vdom.Div(mods...)
}
