package protocol

import (
	"fmt"
	"regexp"
	"sort"
	"strings"
)

const knowledgeQACitationURITailPattern = `[a-z0-9._~:/?#\[\]@!$&'*+,;=%-]+`

const sharedEvidenceURIPrefix = "agentteam-kqa-shared-evidence://"

var (
	knowledgeQACitationDestinationPattern = regexp.MustCompile(
		`(?i)\(\s*<?agentteam-kqa-citation://` + knowledgeQACitationURITailPattern +
			`>?(?:\s+(?:"[^"\r\n]*"|'[^'\r\n]*'|\([^\r\n)]*\)))?\s*\)`,
	)
	knowledgeQACitationAutolinkPattern = regexp.MustCompile(
		`(?i)<agentteam-kqa-citation://` + knowledgeQACitationURITailPattern + `>`,
	)
	knowledgeQACitationBarePattern = regexp.MustCompile(
		`(?i)agentteam-kqa-citation://` + knowledgeQACitationURITailPattern,
	)
	knowledgeQACitationIDPattern = regexp.MustCompile(
		`(?i)agentteam-kqa-citation://[a-z0-9_]+/(kqc_[a-z0-9]{14})(?:[)>\s]|$)`,
	)
	runtimeEvidenceMarkdownPattern = regexp.MustCompile(
		`\[(E[1-9][0-9]*)\](?:\((?:[^()\r\n]|\([^()\r\n]*\))*\)|\[[^\]\r\n]*\])?`,
	)
)

type SharedEvidenceReference struct {
	EvidenceID string
	CitationID string
}

// CanonicalizeEvidenceMarkdown makes AgentTeam the only authority that can add
// durable citation destinations. Model-authored citation destinations are
// removed first, then verified evidence markers in Markdown text are linked to
// the server-allocated citation. Code spans and fenced code blocks stay literal.
func CanonicalizeEvidenceMarkdown(
	markdown string,
	hrefByEvidenceID map[string]string,
) (string, map[string]struct{}) {
	linked := make(map[string]struct{}, len(hrefByEvidenceID))
	canonical := transformKnowledgeQAMarkdownText(markdown, func(text string) string {
		text = stripKnowledgeQACitationDestinations(text)
		return linkRuntimeEvidenceMarkers(text, hrefByEvidenceID, linked)
	})
	return canonical, linked
}

func stripKnowledgeQACitationDestinations(text string) string {
	text = knowledgeQACitationDestinationPattern.ReplaceAllString(text, "")
	text = knowledgeQACitationAutolinkPattern.ReplaceAllString(text, "")
	return knowledgeQACitationBarePattern.ReplaceAllString(text, "")
}

// StripCitationDestinations removes every server-owned Knowledge Q&A citation
// destination from rendered Markdown while preserving visible evidence markers.
// Unlike canonical answer rendering, this intentionally also scrubs code spans
// and blocks: authority-bound session/citation identifiers must not cross into
// prompts or shared snapshots that cannot reopen the owner-scoped citation.
func StripCitationDestinations(markdown string) string {
	return stripKnowledgeQACitationDestinations(markdown)
}

// PrepareSharedEvidenceMarkdown replaces each verified repository evidence
// marker with a presentation-only destination that contains only its E-number,
// and returns the server-side citation mapping separately. The share reader can
// therefore request one account-bound evidence item without receiving an
// owner-scoped citation ID. Authority-bearing destinations are still removed
// everywhere, including code spans and fenced code blocks.
func PrepareSharedEvidenceMarkdown(markdown string) (string, []SharedEvidenceReference, error) {
	references := make(map[string]string)
	restricted := transformKnowledgeQAMarkdownText(markdown, func(text string) string {
		return restrictLinkedEvidenceMarkers(text, references)
	})
	result := make([]SharedEvidenceReference, 0, len(references))
	for evidenceID, citationID := range references {
		result = append(result, SharedEvidenceReference{
			EvidenceID: evidenceID,
			CitationID: citationID,
		})
	}
	sort.Slice(result, func(i, j int) bool {
		return result[i].EvidenceID < result[j].EvidenceID
	})
	for _, reference := range result {
		if reference.CitationID == "" || len(reference.EvidenceID) > 64 {
			return "", nil, fmt.Errorf("shared evidence marker %s is invalid or ambiguous", reference.EvidenceID)
		}
	}
	return stripKnowledgeQACitationDestinations(restricted), result, nil
}

