package kb

import (
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"path"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/store/kb"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

const (
	defaultKBSearchLimit              = 10
	maxKBSearchLimit                  = 50
	maxKBRetrievalBases               = 100
	maxKBRetrievalContextSourcesPerKB = 100
	defaultKBTreeLimit                = 100
	maxKBTreeLimit                    = 500
	defaultKBManifestLimit            = 200
	maxKBBatchItems                   = 25
	maxKBTreeDepth                    = 5
	kbCitationTokenTTL                = 24 * time.Hour
)

type kbRetrievalAuthority string

const (
	kbRetrievalAuthorityAgentGrant                 kbRetrievalAuthority = "agent_grant"
	kbRetrievalAuthorityActingUserRepositoryAccess kbRetrievalAuthority = "acting_user_repository_access"
)

type agentRetrievalBase struct {
	KnowledgeBase kb.KnowledgeBase
	Grant         kb.KnowledgeBaseGrant
	Authority     kbRetrievalAuthority
}

func (base agentRetrievalBase) wikiAuthorizationHeader(agentAuth, actingUserAuth string) (string, error) {
	if base.Authority == kbRetrievalAuthorityActingUserRepositoryAccess {
		if strings.TrimSpace(actingUserAuth) == "" {
			return "", forbidden("acting user repository authorization is required")
		}
		return strings.TrimSpace(actingUserAuth), nil
	}
	return strings.TrimSpace(agentAuth), nil
}

func (base agentRetrievalBase) contentPrincipal(agentID, actingUserID string) (string, string, error) {
	switch base.Authority {
	case kbRetrievalAuthorityAgentGrant:
		if strings.TrimSpace(agentID) == "" {
			return "", "", forbidden("knowledge retrieval agent principal is required")
		}
		return strings.TrimSpace(agentID), kbContentSubjectAgent, nil
	case kbRetrievalAuthorityActingUserRepositoryAccess:
		if strings.TrimSpace(actingUserID) == "" {
			return "", "", forbidden("knowledge retrieval user principal is required")
		}
		return strings.TrimSpace(actingUserID), kbContentSubjectUser, nil
	default:
		return "", "", forbidden("knowledge retrieval authority is invalid")
	}
}

func (s *KBService) wikiReaderForSource(source KBRetrievalSource) (KBWikiReader, error) {
	if source.Native {
		if s.nativeWikiReader == nil {
			return nil, fmt.Errorf("native KB source content is unavailable")
		}
		return s.nativeWikiReader, nil
	}
	if s.wikiReader == nil {
		return nil, invalidArgument("kb wiki reader is not configured")
	}
	return s.wikiReader, nil
}

func (s *KBService) hasKBWikiReader() bool {
	return s != nil && (s.wikiReader != nil || s.nativeWikiReader != nil)
}

// KBWikiReader is the narrow source-content facade used by MeshX retrieval.
// MeshX owns KB scope, source selection, exact identity, and citations; the
// concrete reader is selected by the source's irreversible authority state.
type KBWikiReader interface {
	SearchWiki(ctx context.Context, request KBWikiSearchRequest) (KBWikiSearchResponse, error)
	ReadWikiPage(ctx context.Context, request KBWikiReadRequest) (KBWikiPage, error)
	ListWikiTree(ctx context.Context, request KBWikiTreeRequest) ([]KBWikiTreeEntry, error)
}

type KBWikiSearchRequest struct {
	AuthorizationHeader string
	PrincipalID         string
	PrincipalType       string
	WorkspaceID         string
	KnowledgeBaseID     string
	SourceID            string
	ContentID           string
	ContentVersionID    string
	RepoRole            string
	Owner               string
	Repo                string
	Query               string
	Limit               int
	Offset              int
	Revision            string
}

type KBWikiSearchResponse struct {
	Results []KBWikiSearchHit
	Total   int
	HasMore bool
}

type KBWikiSearchHit struct {
	Slug    string
	Title   string
	Snippet string
	SHA     string
	Score   float64
}

type KBWikiReadRequest struct {
	AuthorizationHeader string
	PrincipalID         string
	PrincipalType       string
	WorkspaceID         string
	KnowledgeBaseID     string
	SourceID            string
	ContentID           string
	ContentVersionID    string
	RepoRole            string
	Owner               string
	Repo                string
	Slug                string
	Revision            string
}

type KBWikiPage struct {
	Slug  string
	Title string
	Body  string
	SHA   string
}

type KBWikiTreeRequest struct {
	AuthorizationHeader string
	PrincipalID         string
	PrincipalType       string
	WorkspaceID         string
	KnowledgeBaseID     string
	SourceID            string
	ContentID           string
	ContentVersionID    string
	RepoRole            string
	Owner               string
	Repo                string
	Path                string
	Revision            string
}

type KBWikiTreeEntry struct {
	Path  string
	Name  string
	Kind  string
	Slug  string
	Title string
	IsDir bool
	SHA   string
	Size  int64
}

type KBSearchRequest struct {
	AgentID                       string
	ActingUserID                  string
	KnowledgeBaseIDs              []string
	SourceIDs                     []string
	Query                         string
	SearchMode                    string
	TitleOnly                     bool
	SlugPrefix                    string
	Limit                         int
	Offset                        int
	AuthorizationHeader           string
	ActingUserAuthorizationHeader string
}

type KBSearchResult struct {
	Hits      []KBSearchHit
	Total     int
	HasMore   bool
	Truncated bool
	Warnings  []string
	Error     *KBStructuredError
}

type KBSearchHit struct {
	CitationID        string
	KnowledgeBaseID   string
	KnowledgeBaseName string
	SourceID          string
	SourceKind        string
	RepoRole          string
	ResourceType      string
	ResourceID        string
	IndexStatus       string
	RepoOwner         string
	RepoName          string
	RepoFullName      string
	Slug              string
	Title             string
	Snippet           string
	SnippetPlain      string
	SHA               string
	Revision          string
	FreshnessStatus   string
	Score             float64
	CitationURI       string
	Route             KBCitationRoute
}

type KBReadRequest struct {
	AgentID                       string
	ActingUserID                  string
	KnowledgeBaseIDs              []string
	SourceIDs                     []string
	CitationURI                   string
	Route                         KBCitationRoute
	SourceID                      string
	RepoID                        string
	RepoRole                      string
	Path                          string
	Slug                          string
	Revision                      string
	HeadingsOnly                  bool
	Section                       string
	Offset                        int
	Limit                         int
	MaxBytes                      int
	Format                        string
	AuthorizationHeader           string
	ActingUserAuthorizationHeader string
}

type KBFetchRequest struct {
	AgentID                       string
	ActingUserID                  string
	CitationIDs                   []string
	MaxCharsPerItem               int
	AuthorizationHeader           string
	ActingUserAuthorizationHeader string
}

type KBFetchResult struct {
	Items    []KBFetchItem
	Warnings []string
}

type KBFetchItem struct {
	CitationID    string
	Title         string
	Content       string
	SourceLabel   string
	Truncated     bool
	OriginalBytes int
	Error         *KBStructuredError
}

type KBReadResult struct {
	KnowledgeBaseID   string
	KnowledgeBaseName string
	SourceID          string
	SourceKind        string
	RepoRole          string
	ResourceType      string
	ResourceID        string
	IndexStatus       string
	RepoOwner         string
	RepoName          string
	RepoFullName      string
	Slug              string
	Title             string
	Body              string
	Format            string
	Truncated         bool
	OriginalBytes     int
	SHA               string
	Revision          string
	FreshnessStatus   string
	CitationURI       string
	Route             KBCitationRoute
	Error             *KBStructuredError
}

type KBTreeRequest struct {
	AgentID                       string
	ActingUserID                  string
	KnowledgeBaseIDs              []string
	SourceIDs                     []string
	SourceID                      string
	CitationURI                   string
	RepoID                        string
	RepoRole                      string
	Path                          string
	Revision                      string
	Limit                         int
	Cursor                        string
	Depth                         int
	Compact                       bool
	IncludePages                  *bool
	IncludeDirs                   *bool
	AuthorizationHeader           string
	ActingUserAuthorizationHeader string
}

type KBTreeResult struct {
	Entries    []KBTreeEntry
	Total      int
	HasMore    bool
	NextCursor string
	Error      *KBStructuredError
}

type KBTreeEntry struct {
	KnowledgeBaseID   string
	KnowledgeBaseName string
	SourceID          string
	SourceKind        string
	RepoRole          string
	ResourceType      string
	ResourceID        string
	IndexStatus       string
	RepoOwner         string
	RepoName          string
	RepoFullName      string
	Path              string
	Name              string
	Kind              string
	Slug              string
	Title             string
	IsDir             bool
	SHA               string
	Revision          string
	FreshnessStatus   string
	Size              int64
	CitationURI       string
	Route             KBCitationRoute
}

type KBCapabilitiesRequest struct {
	AgentID          string
	ActingUserID     string
	KnowledgeBaseIDs []string
	SourceIDs        []string
}

type KBCapabilitiesResult struct {
	KnowledgeBases []KBCapabilityKnowledgeBase
}

type KBCapabilityKnowledgeBase struct {
	ID          string
	Name        string
	Description string
	Sources     []KBCapabilitySource
	Operations  map[string]KBCapabilityOperation
}

type KBCapabilitySource struct {
	SourceID        string
	SourceKind      string
	RepoRole        string
	ResourceType    string
	ResourceID      string
	IndexStatus     string
	RepoOwner       string
	RepoName        string
	RepoFullName    string
	Revision        string
	FreshnessStatus string
	Operations      map[string]KBCapabilityOperation
}

type KBCapabilityOperation struct {
	Supported       bool
	ReadOnly        bool
	Writable        bool
	BackendWritable bool
	MCPToolExposed  bool
	TargetType      string
	Limitations     []string
	PreferredTool   string
}

type KBManifestRequest struct {
	AgentID                       string
	ActingUserID                  string
	KnowledgeBaseIDs              []string
	SourceIDs                     []string
	Limit                         int
	Depth                         int
	Compact                       *bool
	AuthorizationHeader           string
	ActingUserAuthorizationHeader string
}

type KBManifestResult struct {
	KnowledgeBases []KBManifestKnowledgeBase
	TotalSources   int
	TotalPages     int
	HasMore        bool
}

type KBManifestKnowledgeBase struct {
	ID          string
	Name        string
	Description string
	Sources     []KBManifestSource
}

type KBManifestSource struct {
	SourceID        string
	SourceKind      string
	RepoRole        string
	ResourceType    string
	ResourceID      string
	IndexStatus     string
	RepoOwner       string
	RepoName        string
	RepoFullName    string
	Revision        string
	FreshnessStatus string
	Entries         []KBTreeEntry
	PageCount       int
	DirCount        int
	HasMore         bool
	NextCursor      string
	Error           *KBStructuredError
}

type KBStatRequest struct {
	AgentID                       string
	ActingUserID                  string
	KnowledgeBaseIDs              []string
	SourceIDs                     []string
	CitationURI                   string
	Route                         KBCitationRoute
	SourceID                      string
	RepoID                        string
	RepoRole                      string
	Path                          string
	Slug                          string
	Revision                      string
	AuthorizationHeader           string
	ActingUserAuthorizationHeader string
}

type KBStatResult struct {
	Exists            bool
	KnowledgeBaseID   string
	KnowledgeBaseName string
	SourceID          string
	SourceKind        string
	RepoRole          string
	ResourceType      string
	ResourceID        string
	IndexStatus       string
	RepoOwner         string
	RepoName          string
	RepoFullName      string
	Slug              string
	Title             string
	SHA               string
	Revision          string
	FreshnessStatus   string
	CitationURI       string
	Route             KBCitationRoute
	Error             *KBStructuredError
}

type KBBatchReadRequest struct {
	AgentID                       string
	ActingUserID                  string
	KnowledgeBaseIDs              []string
	SourceIDs                     []string
	Items                         []KBReadRequest
	AuthorizationHeader           string
	ActingUserAuthorizationHeader string
}

type KBBatchReadResult struct {
	Results []KBBatchReadItem
}

type KBBatchReadItem struct {
	Index int
	Page  *KBReadResult
	Error *KBStructuredError
}

type KBBatchTreeRequest struct {
	AgentID                       string
	ActingUserID                  string
	KnowledgeBaseIDs              []string
	SourceIDs                     []string
	Items                         []KBTreeRequest
	AuthorizationHeader           string
	ActingUserAuthorizationHeader string
}

type KBBatchTreeResult struct {
	Results []KBBatchTreeItem
}

type KBBatchTreeItem struct {
	Index int
	Tree  *KBTreeResult
	Error *KBStructuredError
}

type KBStructuredError struct {
	Code    string
	Message string
	Status  int
}

type KBCitationRoute struct {
	KnowledgeBaseID string
	SourceID        string
	RepoID          string
	RepoRole        string
	Kind            string
	Path            string
	Slug            string
	Revision        string
}

type KBRetrievalContextRequest struct {
	AgentID          string
	ActingUserID     string
	KnowledgeBaseIDs []string
	SourceIDs        []string
}

type KBRetrievalContextResult struct {
	KnowledgeBases []KBRetrievalContextKnowledgeBase
}

type KBRetrievalContextKnowledgeBase struct {
	ID             string
	Name           string
	Description    string
	Sources        []KBRetrievalContextSource
	HasMoreSources bool
}

type KBRetrievalContextSource struct {
	SourceID        string
	SourceKind      string
	RepoRole        string
	ResourceType    string
	ResourceID      string
	IndexStatus     string
	RepoOwner       string
	RepoName        string
	RepoFullName    string
	Revision        string
	FreshnessStatus string
}

func (s *KBService) DescribeKnowledgeContext(ctx context.Context, workspaceID, subject string, request KBRetrievalContextRequest) (KBRetrievalContextResult, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return KBRetrievalContextResult{}, err
	}
	bases, err := s.resolveRetrievalBases(ctx, workspaceID, request.AgentID, request.ActingUserID, request.KnowledgeBaseIDs)
	if err != nil {
		return KBRetrievalContextResult{}, err
	}
	result := KBRetrievalContextResult{KnowledgeBases: make([]KBRetrievalContextKnowledgeBase, 0, len(bases))}
	for _, retrievalBase := range bases {
		base := retrievalBase.KnowledgeBase
		sources, err := s.listAuthorizedRetrievalSourcesForBaseSelection(ctx, retrievalBase, request.SourceIDs)
		if err != nil {
			return KBRetrievalContextResult{}, err
		}
		hasMoreSources := len(sources) > maxKBRetrievalContextSourcesPerKB
		if hasMoreSources {
			sources = sources[:maxKBRetrievalContextSourcesPerKB]
		}
		out := KBRetrievalContextKnowledgeBase{
			ID:             base.ID,
			Name:           base.Name,
			Description:    base.Description,
			Sources:        make([]KBRetrievalContextSource, 0, len(sources)),
			HasMoreSources: hasMoreSources,
		}
		for _, source := range sources {
			owner, repo, fullName, err := kbRetrievalSourceRepo(source)
			if err != nil {
				return KBRetrievalContextResult{}, err
			}
			out.Sources = append(out.Sources, KBRetrievalContextSource{
				SourceID:        source.ID,
				SourceKind:      source.SourceKind,
				RepoRole:        source.RepoRole,
				ResourceType:    source.ResourceType,
				ResourceID:      source.ResourceID,
				IndexStatus:     source.IndexStatus,
				RepoOwner:       owner,
				RepoName:        repo,
				RepoFullName:    fullName,
				Revision:        source.Revision,
				FreshnessStatus: source.FreshnessStatus,
			})
		}
		result.KnowledgeBases = append(result.KnowledgeBases, out)
	}
	return result, nil
}

