package kb

import (
	"context"
	"fmt"
	"io"
	"path"
	"sort"
	"strings"
	"unicode/utf8"

	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	"github.com/tidbcloud/AgentTeam/internal/service/content"
	storekb "github.com/tidbcloud/AgentTeam/internal/store/kb"
)

const maxKBNativeTreePaths = KBSourceMigrationMaxFiles

type kbSourceContentReadStore interface {
	GetVersion(context.Context, string, string, string) (content.Version, error)
	ListVersionPaths(context.Context, string, string, string, int) ([]content.Path, error)
	OpenFile(context.Context, string, string, string, string, *contentpayload.ByteRange) (io.ReadCloser, contentpayload.BlobMeta, error)
}

type kbSourceContentSearchStore interface {
	SearchSourceContentDocuments(context.Context, string, string, string, string, string, int, int) ([]storekb.SourceSearchDocumentHit, bool, error)
}

// NativeKBSourceContentReader serves one exact immutable source version. It
// reads and searches only one exact immutable ContentVersion. Search consumes
// the bounded projection produced by the owning source importer.
type NativeKBSourceContentReader struct {
	Content kbSourceContentReadStore
	Search  kbSourceContentSearchStore
}

func (reader NativeKBSourceContentReader) SearchWiki(ctx context.Context, request KBWikiSearchRequest) (KBWikiSearchResponse, error) {
	if reader.Content == nil || reader.Search == nil {
		return KBWikiSearchResponse{}, fmt.Errorf("native KB source search projection is not configured")
	}
	if err := validateNativeKBSourceReadScope(request.PrincipalID, request.PrincipalType, request.WorkspaceID, request.KnowledgeBaseID,
		request.SourceID, request.ContentID, request.ContentVersionID, request.Revision, request.RepoRole); err != nil {
		return KBWikiSearchResponse{}, err
	}
	query := strings.TrimSpace(request.Query)
	if query == "" || request.Limit <= 0 || request.Limit > storekb.SourceSearchProjectionMaxResults || request.Offset < 0 {
		return KBWikiSearchResponse{}, invalidArgument("native KB source search query and bounds are invalid")
	}
	readCtx := kbSourceContentPrincipalContext(ctx, request.PrincipalID, request.PrincipalType, request.WorkspaceID,
		request.SourceID, request.ContentID, content.AccessReadMetadata)
	// This exact-version read revalidates live Content Store ACL before the KB
	// projection store may reveal any indexed source text.
	if _, err := reader.Content.GetVersion(readCtx, request.WorkspaceID, request.ContentID, request.ContentVersionID); err != nil {
		return KBWikiSearchResponse{}, fmt.Errorf("authorize native KB source search: %w", err)
	}
	rows, hasMore, err := reader.Search.SearchSourceContentDocuments(ctx, request.WorkspaceID, request.SourceID, request.ContentID, request.ContentVersionID, query, request.Limit, request.Offset)
	if err != nil {
		return KBWikiSearchResponse{}, err
	}
	hits := make([]KBWikiSearchHit, 0, len(rows))
	for _, row := range rows {
		score := 1.0
		lowerQuery := strings.ToLower(query)
		if strings.Contains(strings.ToLower(row.Title), lowerQuery) || strings.Contains(strings.ToLower(row.Path), lowerQuery) {
			score = 2
		}
		hits = append(hits, KBWikiSearchHit{
			Slug: row.Path, Title: row.Title, Snippet: nativeKBSearchSnippet(row.SearchText, query),
			SHA: strings.TrimSpace(request.ContentVersionID), Score: score,
		})
	}
	return KBWikiSearchResponse{Results: hits, Total: len(hits), HasMore: hasMore}, nil
}

func (reader NativeKBSourceContentReader) ReadWikiPage(ctx context.Context, request KBWikiReadRequest) (KBWikiPage, error) {
	if reader.Content == nil {
		return KBWikiPage{}, fmt.Errorf("native KB source content reader is not configured")
	}
	if err := validateNativeKBSourceReadScope(request.PrincipalID, request.PrincipalType, request.WorkspaceID, request.KnowledgeBaseID,
		request.SourceID, request.ContentID, request.ContentVersionID, request.Revision, request.RepoRole); err != nil {
		return KBWikiPage{}, err
	}
	filePath, err := normalizeRepositoryRelativePath(request.Slug, false)
	if err != nil {
		return KBWikiPage{}, err
	}
	readCtx := kbSourceContentPrincipalContext(ctx, request.PrincipalID, request.PrincipalType, request.WorkspaceID,
		request.SourceID, request.ContentID, content.AccessReadPayload)
	body, meta, err := reader.Content.OpenFile(readCtx, request.WorkspaceID, request.ContentID, request.ContentVersionID, filePath, nil)
	if err != nil {
		return KBWikiPage{}, fmt.Errorf("read native KB source file: %w", err)
	}
	defer body.Close()
	if meta.SizeBytes < 0 || meta.SizeBytes > maxKBRepositoryFileBytes {
		return KBWikiPage{}, invalidArgument(fmt.Sprintf("repository file exceeds %d bytes", maxKBRepositoryFileBytes))
	}
	data, err := io.ReadAll(io.LimitReader(body, maxKBRepositoryFileBytes+1))
	if err != nil {
		return KBWikiPage{}, fmt.Errorf("read native KB source file body: %w", err)
	}
	if len(data) > maxKBRepositoryFileBytes {
		return KBWikiPage{}, invalidArgument(fmt.Sprintf("repository file exceeds %d bytes", maxKBRepositoryFileBytes))
	}
	if !utf8.Valid(data) {
		return KBWikiPage{}, invalidArgument("repository file is not UTF-8 text")
	}
	return KBWikiPage{
		Slug: filePath, Title: path.Base(filePath), Body: string(data), SHA: strings.TrimSpace(request.ContentVersionID),
	}, nil
}

