package mentions

import (
	"sort"
	"strings"
	"unicode"

	"chat-switchboard/models"
)

// Mention represents a parsed @mention token in message content.
type Mention struct {
	Raw      string              // "@veronica-sharpe" as written (includes @)
	Name     string              // "veronica-sharpe" (normalized, no @)
	Start    int                 // byte offset in message content
	End      int                 // byte offset end (exclusive)
	Resolved *models.ChannelModel // nil if unresolved
	IsAll    bool                // true for @all special token
}

// Parse extracts @mentions from message content and resolves them
// against the channel's model roster.
//
// Resolution priority:
//   1. Exact handle match (e.g. @veronica-sharpe → handle "veronica-sharpe")
//   2. Prefix handle match (e.g. @veronica → only one handle starts with "veronica")
//   3. Fallback: normalized display_name match (backward compat)
//   4. @all is a special token that resolves to all roster entries
//
// @ must be at start of content or preceded by whitespace.
func Parse(content string, roster []models.ChannelModel) []Mention {
	if len(roster) == 0 || !strings.Contains(content, "@") {
		return nil
	}

	// Build handle lookup: handle → *ChannelModel
	// Also build display name lookup for backward compat
	type entry struct {
		handle     string // normalized handle
		normalized string // normalized display_name
		model      models.ChannelModel
	}
	entries := make([]entry, 0, len(roster))
	for _, cm := range roster {
		h := normalize(cm.Handle)
		dn := normalize(cm.DisplayName)
		entries = append(entries, entry{handle: h, normalized: dn, model: cm})
	}
	// Sort by handle length descending for longest-match-first
	sort.Slice(entries, func(i, j int) bool {
		return len(entries[i].handle) > len(entries[j].handle)
	})

	// Extract @tokens
	var mentions []Mention
	i := 0
	for i < len(content) {
		if content[i] != '@' {
			i++
			continue
		}

		// @ must be at start or preceded by whitespace
		if i > 0 && !unicode.IsSpace(rune(content[i-1])) {
			i++
			continue
		}

		// Extract the token after @
		start := i
		i++ // skip @
		tokenStart := i
		for i < len(content) && !unicode.IsSpace(rune(content[i])) {
			i++
		}

		if i == tokenStart {
			continue // bare @ with no token
		}

		// Strip trailing punctuation
		tokenEnd := i
		for tokenEnd > tokenStart && isMentionTrailingPunct(content[tokenEnd-1]) {
			tokenEnd--
		}
		if tokenEnd == tokenStart {
			continue
		}

		raw := content[start:tokenEnd]
		name := content[tokenStart:tokenEnd]
		normalizedName := normalize(name)

		// Special: @all
		if normalizedName == "all" {
			mentions = append(mentions, Mention{
				Raw:   raw,
				Name:  name,
				Start: start,
				End:   tokenEnd,
				IsAll: true,
			})
			continue
		}

		// 1. Exact handle match
		var resolved *models.ChannelModel
		for idx := range entries {
			if entries[idx].handle != "" && normalizedName == entries[idx].handle {
				cm := entries[idx].model
				resolved = &cm
				break
			}
		}

		// 2. Prefix handle match (unambiguous only)
		if resolved == nil {
			var prefixMatches []int
			for idx := range entries {
				if entries[idx].handle != "" && strings.HasPrefix(entries[idx].handle, normalizedName) {
					prefixMatches = append(prefixMatches, idx)
				}
			}
			if len(prefixMatches) == 1 {
				cm := entries[prefixMatches[0]].model
				resolved = &cm
			}
		}

		// 3. Fallback: display_name match (backward compat)
		if resolved == nil {
			for idx := range entries {
				if entries[idx].normalized != "" && normalizedName == entries[idx].normalized {
					cm := entries[idx].model
					resolved = &cm
					break
				}
			}
			// Display name prefix
			if resolved == nil {
				var prefixMatches []int
				for idx := range entries {
					if entries[idx].normalized != "" && strings.HasPrefix(entries[idx].normalized, normalizedName+" ") {
						prefixMatches = append(prefixMatches, idx)
					}
				}
				if len(prefixMatches) == 0 {
					for idx := range entries {
						if entries[idx].normalized != "" && strings.HasPrefix(entries[idx].normalized, normalizedName) {
							prefixMatches = append(prefixMatches, idx)
						}
					}
				}
				if len(prefixMatches) == 1 {
					cm := entries[prefixMatches[0]].model
					resolved = &cm
				}
			}
		}

		mentions = append(mentions, Mention{
			Raw:      raw,
			Name:     name,
			Start:    start,
			End:      tokenEnd,
			Resolved: resolved,
		})
	}

	return mentions
}

// ResolvedModels returns deduplicated resolved models from parsed mentions.
// Returns nil if no mentions resolved.
func ResolvedModels(mentions []Mention) []models.ChannelModel {
	if len(mentions) == 0 {
		return nil
	}

	seen := make(map[string]bool)
	var result []models.ChannelModel
	for _, m := range mentions {
		if m.Resolved != nil && !seen[m.Resolved.ID] {
			seen[m.Resolved.ID] = true
			result = append(result, *m.Resolved)
		}
	}
	return result
}

// HasAll returns true if any mention is @all.
func HasAll(mentions []Mention) bool {
	for _, m := range mentions {
		if m.IsAll {
			return true
		}
	}
	return false
}

// isMentionTrailingPunct returns true for punctuation that commonly
// follows an @mention but isn't part of the name.
func isMentionTrailingPunct(b byte) bool {
	switch b {
	case ',', '.', '!', '?', ':', ';', ')', ']', '}':
		return true
	}
	return false
}

// normalize converts a display name to a canonical form for matching:
// lowercase, hyphens → spaces collapsed, trimmed.
func normalize(s string) string {
	s = strings.ToLower(s)
	s = strings.ReplaceAll(s, "-", " ")
	s = strings.ReplaceAll(s, "_", " ")
	// Collapse multiple spaces
	fields := strings.Fields(s)
	return strings.Join(fields, " ")
}