func (s *KBService) DescribeKnowledgeCapabilities(ctx context.Context, workspaceID, subject string, request KBCapabilitiesRequest) (KBCapabilitiesResult, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return KBCapabilitiesResult{}, err
	}
	bases, err := s.resolveRetrievalBases(ctx, workspaceID, request.AgentID, request.ActingUserID, request.KnowledgeBaseIDs)
	if err != nil {
		return KBCapabilitiesResult{}, err
	}
	result := KBCapabilitiesResult{KnowledgeBases: make([]KBCapabilityKnowledgeBase, 0, len(bases))}
	for _, retrievalBase := range bases {
		base := retrievalBase.KnowledgeBase
		sources, err := s.listAuthorizedRetrievalSourcesForBaseSelection(ctx, retrievalBase, request.SourceIDs)
		if err != nil {
			return KBCapabilitiesResult{}, err
		}
		if len(sources) > maxKBRetrievalContextSourcesPerKB {
			return KBCapabilitiesResult{}, invalidArgument("knowledge retrieval source set exceeds the product limit; select source_ids")
		}
		out := KBCapabilityKnowledgeBase{
			ID:          base.ID,
			Name:        base.Name,
			Description: base.Description,
			Sources:     make([]KBCapabilitySource, 0, len(sources)),
			Operations:  kbCapabilityOperations(s.hasKBWikiReader(), true, false),
		}
		for _, source := range sources {
			owner, repo, fullName, err := kbRetrievalSourceRepo(source)
			if err != nil {
				return KBCapabilitiesResult{}, err
			}
			ready := strings.TrimSpace(source.IndexStatus) == "ready"
			readerAvailable := (!source.Native && s.wikiReader != nil) || (source.Native && s.nativeWikiReader != nil)
			out.Sources = append(out.Sources, KBCapabilitySource{
				SourceID:        source.ID,
				SourceKind:      source.SourceKind,
				RepoRole:        source.RepoRole,
				ResourceType:    source.ResourceType,
				ResourceID:      source.ResourceID,
				IndexStatus:     source.IndexStatus,
				RepoOwner:       owner,
				RepoName:        repo,
				RepoFullName:    fullName,
				Revision:        source.Revision,
				FreshnessStatus: source.FreshnessStatus,
				Operations:      kbCapabilityOperations(readerAvailable, ready, source.SearchProjectionTruncated),
			})
		}
		result.KnowledgeBases = append(result.KnowledgeBases, out)
	}
	return result, nil
}

func kbCapabilityOperations(hasWikiReader bool, sourceReady bool, searchProjectionTruncated bool) map[string]KBCapabilityOperation {
	enabled := hasWikiReader && sourceReady
	limitations := []string{}
	if !hasWikiReader {
		limitations = append(limitations, "wiki reader is not configured")
	}
	if !sourceReady {
		limitations = append(limitations, "index_not_ready")
	}
	readOnly := func(tool string) KBCapabilityOperation {
		return KBCapabilityOperation{Supported: enabled, ReadOnly: true, MCPToolExposed: true, PreferredTool: tool, Limitations: append([]string(nil), limitations...)}
	}
	operations := map[string]KBCapabilityOperation{
		"read":       readOnly("kb_read"),
		"search":     readOnly("kb_search"),
		"tree":       readOnly("kb_tree"),
		"stat":       readOnly("kb_stat"),
		"manifest":   readOnly("kb_manifest"),
		"batch_read": readOnly("kb_batch_read"),
		"batch_tree": readOnly("kb_batch_tree"),
		"create":     {Supported: false, BackendWritable: true, MCPToolExposed: false, TargetType: "wiki_page", PreferredTool: "kb_wiki_create", Limitations: []string{"KB MCP write tools are not exposed in this build"}},
		"update":     {Supported: false, BackendWritable: true, MCPToolExposed: false, TargetType: "wiki_page", PreferredTool: "kb_wiki_update", Limitations: []string{"KB MCP write tools are not exposed in this build"}},
		"delete":     {Supported: false, BackendWritable: true, MCPToolExposed: false, TargetType: "wiki_page", PreferredTool: "kb_wiki_delete", Limitations: []string{"KB MCP write tools are not exposed in this build"}},
	}
	if searchProjectionTruncated {
		search := operations["search"]
		search.Limitations = append(search.Limitations, "exact-version search projection is bounded and truncated")
		operations["search"] = search
	}
	return operations
}