func (reader NativeKBSourceContentReader) ListWikiTree(ctx context.Context, request KBWikiTreeRequest) ([]KBWikiTreeEntry, error) {
	if reader.Content == nil {
		return nil, fmt.Errorf("native KB source content reader is not configured")
	}
	if err := validateNativeKBSourceReadScope(request.PrincipalID, request.PrincipalType, request.WorkspaceID, request.KnowledgeBaseID,
		request.SourceID, request.ContentID, request.ContentVersionID, request.Revision, request.RepoRole); err != nil {
		return nil, err
	}
	treePath, err := normalizeRepositoryRelativePath(request.Path, true)
	if err != nil {
		return nil, err
	}
	readCtx := kbSourceContentPrincipalContext(ctx, request.PrincipalID, request.PrincipalType, request.WorkspaceID,
		request.SourceID, request.ContentID, content.AccessReadPayload)
	paths, err := reader.Content.ListVersionPaths(readCtx, request.WorkspaceID, request.ContentID, request.ContentVersionID, maxKBNativeTreePaths)
	if err != nil {
		return nil, fmt.Errorf("list native KB source version paths: %w", err)
	}
	entries := nativeKBImmediateTree(paths, treePath, request.ContentVersionID)
	if treePath != "" && len(entries) == 0 {
		return nil, codedNotFound("not_found", "repository directory not found")
	}
	return entries, nil
}

func validateNativeKBSourceReadScope(principalID, principalType, workspaceID, knowledgeBaseID, sourceID, contentID, contentVersionID, revision, repoRole string) error {
	principalType = strings.TrimSpace(principalType)
	if strings.TrimSpace(principalID) == "" || (principalType != kbContentSubjectUser && principalType != kbContentSubjectAgent) ||
		strings.TrimSpace(workspaceID) == "" || strings.TrimSpace(knowledgeBaseID) == "" || strings.TrimSpace(sourceID) == "" ||
		strings.TrimSpace(contentID) == "" || strings.TrimSpace(contentVersionID) == "" || !kbRepositoryFileRole(repoRole) {
		return invalidArgument("native KB source content scope is incomplete")
	}
	if revision = strings.TrimSpace(revision); revision != "" && revision != strings.TrimSpace(contentVersionID) {
		return invalidArgument("native KB source revision must name the exact ContentVersion")
	}
	return nil
}

func kbSourceContentPrincipalContext(ctx context.Context, subject, subjectType, workspaceID, sourceID, contentID string, actions ...content.AccessAction) context.Context {
	return content.WithAccessPrincipal(ctx, content.AccessPrincipal{
		Subject: strings.TrimSpace(subject), SubjectType: strings.TrimSpace(subjectType),
		WorkspaceID: strings.TrimSpace(workspaceID), Domain: kbSourceContentDomain,
		BindingKey: strings.TrimSpace(sourceID), ContentID: strings.TrimSpace(contentID), Actions: actions,
	})
}

func nativeKBImmediateTree(rows []content.Path, treePath, versionID string) []KBWikiTreeEntry {
	treePath = strings.Trim(strings.TrimSpace(treePath), "/")
	prefix := ""
	if treePath != "" {
		prefix = treePath + "/"
	}
	entries := make(map[string]KBWikiTreeEntry)
	for _, row := range rows {
		filePath := strings.Trim(strings.TrimSpace(row.Path), "/")
		if filePath == "" || !strings.HasPrefix(filePath, prefix) {
			continue
		}
		remainder := strings.TrimPrefix(filePath, prefix)
		if remainder == "" {
			continue
		}
		name, _, nested := strings.Cut(remainder, "/")
		entryPath := path.Join(treePath, name)
		if nested {
			entries[entryPath] = KBWikiTreeEntry{
				Path: entryPath, Name: name, Kind: "directory", Title: entryPath, IsDir: true, SHA: strings.TrimSpace(versionID),
			}
			continue
		}
		entries[entryPath] = KBWikiTreeEntry{
			Path: entryPath, Name: name, Kind: "file", Slug: entryPath, Title: entryPath,
			SHA: strings.TrimSpace(versionID), Size: row.SizeBytes,
		}
	}
	out := make([]KBWikiTreeEntry, 0, len(entries))
	for _, entry := range entries {
		out = append(out, entry)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
	return out
}

func nativeKBSearchSnippet(text, query string) string {
	const maxRunes = 240
	text = strings.TrimSpace(text)
	runes := []rune(text)
	if len(runes) <= maxRunes {
		return string(runes)
	}
	start := 0
	lowerText := strings.ToLower(text)
	if byteIndex := strings.Index(lowerText, strings.ToLower(strings.TrimSpace(query))); byteIndex >= 0 {
		start = utf8.RuneCountInString(lowerText[:byteIndex]) - maxRunes/3
		if start < 0 {
			start = 0
		}
	}
	end := start + maxRunes
	if end > len(runes) {
		end = len(runes)
		start = end - maxRunes
	}
	return string(runes[start:end])
}