func restrictLinkedEvidenceMarkers(text string, references map[string]string) string {
	matches := runtimeEvidenceMarkdownPattern.FindAllStringSubmatchIndex(text, -1)
	if len(matches) == 0 {
		return text
	}
	var result strings.Builder
	result.Grow(len(text))
	cursor := 0
	bracketDepth := 0
	for _, match := range matches {
		start, end := match[0], match[1]
		prefix := text[cursor:start]
		result.WriteString(prefix)
		bracketDepth = markdownBracketDepth(bracketDepth, prefix)
		markerEnd := match[3] + 1
		if end == markerEnd || bracketDepth > 0 ||
			isEscapedMarkdownMarker(text, start) ||
			isMarkdownImageMarker(text, start) ||
			isMarkdownReferenceDefinition(text, start, markerEnd) {
			result.WriteString(text[start:end])
			cursor = end
			continue
		}
		evidenceID := text[match[2]:match[3]]
		citationMatch := knowledgeQACitationIDPattern.FindStringSubmatch(text[start:end])
		if len(citationMatch) != 2 {
			result.WriteString(text[start:end])
			cursor = end
			continue
		}
		citationID := strings.ToLower(citationMatch[1])
		if existing, ok := references[evidenceID]; ok && existing != citationID {
			references[evidenceID] = ""
		} else if !ok {
			references[evidenceID] = citationID
		}
		result.WriteString("[" + evidenceID + "](" + sharedEvidenceURIPrefix + evidenceID + ")")
		cursor = end
	}
	result.WriteString(text[cursor:])
	return result.String()
}

func linkRuntimeEvidenceMarkers(
	text string,
	hrefByEvidenceID map[string]string,
	linked map[string]struct{},
) string {
	matches := runtimeEvidenceMarkdownPattern.FindAllStringSubmatchIndex(text, -1)
	if len(matches) == 0 {
		return text
	}
	var result strings.Builder
	result.Grow(len(text))
	cursor := 0
	bracketDepth := 0
	for _, match := range matches {
		start, end := match[0], match[1]
		prefix := text[cursor:start]
		result.WriteString(prefix)
		bracketDepth = markdownBracketDepth(bracketDepth, prefix)
		markerEnd := match[3] + 1
		if bracketDepth > 0 || isEscapedMarkdownMarker(text, start) ||
			isMarkdownImageMarker(text, start) ||
			isMarkdownReferenceDefinition(text, start, markerEnd) {
			result.WriteString(text[start:end])
			cursor = end
			continue
		}
		evidenceID := text[match[2]:match[3]]
		marker := "[" + evidenceID + "]"
		if href, exists := hrefByEvidenceID[evidenceID]; exists {
			result.WriteString(marker + "(" + href + ")")
			linked[evidenceID] = struct{}{}
		} else {
			result.WriteString(marker)
		}
		cursor = end
	}
	result.WriteString(text[cursor:])
	return result.String()
}

// CollapseAdjacentEvidenceLinks removes exact adjacent repetitions that resolve
// to the same verified citation while leaving non-adjacent markers and Markdown
// code untouched.
func CollapseAdjacentEvidenceLinks(
	markdown string,
	hrefByEvidenceID map[string]string,
) string {
	return transformKnowledgeQAMarkdownText(markdown, func(text string) string {
		matches := runtimeEvidenceMarkdownPattern.FindAllStringSubmatchIndex(text, -1)
		if len(matches) < 2 {
			return text
		}
		var result strings.Builder
		result.Grow(len(text))
		cursor := 0
		bracketDepth := 0
		previousHref := ""
		previousMarkerEnd := -1
		for _, match := range matches {
			start, end := match[0], match[1]
			prefix := text[cursor:start]
			depthAtMarker := markdownBracketDepth(bracketDepth, prefix)
			evidenceID := text[match[2]:match[3]]
			href, allowed := hrefByEvidenceID[evidenceID]
			canonicalLink := "[" + evidenceID + "](" + href + ")"
			linkedCitation := allowed && text[start:end] == canonicalLink &&
				depthAtMarker == 0 && !isEscapedMarkdownMarker(text, start) &&
				!isMarkdownImageMarker(text, start) &&
				!isMarkdownReferenceDefinition(text, start, match[3]+1)
			if linkedCitation && href == previousHref &&
				previousMarkerEnd >= 0 &&
				strings.TrimSpace(text[previousMarkerEnd:start]) == "" {
				cursor = end
				bracketDepth = depthAtMarker
				previousMarkerEnd = end
				continue
			}
			result.WriteString(prefix)
			bracketDepth = depthAtMarker
			result.WriteString(text[start:end])
			cursor = end
			if linkedCitation {
				previousHref = href
				previousMarkerEnd = end
			}
		}
		result.WriteString(text[cursor:])
		return result.String()
	})
}