func (s *KBService) SearchKnowledge(ctx context.Context, workspaceID, subject string, request KBSearchRequest) (KBSearchResult, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return KBSearchResult{}, err
	}
	if !s.hasKBWikiReader() {
		return KBSearchResult{}, invalidArgument("kb wiki reader is not configured")
	}
	query := strings.TrimSpace(request.Query)
	if query == "" {
		return KBSearchResult{}, invalidArgument("knowledge search query is required")
	}
	slugPrefix := strings.Trim(strings.TrimSpace(request.SlugPrefix), "/")
	bases, err := s.resolveRetrievalBases(ctx, workspaceID, request.AgentID, request.ActingUserID, request.KnowledgeBaseIDs)
	if err != nil {
		return KBSearchResult{}, err
	}
	limit, clamped := normalizeKBSearchLimitWithClamp(request.Limit)
	offset := request.Offset
	if offset < 0 {
		offset = 0
	}
	hits := make([]KBSearchHit, 0)
	projectionTruncated := false
	requestedSourceIDs := normalizeKBIDSelection(request.SourceIDs)
	matchedSourceIDs := map[string]struct{}{}
	for _, retrievalBase := range bases {
		sources, err := s.listAuthorizedRetrievalSourcesForBaseSelection(ctx, retrievalBase, request.SourceIDs)
		if err != nil {
			return KBSearchResult{}, err
		}
		if len(sources) > maxKBRetrievalContextSourcesPerKB {
			return KBSearchResult{}, invalidArgument("knowledge retrieval source set exceeds the product limit; select source_ids")
		}
		for _, source := range sources {
			projectionTruncated = projectionTruncated || source.SearchProjectionTruncated
			if len(requestedSourceIDs) > 0 {
				matchedSourceIDs[source.ID] = struct{}{}
			}
			if strings.TrimSpace(source.IndexStatus) != "ready" {
				continue
			}
			sourceHits, err := s.searchRetrievalSource(ctx, retrievalBase, source, request, query, slugPrefix, limit, offset)
			if err != nil {
				return KBSearchResult{}, err
			}
			hits = append(hits, sourceHits...)
		}
	}
	for _, sourceID := range requestedSourceIDs {
		if _, ok := matchedSourceIDs[sourceID]; !ok {
			return KBSearchResult{}, codedNotFound("not_found", "knowledge base source not found")
		}
	}
	sort.SliceStable(hits, func(i, j int) bool {
		return hits[i].Score > hits[j].Score
	})
	total := len(hits)
	if total > limit {
		hits = hits[:limit]
	}
	for _, retrievalBase := range bases {
		base := retrievalBase.KnowledgeBase
		_ = s.auditRetrieval(ctx, base.WorkspaceID, base.ID, subject, "kb.retrieval.search", "knowledge_search", query, "Searched knowledge")
	}
	hasMore := total > len(hits)
	warnings := make([]string, 0, 2)
	if clamped {
		warnings = append(warnings, "max_results exceeded 50 and was clamped to 50")
	}
	if hasMore {
		warnings = append(warnings, "results truncated; refine your query for more specific results (no pagination)")
	}
	if projectionTruncated {
		warnings = append(warnings, "one or more native exact-version search projections are bounded and truncated")
	}
	return KBSearchResult{Hits: hits, Total: total, HasMore: hasMore, Truncated: hasMore, Warnings: warnings}, nil
}

func (s *KBService) searchRetrievalSource(ctx context.Context, retrievalBase agentRetrievalBase, source KBRetrievalSource, request KBSearchRequest, query, slugPrefix string, limit, offset int) ([]KBSearchHit, error) {
	base := retrievalBase.KnowledgeBase
	owner, repo, fullName, err := kbRetrievalSourceRepo(source)
	if err != nil {
		return nil, err
	}
	reader, err := s.wikiReaderForSource(source)
	if err != nil {
		return nil, err
	}
	principalID, principalType, err := retrievalBase.contentPrincipal(request.AgentID, request.ActingUserID)
	if err != nil {
		return nil, err
	}
	authorizationHeader := ""
	if !source.Native {
		authorizationHeader, err = retrievalBase.wikiAuthorizationHeader(request.AuthorizationHeader, request.ActingUserAuthorizationHeader)
		if err != nil {
			return nil, err
		}
	}
	response, err := reader.SearchWiki(ctx, KBWikiSearchRequest{
		AuthorizationHeader: authorizationHeader,
		PrincipalID:         principalID, PrincipalType: principalType, WorkspaceID: base.WorkspaceID,
		KnowledgeBaseID: base.ID, SourceID: source.ID, ContentID: source.ContentID, ContentVersionID: source.ContentVersionID,
		Owner: owner, Repo: repo, Query: query, Limit: limit, Offset: offset,
		Revision: strings.TrimSpace(source.Revision), RepoRole: source.RepoRole,
	})
	if err != nil {
		return nil, err
	}
	hits := make([]KBSearchHit, 0, len(response.Results))
	for _, hit := range response.Results {
		slug := strings.TrimSpace(hit.Slug)
		if slug == "" || (slugPrefix != "" && !strings.HasPrefix(slug, slugPrefix)) ||
			(request.TitleOnly && !strings.Contains(strings.ToLower(strings.TrimSpace(hit.Title)), strings.ToLower(query))) {
			continue
		}
		revision := firstNonEmptyString(strings.TrimSpace(hit.SHA), source.Revision)
		hits = append(hits, KBSearchHit{
			KnowledgeBaseID: base.ID, KnowledgeBaseName: base.Name, SourceID: source.ID, SourceKind: source.SourceKind,
			RepoRole: source.RepoRole, ResourceType: source.ResourceType, ResourceID: source.ResourceID, IndexStatus: source.IndexStatus,
			RepoOwner: owner, RepoName: repo, RepoFullName: fullName, Slug: slug, Title: strings.TrimSpace(hit.Title),
			Snippet: strings.TrimSpace(hit.Snippet), SnippetPlain: plainKnowledgeText(strings.TrimSpace(hit.Snippet)),
			SHA: strings.TrimSpace(hit.SHA), Revision: revision, FreshnessStatus: source.FreshnessStatus, Score: hit.Score,
			CitationID: s.kbCitationID(base.ID, source.ID, slug, revision), CitationURI: kbCitationURI(base.ID, source.ID, slug, revision),
			Route: kbCitationRoute(base.ID, source, slug, revision),
		})
	}
	return hits, nil
}

func (s *KBService) FetchKnowledge(ctx context.Context, workspaceID, subject string, request KBFetchRequest) (KBFetchResult, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return KBFetchResult{}, err
	}
	ids := normalizeKBIDSelection(request.CitationIDs)
	if len(ids) == 0 {
		return KBFetchResult{}, invalidArgument("citation_ids is required")
	}
	if len(ids) > maxKBBatchItems {
		ids = ids[:maxKBBatchItems]
	}
	result := KBFetchResult{Items: make([]KBFetchItem, 0, len(ids))}
	for _, id := range ids {
		item := KBFetchItem{CitationID: id}
		parsed, err := s.parseKBCitationID(id)
		if err != nil {
			item.Error = &KBStructuredError{Code: "stale", Message: "citation is stale; run kb_search again", Status: 404}
			result.Items = append(result.Items, item)
			continue
		}
		read, err := s.ReadKnowledge(ctx, workspaceID, subject, KBReadRequest{
			AgentID: request.AgentID, ActingUserID: request.ActingUserID, CitationURI: parsed.CitationURI,
			MaxBytes: request.MaxCharsPerItem, AuthorizationHeader: request.AuthorizationHeader, ActingUserAuthorizationHeader: request.ActingUserAuthorizationHeader,
		})
		if err != nil {
			structured := structuredKBError(err)
			switch structured.Code {
			case "permission_denied":
				structured.Code = "denied"
			case "not_found":
				structured.Code = "missing"
			}
			item.Error = structured
			result.Items = append(result.Items, item)
			continue
		}
		item.Title = read.Title
		item.Content = read.Body
		item.SourceLabel = firstNonEmptyString(read.KnowledgeBaseName, read.RepoFullName)
		item.Truncated = read.Truncated
		item.OriginalBytes = read.OriginalBytes
		if read.Truncated {
			result.Warnings = append(result.Warnings, "content truncated; refine kb_search and fetch a more specific citation")
		}
		result.Items = append(result.Items, item)
	}
	return result, nil
}

