package routing

import (
	"fmt"
	"net/url"
	"regexp"
	"sort"
	"strings"
)

var mentionTokenPattern = regexp.MustCompile(`(^|[[:space:]])@([\p{L}\p{N}_-]+(?:/[\p{L}\p{N}_-]+)?)`)
var resolvedMentionLinkPattern = regexp.MustCompile(`\[(@([\p{L}\p{N}_-]+(?:/[\p{L}\p{N}_-]+)?))\]\(/mentions/(agent|human|service|team)/([^) \t\r\n]+)\)`)

type mentionTokenMatch struct {
	token       string
	start       int
	end         int
	resolved    bool
	mentionType string
	mentionID   string
}

type ResolvedMentionLink struct {
	Type  string
	ID    string
	Token string
}

// FormatResolvedMentionLink renders the same resolved Markdown mention
// syntax consumed by ParseResolvedMentionLinks. Callers should prefer this
// helper over assembling /mentions links themselves so a server-generated
// mention is guaranteed to participate in ordinary mention routing.
func FormatResolvedMentionLink(link ResolvedMentionLink) (string, error) {
	mentionType := strings.ToLower(strings.TrimSpace(link.Type))
	id := strings.TrimSpace(link.ID)
	token := strings.TrimPrefix(strings.TrimSpace(link.Token), "@")
	switch mentionType {
	case "agent", "human", "service", "team":
	default:
		return "", fmt.Errorf("resolved mention type %q is invalid", mentionType)
	}
	if id == "" || token == "" {
		return "", fmt.Errorf("resolved mention id and token are required")
	}
	rendered := fmt.Sprintf("[@%s](/mentions/%s/%s)", token, mentionType, url.PathEscape(id))
	parsed := ParseResolvedMentionLinks(rendered)
	if len(parsed) != 1 || parsed[0].Type != mentionType || parsed[0].ID != id ||
		!strings.EqualFold(parsed[0].Token, token) {
		return "", fmt.Errorf("resolved mention token %q is invalid", token)
	}
	return rendered, nil
}

type DirectMentionSpan struct {
	Token string
	Type  string
	ID    string
	Start int
	End   int
}

type ChannelBroadcastMode string

const (
	ChannelBroadcastModeNone   ChannelBroadcastMode = ""
	ChannelBroadcastModeAll    ChannelBroadcastMode = "all"
	ChannelBroadcastModeOnline ChannelBroadcastMode = "online"
)

func ParseMentionTokens(content string) []string {
	matches := scanMentionMatches(content)
	seen := make(map[string]struct{}, len(matches))
	tokens := make([]string, 0, len(matches))
	for _, match := range matches {
		if match.resolved {
			continue
		}
		token := strings.ToLower(strings.TrimSpace(match.token))
		if token == "" {
			continue
		}
		if _, exists := seen[token]; exists {
			continue
		}
		seen[token] = struct{}{}
		tokens = append(tokens, token)
	}
	if len(tokens) == 0 {
		return nil
	}
	return tokens
}

func ParseDirectMentionTokens(content string) []string {
	return ParseDirectMentionTokensLimit(content, 0)
}

func ParseDirectMentionTokensLimit(content string, limit int) []string {
	return directMentionTokensLimit(ParseDirectMentionSpansLimit(content, 0), limit)
}

func ParseDirectMentionSpansLimit(content string, limit int) []DirectMentionSpan {
	matches := scanMentionMatches(content)
	seen := make(map[string]bool)
	spans := make([]DirectMentionSpan, 0, len(matches))
	for _, match := range matches {
		token := strings.ToLower(strings.TrimSpace(match.token))
		if token == "" {
			continue
		}
		if !match.resolved {
			switch token {
			case string(ChannelBroadcastModeAll), string(ChannelBroadcastModeOnline):
				continue
			}
		}
		key := token
		if match.resolved {
			key = match.mentionType + "\x00" + match.mentionID
		}
		accepted, exists := seen[key]
		if !exists {
			accepted = limit <= 0 || len(seen) < limit
			seen[key] = accepted
		}
		if !accepted {
			continue
		}
		spans = append(spans, DirectMentionSpan{
			Token: token,
			Type:  match.mentionType,
			ID:    match.mentionID,
			Start: match.start,
			End:   match.end,
		})
	}
	if len(spans) == 0 {
		return nil
	}
	return spans
}