// IsEvidenceOnlyMarkdown reports whether the non-code portion of a Markdown
// block contains only evidence markers and whitespace.
func IsEvidenceOnlyMarkdown(markdown string) bool {
	withoutMarkers := transformKnowledgeQAMarkdownText(markdown, func(text string) string {
		return runtimeEvidenceMarkdownPattern.ReplaceAllString(text, "")
	})
	return strings.TrimSpace(withoutMarkers) == ""
}

func markdownBracketDepth(depth int, text string) int {
	escaped := false
	for _, value := range text {
		if escaped {
			escaped = false
			continue
		}
		if value == '\\' {
			escaped = true
			continue
		}
		switch value {
		case '[':
			depth++
		case ']':
			if depth > 0 {
				depth--
			}
		}
	}
	return depth
}

func isMarkdownReferenceDefinition(text string, markerStart, markerEnd int) bool {
	if markerEnd >= len(text) || text[markerEnd] != ':' {
		return false
	}
	lineStart := strings.LastIndexByte(text[:markerStart], '\n') + 1
	prefix := text[lineStart:markerStart]
	return len(prefix) <= 3 && strings.Trim(prefix, " \t") == ""
}

func isEscapedMarkdownMarker(text string, start int) bool {
	backslashes := 0
	for start > 0 && text[start-1] == '\\' {
		backslashes++
		start--
	}
	return backslashes%2 == 1
}

func isMarkdownImageMarker(text string, start int) bool {
	return start > 0 && text[start-1] == '!'
}

func transformKnowledgeQAMarkdownText(markdown string, transform func(string) string) string {
	var result strings.Builder
	result.Grow(len(markdown))
	plainStart := 0
	for cursor := 0; cursor < len(markdown); {
		if isMarkdownLineStart(markdown, cursor) {
			if end, ok := fencedMarkdownBlockEnd(markdown, cursor); ok {
				result.WriteString(transform(markdown[plainStart:cursor]))
				result.WriteString(markdown[cursor:end])
				cursor, plainStart = end, end
				continue
			}
		}
		if markdown[cursor] == '`' {
			if end, ok := inlineMarkdownCodeSpanEnd(markdown, cursor); ok {
				result.WriteString(transform(markdown[plainStart:cursor]))
				result.WriteString(markdown[cursor:end])
				cursor, plainStart = end, end
				continue
			}
		}
		cursor++
	}
	result.WriteString(transform(markdown[plainStart:]))
	return result.String()
}

func isMarkdownLineStart(markdown string, index int) bool {
	return index == 0 || markdown[index-1] == '\n'
}

func fencedMarkdownBlockEnd(markdown string, lineStart int) (int, bool) {
	markerStart := lineStart
	for markerStart < len(markdown) && markerStart-lineStart < 4 && markdown[markerStart] == ' ' {
		markerStart++
	}
	if markerStart >= len(markdown) ||
		(markdown[markerStart] != '`' && markdown[markerStart] != '~') {
		return lineStart, false
	}
	marker := markdown[markerStart]
	markerLength := repeatedMarkdownByteCount(markdown, markerStart, marker)
	if markerLength < 3 {
		return lineStart, false
	}
	for nextLine := markdownLineEnd(markdown, markerStart+markerLength); nextLine < len(markdown); {
		lineEnd := markdownLineEnd(markdown, nextLine)
		line := strings.Trim(markdown[nextLine:lineEnd], " \t\r\n")
		if len(line) >= markerLength && strings.Trim(line, string(marker)) == "" {
			return lineEnd, true
		}
		nextLine = lineEnd
	}
	return len(markdown), true
}

func markdownLineEnd(markdown string, start int) int {
	if offset := strings.IndexByte(markdown[start:], '\n'); offset >= 0 {
		return start + offset + 1
	}
	return len(markdown)
}

func inlineMarkdownCodeSpanEnd(markdown string, start int) (int, bool) {
	fenceLength := repeatedMarkdownByteCount(markdown, start, '`')
	for cursor := start + fenceLength; cursor < len(markdown); {
		next := strings.IndexByte(markdown[cursor:], '`')
		if next < 0 {
			return start, false
		}
		candidate := cursor + next
		candidateLength := repeatedMarkdownByteCount(markdown, candidate, '`')
		if candidateLength == fenceLength {
			return candidate + candidateLength, true
		}
		cursor = candidate + candidateLength
	}
	return start, false
}

func repeatedMarkdownByteCount(markdown string, start int, value byte) int {
	end := start
	for end < len(markdown) && markdown[end] == value {
		end++
	}
	return end - start
}