func (s *KBService) ReadKnowledge(ctx context.Context, workspaceID, subject string, request KBReadRequest) (KBReadResult, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return KBReadResult{}, err
	}
	if !s.hasKBWikiReader() {
		return KBReadResult{}, invalidArgument("kb wiki reader is not configured")
	}
	selectedKBIDs := normalizeKBIDSelection(request.KnowledgeBaseIDs)
	parsed := parsedKBCitation{}
	if strings.TrimSpace(request.CitationURI) != "" {
		var err error
		parsed, err = parseKBCitationURI(request.CitationURI)
		if err != nil {
			return KBReadResult{}, err
		}
		if len(selectedKBIDs) > 0 && !containsString(selectedKBIDs, parsed.KnowledgeBaseID) {
			return KBReadResult{}, forbidden("knowledge citation is outside the selected knowledge base scope")
		}
		request.SourceID = parsed.SourceID
		request.Slug = parsed.Slug
		if request.Revision == "" {
			request.Revision = parsed.Revision
		}
		selectedKBIDs = []string{parsed.KnowledgeBaseID}
	}
	if strings.TrimSpace(request.Route.KnowledgeBaseID) != "" || strings.TrimSpace(request.Route.SourceID) != "" || strings.TrimSpace(request.Route.Slug) != "" || strings.TrimSpace(request.Route.RepoID) != "" || strings.TrimSpace(request.Route.RepoRole) != "" || strings.TrimSpace(request.Route.Path) != "" {
		if strings.TrimSpace(request.Route.Kind) != "" && strings.TrimSpace(request.Route.Kind) != "wiki" {
			return KBReadResult{}, invalidArgument("knowledge read route kind must be wiki")
		}
		if len(selectedKBIDs) > 0 && strings.TrimSpace(request.Route.KnowledgeBaseID) != "" && !containsString(selectedKBIDs, request.Route.KnowledgeBaseID) {
			return KBReadResult{}, forbidden("knowledge route is outside the selected knowledge base scope")
		}
		if strings.TrimSpace(request.Route.KnowledgeBaseID) != "" {
			selectedKBIDs = []string{strings.TrimSpace(request.Route.KnowledgeBaseID)}
		}
		request.SourceID = firstNonEmptyString(request.Route.SourceID, request.SourceID)
		request.RepoID = firstNonEmptyString(request.Route.RepoID, request.RepoID)
		request.RepoRole = firstNonEmptyString(request.Route.RepoRole, request.RepoRole)
		request.Path = firstNonEmptyString(request.Route.Path, request.Path)
		request.Slug = firstNonEmptyString(request.Route.Slug, request.Slug)
		request.Revision = firstNonEmptyString(request.Route.Revision, request.Revision)
	}
	sourceID := strings.TrimSpace(request.SourceID)
	slug := firstNonEmptyString(strings.TrimSpace(request.Slug), wikiSlugFromPath(request.Path))
	if slug == "" {
		return KBReadResult{}, invalidArgument("knowledge read slug is required")
	}
	bases, err := s.resolveRetrievalBases(ctx, workspaceID, request.AgentID, request.ActingUserID, selectedKBIDs)
	if err != nil {
		return KBReadResult{}, err
	}
	retrievalBase, source, err := s.findSelectedSourceByRoute(ctx, bases, sourceID, request.RepoID, request.RepoRole)
	if err != nil {
		return KBReadResult{}, err
	}
	base := retrievalBase.KnowledgeBase
	if len(normalizeKBIDSelection(request.SourceIDs)) > 0 && !containsString(request.SourceIDs, source.ID) {
		return KBReadResult{}, forbidden("knowledge read source is outside the selected source scope")
	}
	if strings.TrimSpace(source.IndexStatus) != "ready" {
		return KBReadResult{}, invalidArgument("knowledge read source is not ready")
	}
	owner, repo, fullName, err := kbRetrievalSourceRepo(source)
	if err != nil {
		return KBReadResult{}, err
	}
	reader, err := s.wikiReaderForSource(source)
	if err != nil {
		return KBReadResult{}, err
	}
	principalID, principalType, err := retrievalBase.contentPrincipal(request.AgentID, request.ActingUserID)
	if err != nil {
		return KBReadResult{}, err
	}
	authorizationHeader := ""
	if !source.Native {
		authorizationHeader, err = retrievalBase.wikiAuthorizationHeader(request.AuthorizationHeader, request.ActingUserAuthorizationHeader)
		if err != nil {
			return KBReadResult{}, err
		}
	}
	versionID := ""
	if source.Native {
		versionID = firstNonEmptyString(request.Revision, source.ContentVersionID)
	}
	page, err := reader.ReadWikiPage(ctx, KBWikiReadRequest{
		AuthorizationHeader: authorizationHeader,
		PrincipalID:         principalID, PrincipalType: principalType, WorkspaceID: base.WorkspaceID,
		KnowledgeBaseID:  base.ID,
		SourceID:         source.ID,
		ContentID:        source.ContentID,
		ContentVersionID: versionID,
		Owner:            owner,
		Repo:             repo,
		Slug:             slug,
		Revision:         firstNonEmptyString(request.Revision, source.Revision),
		RepoRole:         source.RepoRole,
	})
	if err != nil {
		return KBReadResult{}, err
	}
	if strings.TrimSpace(page.Slug) != "" {
		slug = strings.TrimSpace(page.Slug)
	}
	sha := strings.TrimSpace(page.SHA)
	body, bodyFormat, truncated, originalBytes, err := shapeKnowledgeBody(page.Body, request)
	if err != nil {
		return KBReadResult{}, err
	}
	result := KBReadResult{
		KnowledgeBaseID:   base.ID,
		KnowledgeBaseName: base.Name,
		SourceID:          source.ID,
		SourceKind:        source.SourceKind,
		RepoRole:          source.RepoRole,
		ResourceType:      source.ResourceType,
		ResourceID:        source.ResourceID,
		IndexStatus:       source.IndexStatus,
		RepoOwner:         owner,
		RepoName:          repo,
		RepoFullName:      fullName,
		Slug:              slug,
		Title:             strings.TrimSpace(page.Title),
		Body:              body,
		Format:            bodyFormat,
		Truncated:         truncated,
		OriginalBytes:     originalBytes,
		SHA:               sha,
		Revision:          firstNonEmptyString(sha, request.Revision, source.Revision),
		FreshnessStatus:   source.FreshnessStatus,
		CitationURI:       kbCitationURI(base.ID, source.ID, slug, firstNonEmptyString(sha, request.Revision, source.Revision)),
		Route:             kbCitationRoute(base.ID, source, slug, firstNonEmptyString(sha, request.Revision, source.Revision)),
	}
	_ = s.auditRetrieval(ctx, base.WorkspaceID, base.ID, subject, "kb.retrieval.read", "citation", result.CitationURI, "Read knowledge citation")
	return result, nil
}

func (s *KBService) ListKnowledgeTree(ctx context.Context, workspaceID, subject string, request KBTreeRequest) (KBTreeResult, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return KBTreeResult{}, err
	}
	if !s.hasKBWikiReader() {
		return KBTreeResult{}, invalidArgument("kb wiki reader is not configured")
	}
	selectedKBIDs := normalizeKBIDSelection(request.KnowledgeBaseIDs)
	if strings.TrimSpace(request.CitationURI) != "" {
		parsed, err := parseKBCitationURI(request.CitationURI)
		if err != nil {
			return KBTreeResult{}, err
		}
		if len(selectedKBIDs) > 0 && !containsString(selectedKBIDs, parsed.KnowledgeBaseID) {
			return KBTreeResult{}, forbidden("knowledge citation is outside the selected knowledge base scope")
		}
		request.SourceID = parsed.SourceID
		if strings.TrimSpace(request.Revision) == "" {
			request.Revision = parsed.Revision
		}
		selectedKBIDs = []string{parsed.KnowledgeBaseID}
	}
	bases, err := s.resolveRetrievalBases(ctx, workspaceID, request.AgentID, request.ActingUserID, selectedKBIDs)
	if err != nil {
		return KBTreeResult{}, err
	}
	sourceID := strings.TrimSpace(request.SourceID)
	if sourceID == "" && strings.TrimSpace(request.RepoID) == "" {
		return KBTreeResult{}, invalidArgument("knowledge tree source id is required")
	}
	retrievalBase, source, err := s.findSelectedSourceByRoute(ctx, bases, sourceID, request.RepoID, request.RepoRole)
	if err != nil {
		return KBTreeResult{}, err
	}
	base := retrievalBase.KnowledgeBase
	if len(normalizeKBIDSelection(request.SourceIDs)) > 0 && !containsString(request.SourceIDs, source.ID) {
		return KBTreeResult{}, forbidden("knowledge tree source is outside the selected source scope")
	}
	if strings.TrimSpace(source.IndexStatus) != "ready" {
		return KBTreeResult{}, invalidArgument("knowledge tree source is not ready")
	}
	owner, repo, fullName, err := kbRetrievalSourceRepo(source)
	if err != nil {
		return KBTreeResult{}, err
	}
	principalID, principalType, err := retrievalBase.contentPrincipal(request.AgentID, request.ActingUserID)
	if err != nil {
		return KBTreeResult{}, err
	}
	authorizationHeader := ""
	if !source.Native {
		authorizationHeader, err = retrievalBase.wikiAuthorizationHeader(request.AuthorizationHeader, request.ActingUserAuthorizationHeader)
		if err != nil {
			return KBTreeResult{}, err
		}
	}
	versionID := ""
	if source.Native {
		versionID = firstNonEmptyString(request.Revision, source.ContentVersionID)
	}
	rawEntries, err := s.listWikiTreeDepth(ctx, KBWikiTreeRequest{
		AuthorizationHeader: authorizationHeader,
		PrincipalID:         principalID, PrincipalType: principalType, WorkspaceID: base.WorkspaceID,
		KnowledgeBaseID:  base.ID,
		SourceID:         source.ID,
		ContentID:        source.ContentID,
		ContentVersionID: versionID,
		Owner:            owner,
		Repo:             repo,
		Path:             strings.Trim(strings.TrimSpace(request.Path), "/"),
		Revision:         firstNonEmptyString(request.Revision, source.Revision),
		RepoRole:         source.RepoRole,
	}, request.Depth)
	if err != nil {
		return KBTreeResult{}, err
	}
	if strings.Trim(strings.TrimSpace(request.Path), "/") != "" && len(rawEntries) == 0 {
		return KBTreeResult{}, codedNotFound("not_found", "knowledge tree path not found")
	}
	includePages := request.IncludePages == nil || *request.IncludePages
	includeDirs := request.IncludeDirs == nil || *request.IncludeDirs
	allEntries := make([]KBTreeEntry, 0, len(rawEntries))
	for _, entry := range rawEntries {
		if entry.IsDir {
			if !includeDirs {
				continue
			}
		} else if !includePages {
			continue
		}
		slug := strings.TrimSpace(entry.Slug)
		citation := ""
		route := KBCitationRoute{}
		if slug != "" {
			revision := firstNonEmptyString(strings.TrimSpace(entry.SHA), request.Revision, source.Revision)
			citation = kbCitationURI(base.ID, source.ID, slug, revision)
			route = kbCitationRoute(base.ID, source, slug, revision)
		}
		mapped := KBTreeEntry{
			KnowledgeBaseID:   base.ID,
			KnowledgeBaseName: base.Name,
			SourceID:          source.ID,
			SourceKind:        source.SourceKind,
			RepoRole:          source.RepoRole,
			ResourceType:      source.ResourceType,
			ResourceID:        source.ResourceID,
			IndexStatus:       source.IndexStatus,
			RepoOwner:         owner,
			RepoName:          repo,
			RepoFullName:      fullName,
			Path:              strings.TrimSpace(entry.Path),
			Name:              strings.TrimSpace(entry.Name),
			Kind:              strings.TrimSpace(entry.Kind),
			Slug:              slug,
			Title:             strings.TrimSpace(entry.Title),
			IsDir:             entry.IsDir,
			SHA:               strings.TrimSpace(entry.SHA),
			Revision:          firstNonEmptyString(strings.TrimSpace(entry.SHA), request.Revision, source.Revision),
			FreshnessStatus:   source.FreshnessStatus,
			Size:              entry.Size,
			CitationURI:       citation,
			Route:             route,
		}
		if request.Compact {
			mapped.KnowledgeBaseName = ""
			mapped.ResourceID = ""
			mapped.ResourceType = ""
			mapped.RepoOwner = ""
			mapped.RepoName = ""
		}
		allEntries = append(allEntries, mapped)
	}
	total := len(allEntries)
	start, err := parseKBCursor(request.Cursor)
	if err != nil {
		return KBTreeResult{}, err
	}
	limit := normalizeKBTreeLimit(request.Limit)
	if start > total {
		start = total
	}
	end := start + limit
	if end > total {
		end = total
	}
	result := KBTreeResult{Entries: append([]KBTreeEntry(nil), allEntries[start:end]...), Total: total, HasMore: end < total}
	if result.HasMore {
		result.NextCursor = strconv.Itoa(end)
	}
	_ = s.auditRetrieval(ctx, base.WorkspaceID, base.ID, subject, "kb.retrieval.tree", "source", source.ID, "Listed knowledge tree")
	return result, nil
}

