// Package suggest provides fuzzy matching and "did you mean" suggestions.
package suggest

import (
	"sort"
	"strings"
	"unicode"
)

// Match represents a potential match with its score.
type Match struct {
	Value string
	Score int
}

// FindSimilar finds similar strings from candidates that are close to target.
// Returns up to maxResults matches, sorted by similarity (best first).
func FindSimilar(target string, candidates []string, maxResults int) []string {
	if len(candidates) == 0 || maxResults <= 0 {
		return nil
	}

	target = strings.ToLower(target)

	var matches []Match
	for _, c := range candidates {
		score := similarity(target, strings.ToLower(c))
		if score > 0 {
			matches = append(matches, Match{Value: c, Score: score})
		}
	}

	// Sort by score descending
	sort.Slice(matches, func(i, j int) bool {
		return matches[i].Score > matches[j].Score
	})

	// Take top results
	if len(matches) > maxResults {
		matches = matches[:maxResults]
	}

	result := make([]string, len(matches))
	for i, m := range matches {
		result[i] = m.Value
	}
	return result
}

// similarity calculates a similarity score between two strings.
// Higher is more similar. Uses a combination of techniques:
// - Prefix matching (high weight)
// - Contains matching (medium weight)
// - Levenshtein distance (for close matches)
// - Common substring matching
func similarity(a, b string) int {
	if a == b {
		return 1000 // Exact match
	}

	score := 0

	// Prefix matching - high value
	prefixLen := commonPrefixLength(a, b)
	if prefixLen > 0 {
		score += prefixLen * 20
	}

	// Suffix matching
	suffixLen := commonSuffixLength(a, b)
	if suffixLen > 0 {
		score += suffixLen * 10
	}

	// Contains matching
	if strings.Contains(b, a) {
		score += len(a) * 15
	} else if strings.Contains(a, b) {
		score += len(b) * 10
	}

	// Levenshtein distance for close matches
	dist := levenshteinDistance(a, b)
	maxLen := max(len(a), len(b))
	if maxLen > 0 && dist <= maxLen/2 {
		// Closer distance = higher score
		score += (maxLen - dist) * 5
	}

	// Common characters bonus (order-independent)
	common := commonChars(a, b)
	if common > 0 {
		score += common * 2
	}

	// Penalize very different lengths
	lenDiff := abs(len(a) - len(b))
	if lenDiff > 5 {
		score -= lenDiff * 2
	}

	return score
}

// commonPrefixLength returns the length of the common prefix.
func commonPrefixLength(a, b string) int {
	minLen := min(len(a), len(b))
	for i := 0; i < minLen; i++ {
		if a[i] != b[i] {
			return i
		}
	}
	return minLen
}

// commonSuffixLength returns the length of the common suffix.
func commonSuffixLength(a, b string) int {
	minLen := min(len(a), len(b))
	for i := 0; i < minLen; i++ {
		if a[len(a)-1-i] != b[len(b)-1-i] {
			return i
		}
	}
	return minLen
}

// commonChars counts characters that appear in both strings.
func commonChars(a, b string) int {
	aChars := make(map[rune]int)
	for _, r := range a {
		if unicode.IsLetter(r) || unicode.IsDigit(r) {
			aChars[r]++
		}
	}

	common := 0
	for _, r := range b {
		if count, ok := aChars[r]; ok && count > 0 {
			common++
			aChars[r]--
		}
	}
	return common
}

// levenshteinDistance calculates the edit distance between two strings.
func levenshteinDistance(a, b string) int {
	if len(a) == 0 {
		return len(b)
	}
	if len(b) == 0 {
		return len(a)
	}

	// Create distance matrix
	d := make([][]int, len(a)+1)
	for i := range d {
		d[i] = make([]int, len(b)+1)
		d[i][0] = i
	}
	for j := range d[0] {
		d[0][j] = j
	}

	// Fill the matrix
	for i := 1; i <= len(a); i++ {
		for j := 1; j <= len(b); j++ {
			cost := 1
			if a[i-1] == b[j-1] {
				cost = 0
			}
			d[i][j] = min3(
				d[i-1][j]+1,      // deletion
				d[i][j-1]+1,      // insertion
				d[i-1][j-1]+cost, // substitution
			)
		}
	}

	return d[len(a)][len(b)]
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

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

func min3(a, b, c int) int {
	return min(min(a, b), c)
}

func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

// FormatSuggestion formats an error message with suggestions.
func FormatSuggestion(entity, name string, suggestions []string, createHint string) string {
	var sb strings.Builder

	sb.WriteString(entity)
	sb.WriteString(" '")
	sb.WriteString(name)
	sb.WriteString("' not found")

	if len(suggestions) > 0 {
		sb.WriteString("\n\n  Did you mean?\n")
		for _, s := range suggestions {
			sb.WriteString("    • ")
			sb.WriteString(s)
			sb.WriteString("\n")
		}
	}

	if createHint != "" {
		sb.WriteString("\n  ")
		sb.WriteString(createHint)
	}

	return sb.String()
}