func directMentionTokensLimit(spans []DirectMentionSpan, limit int) []string {
	seen := make(map[string]struct{}, len(spans))
	tokens := make([]string, 0, len(spans))
	for _, span := range spans {
		token := strings.ToLower(strings.TrimSpace(span.Token))
		if token == "" {
			continue
		}
		if _, exists := seen[token]; exists {
			continue
		}
		seen[token] = struct{}{}
		tokens = append(tokens, token)
		if limit > 0 && len(tokens) >= limit {
			break
		}
	}
	if len(tokens) == 0 {
		return nil
	}
	return tokens
}

func ParseResolvedMentionLinks(content string) []ResolvedMentionLink {
	matches := scanMentionMatches(content)
	if len(matches) == 0 {
		return nil
	}

	links := make([]ResolvedMentionLink, 0, len(matches))
	seen := make(map[string]struct{}, len(matches))
	for _, match := range matches {
		if !match.resolved {
			continue
		}
		link := ResolvedMentionLink{
			Type:  match.mentionType,
			ID:    match.mentionID,
			Token: match.token,
		}
		key := link.Type + "\x00" + link.ID + "\x00" + strings.ToLower(link.Token)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		links = append(links, link)
	}
	return links
}

func rawMentionTokenMatches(search string) []mentionTokenMatch {
	matches := make([]mentionTokenMatch, 0)
	for searchOffset := 0; searchOffset < len(search); {
		match := mentionTokenPattern.FindStringSubmatchIndex(search[searchOffset:])
		if match == nil {
			break
		}
		if len(match) < 6 {
			break
		}
		matchStart := searchOffset + match[0]
		usesStartBoundary := match[2] == match[3]
		nextOffset := searchOffset + match[1]
		if usesStartBoundary && matchStart != 0 {
			if nextOffset <= searchOffset {
				break
			}
			searchOffset = nextOffset
			continue
		}
		tokenStart := searchOffset + match[4]
		tokenEnd := searchOffset + match[5]
		token := search[tokenStart:tokenEnd]
		if strings.Contains(token, "/") && tokenEnd < len(search) && search[tokenEnd] == '/' {
			if nextOffset <= searchOffset {
				break
			}
			searchOffset = nextOffset
			continue
		}
		matches = append(matches, mentionTokenMatch{
			token: token,
			start: tokenStart - 1,
			end:   tokenEnd,
		})
		if nextOffset <= searchOffset {
			break
		}
		searchOffset = nextOffset
	}
	return matches
}

type resolvedMentionLinkMatch struct {
	link       ResolvedMentionLink
	start      int
	end        int
	tokenStart int
	tokenEnd   int
}

func resolvedMentionLinkMatches(search string) []resolvedMentionLinkMatch {
	rawMatches := resolvedMentionLinkPattern.FindAllStringSubmatchIndex(search, -1)
	if len(rawMatches) == 0 {
		return nil
	}
	matches := make([]resolvedMentionLinkMatch, 0, len(rawMatches))
	for _, match := range rawMatches {
		if len(match) < 10 {
			continue
		}
		tokenStart := match[4]
		tokenEnd := match[5]
		typeStart := match[6]
		typeEnd := match[7]
		idStart := match[8]
		idEnd := match[9]
		if tokenStart < 0 || tokenEnd < 0 || typeStart < 0 || typeEnd < 0 || idStart < 0 || idEnd < 0 {
			continue
		}
		token := strings.ToLower(strings.TrimSpace(search[tokenStart:tokenEnd]))
		mentionType := strings.ToLower(strings.TrimSpace(search[typeStart:typeEnd]))
		id := strings.TrimSpace(search[idStart:idEnd])
		if decodedID, err := url.PathUnescape(id); err == nil {
			id = strings.TrimSpace(decodedID)
		}
		if token == "" || mentionType == "" || id == "" {
			continue
		}
		matches = append(matches, resolvedMentionLinkMatch{
			link: ResolvedMentionLink{
				Type:  mentionType,
				ID:    id,
				Token: token,
			},
			start:      match[0],
			end:        match[1],
			tokenStart: match[2],
			tokenEnd:   match[3],
		})
	}
	return matches
}