func (s *KBService) BuildKnowledgeManifest(ctx context.Context, workspaceID, subject string, request KBManifestRequest) (KBManifestResult, error) {
	if err := s.validateScope(workspaceID, subject); err != nil {
		return KBManifestResult{}, err
	}
	if !s.hasKBWikiReader() {
		return KBManifestResult{}, invalidArgument("kb wiki reader is not configured")
	}
	bases, err := s.resolveRetrievalBases(ctx, workspaceID, request.AgentID, request.ActingUserID, request.KnowledgeBaseIDs)
	if err != nil {
		return KBManifestResult{}, err
	}
	limit := request.Limit
	if limit <= 0 {
		limit = defaultKBManifestLimit
	}
	compact := true
	if request.Compact != nil {
		compact = *request.Compact
	}
	result := KBManifestResult{KnowledgeBases: make([]KBManifestKnowledgeBase, 0, len(bases))}
	for _, retrievalBase := range bases {
		base := retrievalBase.KnowledgeBase
		sources, err := s.listAuthorizedRetrievalSourcesForBaseSelection(ctx, retrievalBase, request.SourceIDs)
		if err != nil {
			return KBManifestResult{}, err
		}
		if len(sources) > maxKBRetrievalContextSourcesPerKB {
			return KBManifestResult{}, invalidArgument("knowledge retrieval source set exceeds the product limit; select source_ids")
		}
		out := KBManifestKnowledgeBase{ID: base.ID, Name: base.Name, Description: base.Description, Sources: make([]KBManifestSource, 0, len(sources))}
		for _, source := range sources {
			owner, repo, fullName, err := kbRetrievalSourceRepo(source)
			if err != nil {
				return KBManifestResult{}, err
			}
			sourceOut := KBManifestSource{
				SourceID:        source.ID,
				SourceKind:      source.SourceKind,
				RepoRole:        source.RepoRole,
				ResourceType:    source.ResourceType,
				ResourceID:      source.ResourceID,
				IndexStatus:     source.IndexStatus,
				RepoOwner:       owner,
				RepoName:        repo,
				RepoFullName:    fullName,
				Revision:        source.Revision,
				FreshnessStatus: source.FreshnessStatus,
			}
			result.TotalSources++
			if strings.TrimSpace(source.IndexStatus) != "ready" {
				sourceOut.Error = &KBStructuredError{Code: "index_not_ready", Message: "knowledge source is not ready", Status: 400}
				out.Sources = append(out.Sources, sourceOut)
				continue
			}
			tree, err := s.ListKnowledgeTree(ctx, workspaceID, subject, KBTreeRequest{
				AgentID:                       request.AgentID,
				ActingUserID:                  request.ActingUserID,
				KnowledgeBaseIDs:              []string{base.ID},
				SourceID:                      source.ID,
				Limit:                         limit,
				Depth:                         firstPositiveInt(request.Depth, 2),
				Compact:                       compact,
				AuthorizationHeader:           request.AuthorizationHeader,
				ActingUserAuthorizationHeader: request.ActingUserAuthorizationHeader,
			})
			if err != nil {
				sourceOut.Error = structuredKBError(err)
				out.Sources = append(out.Sources, sourceOut)
				continue
			}
			sourceOut.Entries = tree.Entries
			sourceOut.HasMore = tree.HasMore
			sourceOut.NextCursor = tree.NextCursor
			for _, entry := range tree.Entries {
				if entry.IsDir {
					sourceOut.DirCount++
				} else {
					sourceOut.PageCount++
					result.TotalPages++
				}
			}
			if tree.HasMore {
				result.HasMore = true
			}
			out.Sources = append(out.Sources, sourceOut)
		}
		result.KnowledgeBases = append(result.KnowledgeBases, out)
	}
	return result, nil
}

func (s *KBService) StatKnowledge(ctx context.Context, workspaceID, subject string, request KBStatRequest) (KBStatResult, error) {
	page, err := s.ReadKnowledge(ctx, workspaceID, subject, KBReadRequest{
		AgentID:                       request.AgentID,
		ActingUserID:                  request.ActingUserID,
		KnowledgeBaseIDs:              request.KnowledgeBaseIDs,
		SourceIDs:                     request.SourceIDs,
		CitationURI:                   request.CitationURI,
		Route:                         request.Route,
		SourceID:                      request.SourceID,
		RepoID:                        request.RepoID,
		RepoRole:                      request.RepoRole,
		Path:                          request.Path,
		Slug:                          request.Slug,
		Revision:                      request.Revision,
		HeadingsOnly:                  true,
		MaxBytes:                      1,
		AuthorizationHeader:           request.AuthorizationHeader,
		ActingUserAuthorizationHeader: request.ActingUserAuthorizationHeader,
	})
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return KBStatResult{Exists: false, Slug: firstNonEmptyString(strings.TrimSpace(request.Slug), wikiSlugFromPath(request.Path)), Error: structuredKBError(err)}, nil
		}
		return KBStatResult{}, err
	}
	return KBStatResult{
		Exists:            true,
		KnowledgeBaseID:   page.KnowledgeBaseID,
		KnowledgeBaseName: page.KnowledgeBaseName,
		SourceID:          page.SourceID,
		SourceKind:        page.SourceKind,
		RepoRole:          page.RepoRole,
		ResourceType:      page.ResourceType,
		ResourceID:        page.ResourceID,
		IndexStatus:       page.IndexStatus,
		RepoOwner:         page.RepoOwner,
		RepoName:          page.RepoName,
		RepoFullName:      page.RepoFullName,
		Slug:              page.Slug,
		Title:             page.Title,
		SHA:               page.SHA,
		Revision:          page.Revision,
		FreshnessStatus:   page.FreshnessStatus,
		CitationURI:       page.CitationURI,
		Route:             page.Route,
	}, nil
}

func (s *KBService) BatchReadKnowledge(ctx context.Context, workspaceID, subject string, request KBBatchReadRequest) (KBBatchReadResult, error) {
	if len(request.Items) == 0 {
		return KBBatchReadResult{}, invalidArgument("knowledge batch read items are required")
	}
	if len(request.Items) > maxKBBatchItems {
		return KBBatchReadResult{}, invalidArgument(fmt.Sprintf("knowledge batch read supports at most %d items", maxKBBatchItems))
	}
	result := KBBatchReadResult{Results: make([]KBBatchReadItem, 0, len(request.Items))}
	for index, item := range request.Items {
		item = mergeBatchReadDefaults(item, request)
		page, err := s.ReadKnowledge(ctx, workspaceID, subject, item)
		out := KBBatchReadItem{Index: index}
		if err != nil {
			out.Error = structuredKBError(err)
		} else {
			out.Page = &page
		}
		result.Results = append(result.Results, out)
	}
	return result, nil
}

func (s *KBService) BatchKnowledgeTree(ctx context.Context, workspaceID, subject string, request KBBatchTreeRequest) (KBBatchTreeResult, error) {
	if len(request.Items) == 0 {
		return KBBatchTreeResult{}, invalidArgument("knowledge batch tree items are required")
	}
	if len(request.Items) > maxKBBatchItems {
		return KBBatchTreeResult{}, invalidArgument(fmt.Sprintf("knowledge batch tree supports at most %d items", maxKBBatchItems))
	}
	result := KBBatchTreeResult{Results: make([]KBBatchTreeItem, 0, len(request.Items))}
	for index, item := range request.Items {
		item = mergeBatchTreeDefaults(item, request)
		tree, err := s.ListKnowledgeTree(ctx, workspaceID, subject, item)
		out := KBBatchTreeItem{Index: index}
		if err != nil {
			out.Error = structuredKBError(err)
		} else {
			out.Tree = &tree
		}
		result.Results = append(result.Results, out)
	}
	return result, nil
}

func (s *KBService) resolveRetrievalBases(ctx context.Context, workspaceID, agentID, actingUserID string, selectedKBIDs []string) ([]agentRetrievalBase, error) {
	agentID = strings.TrimSpace(agentID)
	workspaceID = strings.TrimSpace(workspaceID)
	actingUserID = strings.TrimSpace(actingUserID)
	if agentID == "" && actingUserID == "" {
		return nil, invalidArgument("knowledge retrieval principal is required")
	}
	selected := normalizeKBIDSelection(selectedKBIDs)
	if len(selected) > maxKBRetrievalBases {
		return nil, invalidArgument("knowledge retrieval selects too many knowledge bases")
	}
	selectedSet := map[string]struct{}{}
	for _, id := range selected {
		selectedSet[id] = struct{}{}
	}
	if agentID == "" {
		actingBases, err := s.resolveActingUserRetrievalBases(ctx, workspaceID, actingUserID, selected)
		if err != nil {
			return nil, err
		}
		accessible := make(map[string]struct{}, len(actingBases))
		bases := make([]agentRetrievalBase, 0, len(actingBases))
		for _, base := range actingBases {
			accessible[base.KnowledgeBase.ID] = struct{}{}
			bases = append(bases, base)
		}
		for _, id := range selected {
			if _, ok := accessible[id]; !ok {
				return nil, forbidden("knowledge base is not accessible to the current user")
			}
		}
		return bases, nil
	}
	var rows []kb.AgentAccessibleKnowledgeBase
	if len(selected) > 0 {
		candidateBases, err := s.store.ListKnowledgeBasesByIDs(ctx, workspaceID, selected)
		if err != nil {
			return nil, err
		}
		grants, err := s.store.ListKnowledgeBaseGrantsForSubject(ctx, workspaceID, "agent", agentID, selected)
		if err != nil {
			return nil, err
		}
		grantByKBID := make(map[string]kb.KnowledgeBaseGrant, len(grants))
		for _, grant := range grants {
			if strings.TrimSpace(grant.Role) != "" && strings.TrimSpace(grant.Role) != "none" {
				grantByKBID[grant.KnowledgeBaseID] = grant
			}
		}
		rows = make([]kb.AgentAccessibleKnowledgeBase, 0, len(candidateBases))
		for _, base := range candidateBases {
			if grant, ok := grantByKBID[base.ID]; ok && base.IsActive && base.DeletedAt == nil {
				rows = append(rows, kb.AgentAccessibleKnowledgeBase{KnowledgeBase: base, Grant: grant})
			}
		}
	} else {
		var err error
		rows, err = s.store.ListAgentAccessibleKnowledgeBases(ctx, workspaceID, agentID, false, 1, maxKBRetrievalBases+1)
		if err != nil {
			return nil, err
		}
		if len(rows) > maxKBRetrievalBases {
			return nil, invalidArgument("knowledge retrieval scope exceeds the product limit; select knowledge_base_ids")
		}
	}
	canAdministerWorkspace := s.canAdministerWorkspaceForRead(ctx, workspaceID, actingUserID)
	bases := make([]agentRetrievalBase, 0, len(rows))
	accessible := map[string]struct{}{}
	for _, row := range rows {
		if strings.TrimSpace(row.KnowledgeBase.PublicationState) == KBPublicationDraft &&
			!s.knowledgeBaseVisibleToSubjectWithWorkspaceAuthority(ctx, row.KnowledgeBase, actingUserID, canAdministerWorkspace) {
			continue
		}
		accessible[row.KnowledgeBase.ID] = struct{}{}
		if len(selectedSet) > 0 {
			if _, ok := selectedSet[row.KnowledgeBase.ID]; !ok {
				continue
			}
		} else if strings.TrimSpace(row.KnowledgeBase.PublicationState) == KBPublicationDraft {
			continue
		}
		bases = append(bases, agentRetrievalBase{KnowledgeBase: row.KnowledgeBase, Grant: row.Grant, Authority: kbRetrievalAuthorityAgentGrant})
	}
	for _, id := range selected {
		if _, ok := accessible[id]; !ok {
			return nil, forbidden("knowledge base is not accessible to the selected agent")
		}
	}
	return bases, nil
}

func (s *KBService) resolveActingUserRetrievalBases(ctx context.Context, workspaceID, actingUserID string, selectedKBIDs []string) ([]agentRetrievalBase, error) {
	actingUserID = strings.TrimSpace(actingUserID)
	if actingUserID == "" {
		return nil, nil
	}
	selected := normalizeKBIDSelection(selectedKBIDs)
	if len(selected) > maxKBRetrievalBases {
		return nil, invalidArgument("knowledge retrieval selects too many knowledge bases")
	}
	candidates := make([]kb.KnowledgeBase, 0, len(selected))
	if len(selected) > 0 {
		var err error
		candidates, err = s.store.ListKnowledgeBasesByIDs(ctx, workspaceID, selected)
		if err != nil {
			return nil, err
		}
	} else {
		bases, err := s.store.ListOfficialKnowledgeBases(ctx, workspaceID, 1, maxKBRetrievalBases+1)
		if err != nil {
			return nil, err
		}
		if len(bases) > maxKBRetrievalBases {
			return nil, invalidArgument("knowledge retrieval scope exceeds the product limit; select knowledge_base_ids")
		}
		candidates = bases
	}
	if len(candidates) == 0 {
		return nil, nil
	}
	ids := make([]string, 0, len(candidates))
	for _, base := range candidates {
		if strings.TrimSpace(base.ID) != "" {
			ids = append(ids, base.ID)
		}
	}
	grants, err := s.store.ListKnowledgeBaseGrantsForSubject(ctx, strings.TrimSpace(workspaceID), "user", actingUserID, ids)
	if err != nil {
		return nil, err
	}
	grantByKBID := make(map[string]kb.KnowledgeBaseGrant, len(grants))
	for _, grant := range grants {
		grantByKBID[grant.KnowledgeBaseID] = grant
	}
	accessByGrantID, err := s.knowledgeBaseRepositoryAccessByGrantID(ctx, workspaceID, grants)
	if err != nil {
		return nil, err
	}
	canAdministerWorkspace := s.canAdministerWorkspaceForRead(ctx, workspaceID, actingUserID)
	bases := make([]agentRetrievalBase, 0, len(candidates))
	for _, base := range candidates {
		if !base.IsActive || base.DeletedAt != nil {
			continue
		}
		if strings.TrimSpace(base.PublicationState) == KBPublicationDraft &&
			!s.knowledgeBaseVisibleToSubjectWithWorkspaceAuthority(ctx, base, actingUserID, canAdministerWorkspace) {
			continue
		}
		grant, ok := grantByKBID[base.ID]
		if !ok || strings.TrimSpace(grant.Role) == "" || strings.TrimSpace(grant.Role) == "none" {
			continue
		}
		if !hasCurrentKnowledgeRepositoryAccess(accessByGrantID[grant.ID]) {
			continue
		}
		bases = append(bases, agentRetrievalBase{KnowledgeBase: base, Grant: grant, Authority: kbRetrievalAuthorityActingUserRepositoryAccess})
	}
	return bases, nil
}

func hasCurrentKnowledgeRepositoryAccess(rows []repos.ResourceRepositoryAccessGrant) bool {
	for _, row := range rows {
		if strings.TrimSpace(row.Status) == repos.AccessStatusCurrent && row.Materialized {
			return true
		}
	}
	return false
}

func (s *KBService) listAuthorizedRetrievalSourcesForBaseSelection(ctx context.Context, retrievalBase agentRetrievalBase, sourceIDs []string) ([]KBRetrievalSource, error) {
	sources, err := s.listRetrievalSourcesForBaseSelection(ctx, retrievalBase.KnowledgeBase, sourceIDs)
	if err != nil {
		return sources, err
	}
	return s.authorizeNativeRetrievalSources(ctx, retrievalBase, sources)
}

func (s *KBService) authorizeNativeRetrievalSources(ctx context.Context, retrievalBase agentRetrievalBase, sources []KBRetrievalSource) ([]KBRetrievalSource, error) {
	if len(sources) == 0 || strings.TrimSpace(retrievalBase.Grant.ID) == "" {
		return sources, nil
	}
	nativeSourceIDs := make([]string, 0, len(sources))
	for _, source := range sources {
		if source.Native {
			nativeSourceIDs = append(nativeSourceIDs, source.ID)
		}
	}
	if len(nativeSourceIDs) == 0 {
		return sources, nil
	}
	overrides, err := s.store.ListKnowledgeBaseSourceRepositoryAccessOverridesForSources(ctx, retrievalBase.KnowledgeBase.WorkspaceID, []string{retrievalBase.Grant.ID}, nativeSourceIDs)
	if err != nil {
		return nil, err
	}
	overrideBySourceRole := make(map[string]string, len(overrides))
	for _, row := range overrides {
		if row.WorkspaceID != retrievalBase.KnowledgeBase.WorkspaceID || row.KnowledgeBaseID != retrievalBase.KnowledgeBase.ID ||
			row.GrantID != retrievalBase.Grant.ID || row.SubjectType != retrievalBase.Grant.SubjectType || row.SubjectID != retrievalBase.Grant.SubjectID {
			return nil, fmt.Errorf("native knowledge repository access override is inconsistent")
		}
		overrideBySourceRole[repositoryOverrideKey(row.SourceID, row.RepoRole)] = kb.NormalizeKnowledgeBaseRepositoryAccessProfile(row.RepositoryAccessProfile)
	}
	filtered := sources[:0]
	for _, source := range sources {
		if !source.Native {
			filtered = append(filtered, source)
			continue
		}
		profile := kb.NormalizeKnowledgeBaseRepositoryAccessProfile(retrievalBase.Grant.RepositoryAccessProfile)
		if override, ok := overrideBySourceRole[repositoryOverrideKey(source.ID, source.RepoRole)]; ok {
			profile = override
		}
		if repositoryAccessProfileCanBrowse(profile) {
			filtered = append(filtered, source)
		}
	}
	return filtered, nil
}

func repositoryAccessProfileCanBrowse(profile string) bool {
	switch kb.NormalizeKnowledgeBaseRepositoryAccessProfile(profile) {
	case kb.KnowledgeBaseRepositoryAccessProfileRead, kb.KnowledgeBaseRepositoryAccessProfileWrite, kb.KnowledgeBaseRepositoryAccessProfileManage:
		return true
	default:
		return false
	}
}

func (s *KBService) findSelectedSource(ctx context.Context, bases []agentRetrievalBase, sourceID string) (agentRetrievalBase, KBRetrievalSource, error) {
	sourceID = strings.TrimSpace(sourceID)
	if sourceID == "" {
		return agentRetrievalBase{}, KBRetrievalSource{}, notFound("knowledge base source not found")
	}
	row, err := s.store.GetSourceByID(ctx, basesWorkspaceID(bases), sourceID)
	if err != nil {
		return agentRetrievalBase{}, KBRetrievalSource{}, mapStoreNotFound(err)
	}
	for _, base := range bases {
		if base.KnowledgeBase.ID != row.KnowledgeBaseID {
			continue
		}
		projected, err := s.projectRetrievalSources(ctx, row.WorkspaceID, []kb.Source{row})
		if err != nil {
			return agentRetrievalBase{}, KBRetrievalSource{}, err
		}
		projected, err = s.authorizeNativeRetrievalSources(ctx, base, projected)
		if err != nil {
			return agentRetrievalBase{}, KBRetrievalSource{}, err
		}
		if len(projected) == 1 {
			return base, projected[0], nil
		}
		break
	}
	return agentRetrievalBase{}, KBRetrievalSource{}, notFound("knowledge base source not found")
}

func basesWorkspaceID(bases []agentRetrievalBase) string {
	if len(bases) == 0 {
		return ""
	}
	return strings.TrimSpace(bases[0].KnowledgeBase.WorkspaceID)
}

func (s *KBService) findSelectedSourceByRoute(ctx context.Context, bases []agentRetrievalBase, sourceID, repoID, repoRole string) (agentRetrievalBase, KBRetrievalSource, error) {
	sourceID = strings.TrimSpace(sourceID)
	repoID = strings.TrimSpace(repoID)
	repoRole = strings.TrimSpace(repoRole)
	if sourceID == "" && repoID == "" {
		return agentRetrievalBase{}, KBRetrievalSource{}, invalidArgument("knowledge source route requires source_id or repo_id")
	}
	if sourceID != "" {
		base, source, err := s.findSelectedSource(ctx, bases, sourceID)
		if err != nil {
			return agentRetrievalBase{}, KBRetrievalSource{}, err
		}
		if repoID != "" && !retrievalSourceRepoMatches(source, repoID) {
			return agentRetrievalBase{}, KBRetrievalSource{}, notFound("knowledge base source not found")
		}
		if repoRole != "" && strings.TrimSpace(source.RepoRole) != repoRole {
			return agentRetrievalBase{}, KBRetrievalSource{}, notFound("knowledge base source not found")
		}
		return base, source, nil
	}
	for _, base := range bases {
		sources, err := s.listRetrievalSourcesForBase(ctx, base.KnowledgeBase, 1, maxKBRetrievalContextSourcesPerKB+1)
		if err != nil {
			return agentRetrievalBase{}, KBRetrievalSource{}, err
		}
		sources, err = s.authorizeNativeRetrievalSources(ctx, base, sources)
		if err != nil {
			return agentRetrievalBase{}, KBRetrievalSource{}, err
		}
		for _, source := range sources {
			if sourceID != "" && strings.TrimSpace(source.ID) != sourceID {
				continue
			}
			if repoID != "" && !retrievalSourceRepoMatches(source, repoID) {
				continue
			}
			if repoRole != "" && strings.TrimSpace(source.RepoRole) != repoRole {
				continue
			}
			return base, source, nil
		}
	}
	return agentRetrievalBase{}, KBRetrievalSource{}, notFound("knowledge base source not found")
}