func scanMentionMatches(content string) []mentionTokenMatch {
	search := mentionRoutingMask(content)
	if strings.TrimSpace(search) == "" {
		return nil
	}
	matches := rawMentionTokenMatches(search)
	for _, resolved := range resolvedMentionLinkMatches(search) {
		matches = append(matches, mentionTokenMatch{
			token:       resolved.link.Token,
			start:       resolved.tokenStart,
			end:         resolved.tokenEnd,
			resolved:    true,
			mentionType: resolved.link.Type,
			mentionID:   resolved.link.ID,
		})
	}
	sort.SliceStable(matches, func(i, j int) bool {
		if matches[i].start == matches[j].start {
			return matches[i].end > matches[j].end
		}
		return matches[i].start < matches[j].start
	})
	return matches
}

func ParseControlledBroadcastMode(content string) (ChannelBroadcastMode, error) {
	hasAll := false
	hasOnline := false
	for _, token := range ParseMentionTokens(content) {
		switch token {
		case string(ChannelBroadcastModeAll):
			hasAll = true
		case string(ChannelBroadcastModeOnline):
			hasOnline = true
		}
	}

	if hasAll && hasOnline {
		return ChannelBroadcastModeNone, InvalidInput("message cannot include both @all and @online")
	}
	switch {
	case hasAll:
		return ChannelBroadcastModeAll, nil
	case hasOnline:
		return ChannelBroadcastModeOnline, nil
	default:
		return ChannelBroadcastModeNone, nil
	}
}

func ContainsControlledBroadcastToken(content string) bool {
	mode, err := ParseControlledBroadcastMode(content)
	return err != nil || mode != ChannelBroadcastModeNone
}

func StripFencedCodeBlocksForMentionRouting(content string) string {
	if content == "" {
		return content
	}
	content = stripMarkdownFencedCodeBlocksForMentionRouting(content)
	return stripInlineCodeSpansForMentionRouting(content)
}

func StripMarkdownBlockquoteForMentionRouting(content string) string {
	if content == "" || !strings.Contains(content, ">") {
		return content
	}

	lines := strings.SplitAfter(content, "\n")
	if len(lines) == 0 {
		return content
	}

	var builder strings.Builder
	for _, line := range lines {
		if isMarkdownBlockquoteLine(line) {
			builder.WriteString(markdownRoutingLineEnding(line))
			continue
		}
		builder.WriteString(line)
	}
	return builder.String()
}

func mentionRoutingMask(content string) string {
	if content == "" {
		return content
	}
	masked := []byte(content)
	mask := func(start, end int) {
		if start < 0 {
			start = 0
		}
		if end > len(masked) {
			end = len(masked)
		}
		for index := start; index < end; index++ {
			if masked[index] != '\n' && masked[index] != '\r' {
				masked[index] = ' '
			}
		}
	}

	offset := 0
	inFencedCodeBlock := false
	fencedStart := 0
	var fencedDelimiter byte
	var fencedDelimiterLen int
	for _, line := range strings.SplitAfter(content, "\n") {
		delimiter, delimiterLen, isDelimiter := fencedCodeBlockDelimiter(line)
		switch {
		case !inFencedCodeBlock && isDelimiter:
			inFencedCodeBlock = true
			fencedStart = offset
			fencedDelimiter = delimiter
			fencedDelimiterLen = delimiterLen
		case inFencedCodeBlock && isMatchingFencedCodeBlockDelimiterLine(line, fencedDelimiter, fencedDelimiterLen):
			mask(fencedStart, offset+len(line))
			inFencedCodeBlock = false
		case !inFencedCodeBlock && isMarkdownBlockquoteLine(line):
			mask(offset, offset+len(line))
		}
		offset += len(line)
	}
	if inFencedCodeBlock {
		mask(fencedStart, len(content))
	}

	for index := 0; index < len(content); {
		if content[index] != '`' || masked[index] == ' ' {
			index++
			continue
		}
		delimiterLen := repeatedDelimiterLength(content, index, '`')
		closingIndex := findInlineCodeSpanClosingDelimiter(content, index+delimiterLen, delimiterLen)
		if closingIndex < 0 {
			index += delimiterLen
			continue
		}
		mask(index, closingIndex+delimiterLen)
		index = closingIndex + delimiterLen
	}
	return string(masked)
}