func retrievalSourceRepoMatches(source KBRetrievalSource, repoID string) bool {
	repoID = strings.TrimSpace(repoID)
	return repoID != "" && (strings.TrimSpace(source.RepoID) == repoID || strings.TrimSpace(source.ResourceID) == repoID)
}

func wikiSlugFromPath(raw string) string {
	slug := strings.Trim(strings.TrimSpace(raw), "/")
	slug = strings.TrimSuffix(slug, ".md")
	slug = strings.TrimSuffix(slug, ".markdown")
	return strings.Trim(slug, "/")
}

func kbRetrievalSourceRepo(source KBRetrievalSource) (owner, repo, fullName string, err error) {
	resourceID, owner, repo, ok := normalizeRepositoryFullName(source.ResourceID)
	if !ok {
		return "", "", "", invalidArgument("knowledge base source resource id must be owner/repo")
	}
	return owner, repo, resourceID, nil
}

func normalizeKBIDSelection(raw []string) []string {
	seen := map[string]struct{}{}
	selected := make([]string, 0, len(raw))
	for _, id := range raw {
		id = strings.TrimSpace(id)
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		selected = append(selected, id)
	}
	return selected
}

func normalizeKBSearchLimit(limit int) int {
	normalized, _ := normalizeKBSearchLimitWithClamp(limit)
	return normalized
}

func normalizeKBSearchLimitWithClamp(limit int) (int, bool) {
	if limit <= 0 {
		return defaultKBSearchLimit, false
	}
	if limit > maxKBSearchLimit {
		return maxKBSearchLimit, true
	}
	return limit, false
}

func normalizeKBTreeLimit(limit int) int {
	if limit <= 0 {
		return defaultKBTreeLimit
	}
	if limit > maxKBTreeLimit {
		return maxKBTreeLimit
	}
	return limit
}

func normalizeKBTreeDepth(depth int) int {
	if depth <= 0 {
		return 1
	}
	if depth > maxKBTreeDepth {
		return maxKBTreeDepth
	}
	return depth
}

func parseKBCursor(cursor string) (int, error) {
	cursor = strings.TrimSpace(cursor)
	if cursor == "" {
		return 0, nil
	}
	offset, err := strconv.Atoi(cursor)
	if err != nil || offset < 0 {
		return 0, invalidArgument("knowledge tree cursor is invalid")
	}
	return offset, nil
}

func firstPositiveInt(values ...int) int {
	for _, value := range values {
		if value > 0 {
			return value
		}
	}
	return 0
}

func (s *KBService) listWikiTreeDepth(ctx context.Context, request KBWikiTreeRequest, depth int) ([]KBWikiTreeEntry, error) {
	reader := s.wikiReader
	if strings.TrimSpace(request.ContentVersionID) != "" {
		reader = s.nativeWikiReader
	}
	if reader == nil {
		return nil, invalidArgument("kb wiki reader is not configured")
	}
	depth = normalizeKBTreeDepth(depth)
	root := strings.Trim(strings.TrimSpace(request.Path), "/")
	seen := map[string]struct{}{}
	out := make([]KBWikiTreeEntry, 0)
	var walk func(string, int) error
	walk = func(treePath string, remaining int) error {
		treePath = strings.Trim(strings.TrimSpace(treePath), "/")
		seenKey := treePath
		if seenKey == "" {
			seenKey = "."
		}
		if _, ok := seen[seenKey]; ok {
			return nil
		}
		seen[seenKey] = struct{}{}
		nextRequest := request
		nextRequest.Path = treePath
		entries, err := reader.ListWikiTree(ctx, nextRequest)
		if err != nil {
			return err
		}
		sort.SliceStable(entries, func(i, j int) bool {
			return strings.TrimSpace(entries[i].Path) < strings.TrimSpace(entries[j].Path)
		})
		for _, entry := range entries {
			out = append(out, entry)
		}
		if remaining <= 1 {
			return nil
		}
		for _, entry := range entries {
			if !entry.IsDir {
				continue
			}
			childPath := strings.Trim(strings.TrimSpace(entry.Path), "/")
			if childPath == "" {
				childPath = path.Join(treePath, strings.TrimSpace(entry.Name))
			}
			if childPath == "" || childPath == treePath {
				continue
			}
			if err := walk(childPath, remaining-1); err != nil {
				return err
			}
		}
		return nil
	}
	if err := walk(root, depth); err != nil {
		return nil, err
	}
	return out, nil
}

func mergeBatchReadDefaults(item KBReadRequest, request KBBatchReadRequest) KBReadRequest {
	item.AgentID = firstNonEmptyString(item.AgentID, request.AgentID)
	item.ActingUserID = firstNonEmptyString(item.ActingUserID, request.ActingUserID)
	if len(item.KnowledgeBaseIDs) == 0 {
		item.KnowledgeBaseIDs = request.KnowledgeBaseIDs
	}
	if len(item.SourceIDs) == 0 {
		item.SourceIDs = request.SourceIDs
	}
	if strings.TrimSpace(item.SourceID) == "" && len(normalizeKBIDSelection(request.SourceIDs)) == 1 {
		item.SourceID = normalizeKBIDSelection(request.SourceIDs)[0]
	}
	item.AuthorizationHeader = firstNonEmptyString(item.AuthorizationHeader, request.AuthorizationHeader)
	item.ActingUserAuthorizationHeader = firstNonEmptyString(item.ActingUserAuthorizationHeader, request.ActingUserAuthorizationHeader)
	return item
}

func mergeBatchTreeDefaults(item KBTreeRequest, request KBBatchTreeRequest) KBTreeRequest {
	item.AgentID = firstNonEmptyString(item.AgentID, request.AgentID)
	item.ActingUserID = firstNonEmptyString(item.ActingUserID, request.ActingUserID)
	if len(item.KnowledgeBaseIDs) == 0 {
		item.KnowledgeBaseIDs = request.KnowledgeBaseIDs
	}
	if len(item.SourceIDs) == 0 {
		item.SourceIDs = request.SourceIDs
	}
	if strings.TrimSpace(item.SourceID) == "" && len(normalizeKBIDSelection(request.SourceIDs)) == 1 {
		item.SourceID = normalizeKBIDSelection(request.SourceIDs)[0]
	}
	item.AuthorizationHeader = firstNonEmptyString(item.AuthorizationHeader, request.AuthorizationHeader)
	item.ActingUserAuthorizationHeader = firstNonEmptyString(item.ActingUserAuthorizationHeader, request.ActingUserAuthorizationHeader)
	return item
}

func shapeKnowledgeBody(body string, request KBReadRequest) (string, string, bool, int, error) {
	originalBytes := len([]byte(body))
	format := strings.ToLower(strings.TrimSpace(request.Format))
	if format == "" {
		format = "markdown"
	}
	if request.HeadingsOnly {
		format = "headings"
		body = markdownHeadings(body)
	} else if section := strings.TrimSpace(request.Section); section != "" {
		var ok bool
		body, ok = markdownSection(body, section)
		if !ok {
			return "", format, false, originalBytes, codedNotFound("not_found", "knowledge section not found")
		}
	}
	switch format {
	case "markdown", "headings":
	case "plain":
		body = plainKnowledgeText(body)
	case "summary":
		body = summarizeKnowledgeMarkdown(body)
	default:
		return "", format, false, originalBytes, invalidArgument("knowledge read format must be markdown, plain, or summary")
	}
	if request.Offset < 0 || request.Limit < 0 || request.MaxBytes < 0 {
		return "", format, false, originalBytes, invalidArgument("knowledge read range values must be non-negative")
	}
	truncated := false
	if request.Offset > 0 || request.Limit > 0 {
		body, truncated = sliceRunes(body, request.Offset, request.Limit)
	}
	if request.MaxBytes > 0 {
		var byteTruncated bool
		body, byteTruncated = truncateUTF8Bytes(body, request.MaxBytes)
		truncated = truncated || byteTruncated
	}
	return body, format, truncated, originalBytes, nil
}

func markdownHeadings(body string) string {
	lines := strings.Split(body, "\n")
	headings := make([]string, 0)
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "#") && strings.TrimSpace(strings.TrimLeft(trimmed, "#")) != "" {
			headings = append(headings, trimmed)
		}
	}
	return strings.Join(headings, "\n")
}

func markdownSection(body, section string) (string, bool) {
	target := normalizeMarkdownHeading(section)
	lines := strings.Split(body, "\n")
	start := -1
	level := 0
	for i, line := range lines {
		currentLevel, title, ok := parseMarkdownHeading(line)
		if !ok {
			continue
		}
		if normalizeMarkdownHeading(title) == target {
			start = i
			level = currentLevel
			break
		}
	}
	if start < 0 {
		return "", false
	}
	end := len(lines)
	for i := start + 1; i < len(lines); i++ {
		currentLevel, _, ok := parseMarkdownHeading(lines[i])
		if ok && currentLevel <= level {
			end = i
			break
		}
	}
	return strings.TrimSpace(strings.Join(lines[start:end], "\n")), true
}

func parseMarkdownHeading(line string) (int, string, bool) {
	trimmed := strings.TrimSpace(line)
	if !strings.HasPrefix(trimmed, "#") {
		return 0, "", false
	}
	level := 0
	for level < len(trimmed) && trimmed[level] == '#' {
		level++
	}
	if level == 0 || level > 6 || level >= len(trimmed) || trimmed[level] != ' ' {
		return 0, "", false
	}
	title := strings.TrimSpace(trimmed[level:])
	if title == "" {
		return 0, "", false
	}
	return level, title, true
}

func normalizeMarkdownHeading(raw string) string {
	raw = strings.TrimSpace(strings.TrimLeft(strings.TrimSpace(raw), "#"))
	raw = strings.Trim(raw, " \t-_:")
	return strings.ToLower(raw)
}

func plainKnowledgeText(body string) string {
	body = strings.ReplaceAll(body, "\r\n", "\n")
	lines := strings.Split(body, "\n")
	out := make([]string, 0, len(lines))
	inFence := false
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "```") {
			inFence = !inFence
			continue
		}
		if !inFence {
			if _, title, ok := parseMarkdownHeading(line); ok {
				line = title
			}
			line = strings.TrimPrefix(strings.TrimSpace(line), "- ")
			line = strings.TrimPrefix(line, "* ")
			line = strings.ReplaceAll(line, "**", "")
			line = strings.ReplaceAll(line, "__", "")
			line = stripSimpleHTMLTags(line)
		}
		out = append(out, strings.TrimSpace(line))
	}
	return strings.TrimSpace(strings.Join(out, "\n"))
}

func stripSimpleHTMLTags(raw string) string {
	var b strings.Builder
	inTag := false
	for _, r := range raw {
		switch r {
		case '<':
			inTag = true
		case '>':
			inTag = false
		default:
			if !inTag {
				b.WriteRune(r)
			}
		}
	}
	return b.String()
}

func summarizeKnowledgeMarkdown(body string) string {
	headings := markdownHeadings(body)
	plain := plainKnowledgeText(body)
	parts := make([]string, 0, 2)
	if strings.TrimSpace(headings) != "" {
		parts = append(parts, "Headings:\n"+headings)
	}
	for _, para := range strings.Split(plain, "\n\n") {
		para = strings.TrimSpace(para)
		if para != "" && !strings.HasPrefix(para, "#") {
			parts = append(parts, "Excerpt:\n"+para)
			break
		}
	}
	summary := strings.Join(parts, "\n\n")
	if summary == "" {
		summary = plain
	}
	summary, _ = truncateUTF8Bytes(summary, 4000)
	return summary
}

func sliceRunes(raw string, offset, limit int) (string, bool) {
	runes := []rune(raw)
	if offset >= len(runes) {
		return "", offset > 0
	}
	end := len(runes)
	if limit > 0 && offset+limit < end {
		end = offset + limit
	}
	return string(runes[offset:end]), end < len(runes)
}

func truncateUTF8Bytes(raw string, maxBytes int) (string, bool) {
	if maxBytes <= 0 || len([]byte(raw)) <= maxBytes {
		return raw, false
	}
	var b strings.Builder
	for _, r := range raw {
		if b.Len()+len(string(r)) > maxBytes {
			break
		}
		b.WriteRune(r)
	}
	return b.String(), true
}

func structuredKBError(err error) *KBStructuredError {
	if err == nil {
		return nil
	}
	return &KBStructuredError{Code: firstNonEmptyString(Code(err), fallbackKBErrorCode(err)), Message: err.Error(), Status: HTTPStatus(err)}
}

func StructuredKBError(err error) *KBStructuredError {
	return structuredKBError(err)
}

func fallbackKBErrorCode(err error) string {
	switch {
	case errors.Is(err, ErrNotFound):
		return "not_found"
	case errors.Is(err, ErrForbidden), errors.Is(err, ErrUnauthorized):
		return "permission_denied"
	case errors.Is(err, ErrInvalidArgument):
		if strings.Contains(strings.ToLower(err.Error()), "not ready") {
			return "index_not_ready"
		}
		return "invalid_argument"
	case errors.Is(err, ErrConflict):
		return "conflict"
	default:
		return "upstream_error"
	}
}

func containsString(values []string, target string) bool {
	target = strings.TrimSpace(target)
	for _, value := range values {
		if strings.TrimSpace(value) == target {
			return true
		}
	}
	return false
}

type kbCitationTokenPayload struct {
	Version         int    `json:"v"`
	KnowledgeBaseID string `json:"kb"`
	SourceID        string `json:"src"`
	Slug            string `json:"slug"`
	Revision        string `json:"rev,omitempty"`
	ExpiresAt       int64  `json:"exp"`
}

type parsedKBCitationID struct {
	CitationURI string
}

func normalizeKBCitationTokenKey(key []byte) []byte {
	if len(key) > 0 {
		return append([]byte(nil), key...)
	}
	generated := make([]byte, 32)
	if _, err := rand.Read(generated); err == nil {
		return generated
	}
	fallback := sha256.Sum256([]byte("agentteam-kb-citation-token-fallback"))
	return fallback[:]
}

func (s *KBService) kbCitationID(kbID, sourceID, slug, revision string) string {
	payload := kbCitationTokenPayload{
		Version:         1,
		KnowledgeBaseID: strings.TrimSpace(kbID),
		SourceID:        strings.TrimSpace(sourceID),
		Slug:            strings.TrimSpace(slug),
		Revision:        strings.TrimSpace(revision),
		ExpiresAt:       s.now().Add(kbCitationTokenTTL).Unix(),
	}
	encodedPayload, err := json.Marshal(payload)
	if err != nil {
		return ""
	}
	signature := signKBCitationToken(s.citationTokenKey, encodedPayload)
	return "cit_" + base64.RawURLEncoding.EncodeToString(encodedPayload) + "." + base64.RawURLEncoding.EncodeToString(signature)
}

func (s *KBService) parseKBCitationID(raw string) (parsedKBCitationID, error) {
	raw = strings.TrimSpace(raw)
	if !strings.HasPrefix(raw, "cit_") {
		return parsedKBCitationID{}, invalidArgument("citation id is invalid")
	}
	parts := strings.Split(strings.TrimPrefix(raw, "cit_"), ".")
	if len(parts) != 2 {
		return parsedKBCitationID{}, invalidArgument("citation id is invalid")
	}
	payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
	if err != nil {
		return parsedKBCitationID{}, invalidArgument("citation id is invalid")
	}
	signature, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return parsedKBCitationID{}, invalidArgument("citation id is invalid")
	}
	if !hmac.Equal(signature, signKBCitationToken(s.citationTokenKey, payloadBytes)) {
		return parsedKBCitationID{}, invalidArgument("citation id is invalid")
	}
	var payload kbCitationTokenPayload
	if err := json.Unmarshal(payloadBytes, &payload); err != nil || payload.Version != 1 {
		return parsedKBCitationID{}, invalidArgument("citation id is invalid")
	}
	if payload.ExpiresAt <= 0 || !s.now().Before(time.Unix(payload.ExpiresAt, 0)) {
		return parsedKBCitationID{}, invalidArgument("citation id is stale")
	}
	citationURI := kbCitationURI(payload.KnowledgeBaseID, payload.SourceID, payload.Slug, payload.Revision)
	if _, err := parseKBCitationURI(citationURI); err != nil {
		return parsedKBCitationID{}, err
	}
	return parsedKBCitationID{CitationURI: citationURI}, nil
}

func signKBCitationToken(key []byte, payload []byte) []byte {
	mac := hmac.New(sha256.New, key)
	_, _ = mac.Write(payload)
	return mac.Sum(nil)
}

func kbCitationURI(kbID, sourceID, slug, revision string) string {
	raw := fmt.Sprintf("kb://%s/sources/%s/wiki/%s", strings.TrimSpace(kbID), url.PathEscape(strings.TrimSpace(sourceID)), url.PathEscape(strings.TrimSpace(slug)))
	if strings.TrimSpace(revision) == "" {
		return raw
	}
	query := url.Values{}
	query.Set("rev", strings.TrimSpace(revision))
	return raw + "?" + query.Encode()
}

func kbCitationRoute(kbID string, source KBRetrievalSource, slug, revision string) KBCitationRoute {
	return KBCitationRoute{
		KnowledgeBaseID: strings.TrimSpace(kbID),
		SourceID:        strings.TrimSpace(source.ID),
		RepoID:          firstNonEmptyString(strings.TrimSpace(source.RepoID), strings.TrimSpace(source.ResourceID)),
		RepoRole:        strings.TrimSpace(source.RepoRole),
		Kind:            "wiki",
		Slug:            strings.TrimSpace(slug),
		Revision:        strings.TrimSpace(revision),
	}
}

func (s *KBService) auditRetrieval(ctx context.Context, workspaceID, kbID, actor, action, resourceType, resourceID, summary string) error {
	if s == nil || s.store == nil {
		return nil
	}
	audit := s.auditEvent(workspaceID, kbID, actor, action, resourceType, resourceID, summary, "")
	if _, err := s.store.CreateAuditEvent(ctx, audit); err != nil {
		return err
	}
	return s.writeKnowledgeBaseAuditActivity(ctx, audit)
}

type parsedKBCitation struct {
	KnowledgeBaseID string
	SourceID        string
	Slug            string
	Revision        string
}

func parseKBCitationURI(raw string) (parsedKBCitation, error) {
	uri, err := url.Parse(strings.TrimSpace(raw))
	if err != nil {
		return parsedKBCitation{}, invalidArgument("knowledge citation uri is invalid")
	}
	if uri.Scheme != "kb" || strings.TrimSpace(uri.Host) == "" {
		return parsedKBCitation{}, invalidArgument("knowledge citation uri is invalid")
	}
	parts := strings.Split(strings.TrimPrefix(uri.EscapedPath(), "/"), "/")
	if len(parts) < 4 || parts[0] != "sources" || parts[2] != "wiki" {
		return parsedKBCitation{}, invalidArgument("knowledge citation uri is invalid")
	}
	sourceID, err := url.PathUnescape(parts[1])
	if err != nil {
		return parsedKBCitation{}, invalidArgument("knowledge citation uri source is invalid")
	}
	slug, err := url.PathUnescape(strings.Join(parts[3:], "/"))
	if err != nil {
		return parsedKBCitation{}, invalidArgument("knowledge citation uri slug is invalid")
	}
	if strings.TrimSpace(sourceID) == "" || strings.TrimSpace(slug) == "" {
		return parsedKBCitation{}, invalidArgument("knowledge citation uri is invalid")
	}
	return parsedKBCitation{
		KnowledgeBaseID: strings.TrimSpace(uri.Host),
		SourceID:        strings.TrimSpace(sourceID),
		Slug:            strings.TrimSpace(slug),
		Revision:        strings.TrimSpace(uri.Query().Get("rev")),
	}, nil
}