func isMarkdownBlockquoteLine(line string) bool {
	trimmed := strings.TrimLeft(line, " \t")
	if trimmed == "" || trimmed[0] != '>' {
		return false
	}
	if len(trimmed) == 1 {
		return true
	}
	switch trimmed[1] {
	case ' ', '\t', '>', '\n', '\r':
		return true
	default:
		return false
	}
}

func markdownRoutingLineEnding(line string) string {
	switch {
	case strings.HasSuffix(line, "\r\n"):
		return "\r\n"
	case strings.HasSuffix(line, "\n"):
		return "\n"
	default:
		return ""
	}
}

func stripMarkdownFencedCodeBlocksForMentionRouting(content string) string {
	if content == "" || (!strings.Contains(content, "```") && !strings.Contains(content, "~~~")) {
		return content
	}

	lines := strings.SplitAfter(content, "\n")
	if len(lines) == 0 {
		return content
	}

	var builder strings.Builder
	inFencedCodeBlock := false
	var fencedDelimiter byte
	var fencedDelimiterLen int
	for _, line := range lines {
		delimiter, delimiterLen, isDelimiter := fencedCodeBlockDelimiter(line)
		if !inFencedCodeBlock && isDelimiter {
			inFencedCodeBlock = true
			fencedDelimiter = delimiter
			fencedDelimiterLen = delimiterLen
			continue
		}
		if inFencedCodeBlock && isMatchingFencedCodeBlockDelimiterLine(line, fencedDelimiter, fencedDelimiterLen) {
			inFencedCodeBlock = false
			fencedDelimiter = 0
			fencedDelimiterLen = 0
			continue
		}
		if inFencedCodeBlock {
			continue
		}
		builder.WriteString(line)
	}
	return builder.String()
}

func fencedCodeBlockDelimiter(line string) (byte, int, bool) {
	trimmed := strings.TrimLeft(line, " \t")
	if len(trimmed) == 0 {
		return 0, 0, false
	}

	switch trimmed[0] {
	case '`', '~':
		delimiter := trimmed[0]
		delimiterLen := 0
		for delimiterLen < len(trimmed) && trimmed[delimiterLen] == delimiter {
			delimiterLen++
		}
		if delimiterLen >= 3 {
			return delimiter, delimiterLen, true
		}
	}
	return 0, 0, false
}

func isMatchingFencedCodeBlockDelimiterLine(line string, delimiter byte, delimiterLen int) bool {
	currentDelimiter, currentDelimiterLen, ok := fencedCodeBlockDelimiter(line)
	return ok && currentDelimiter == delimiter && currentDelimiterLen >= delimiterLen
}

func stripInlineCodeSpansForMentionRouting(content string) string {
	if content == "" || !strings.Contains(content, "`") {
		return content
	}

	var builder strings.Builder
	for index := 0; index < len(content); {
		if content[index] != '`' {
			builder.WriteByte(content[index])
			index++
			continue
		}

		delimiterLen := repeatedDelimiterLength(content, index, '`')
		closingIndex := findInlineCodeSpanClosingDelimiter(content, index+delimiterLen, delimiterLen)
		if closingIndex < 0 {
			builder.WriteString(content[index : index+delimiterLen])
			index += delimiterLen
			continue
		}
		index = closingIndex + delimiterLen
	}
	return builder.String()
}

func repeatedDelimiterLength(content string, start int, delimiter byte) int {
	length := 0
	for start+length < len(content) && content[start+length] == delimiter {
		length++
	}
	return length
}

func findInlineCodeSpanClosingDelimiter(content string, start int, delimiterLen int) int {
	for index := start; index < len(content); {
		if content[index] != '`' {
			index++
			continue
		}
		currentDelimiterLen := repeatedDelimiterLength(content, index, '`')
		if currentDelimiterLen == delimiterLen {
			return index
		}
		index += currentDelimiterLen
	}
	return -1
}
