package kb

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"path"
	"sort"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	contentservice "github.com/tidbcloud/AgentTeam/internal/service/content"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	contentstore "github.com/tidbcloud/AgentTeam/internal/store/content"
	storekb "github.com/tidbcloud/AgentTeam/internal/store/kb"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

const (
	kbSourceContentDomain               = "kb_source"
	kbSourceMigrationOperationNamespace = "kb.source-migration"
	kbSourceMigrationOperationVersion   = uint64(1)
	KBSourceMigrationMaxFiles           = 2_000
	KBSourceMigrationMaxFileBytes       = int64(16 << 20)
	KBSourceMigrationMaxTotalBytes      = int64(64 << 20)
	KBSourceMigrationMaxManifestBytes   = int64(8 << 20)
	KBSourceMigrationMaxRunDuration     = 2 * time.Minute
)

type KBSourceContentMigrationStore interface {
	GetSource(context.Context, string, string, string) (storekb.Source, error)
	GetSourceRevision(context.Context, string, string, string, string) (storekb.SourceRevision, error)
	GetSourceContentMapping(context.Context, string, string) (storekb.SourceContentMapping, error)
	ProjectSourceContentMapping(context.Context, storekb.ProjectSourceContentMappingParams) (storekb.SourceContentMapping, error)
}

type KBSourceMigrationRepositoryStore interface {
	Get(context.Context, string, string, string, string) (reposstore.ResourceRepository, error)
}

type KBSourceMigrationAuthorizer interface {
	CanMigrateKBSource(context.Context, string, string, string, string) (bool, error)
}

type KBSourceMigrationContentStore interface {
	FindContentByBinding(context.Context, string, string, string) (contentservice.Content, bool, error)
	CreateContent(context.Context, contentservice.CreateContentRequest) (contentservice.Content, error)
	GetContent(context.Context, string, string) (contentservice.Content, error)
	GetVersion(context.Context, string, string, string) (contentservice.Version, error)
	FindVersionOperation(context.Context, string, string, string) (contentservice.VersionOperationResult, bool, error)
	CommitVersion(context.Context, contentservice.CommitVersionRequest) (contentservice.CommitVersionResult, error)
}

type KBSourceContentMigrationOptions struct {
	Enabled      bool
	Store        KBSourceContentMigrationStore
	Repositories KBSourceMigrationRepositoryStore
	Authorizer   KBSourceMigrationAuthorizer
	Content      KBSourceMigrationContentStore
	Exporter     KBSourceContentExporter
	NewID        contentservice.IDGenerator
	Now          func() time.Time
}

// KBSourceContentMigrationService performs one explicitly requested, bounded
// best-effort import. It owns no background queue, durable retry cursor, writer
// drain, or AGS mutation path. Content Store operation identity plus the exact
// legacy mapping make a failed run safe to invoke again.
type KBSourceContentMigrationService struct {
	enabled      bool
	store        KBSourceContentMigrationStore
	repositories KBSourceMigrationRepositoryStore
	authorizer   KBSourceMigrationAuthorizer
	content      KBSourceMigrationContentStore
	exporter     KBSourceContentExporter
	newID        contentservice.IDGenerator
	now          func() time.Time
	semaphore    chan struct{}
}

func NewKBSourceContentMigrationService(options KBSourceContentMigrationOptions) (*KBSourceContentMigrationService, error) {
	if options.Store == nil || options.Repositories == nil || options.Authorizer == nil || options.Content == nil || options.Exporter == nil || options.NewID == nil {
		return nil, fmt.Errorf("KB source content migration dependencies are required")
	}
	now := options.Now
	if now == nil {
		now = func() time.Time { return time.Now().UTC() }
	}
	return &KBSourceContentMigrationService{
		enabled: options.Enabled, store: options.Store, repositories: options.Repositories,
		authorizer: options.Authorizer, content: options.Content, exporter: options.Exporter,
		newID: options.NewID, now: now, semaphore: make(chan struct{}, 1),
	}, nil
}

type KBSourceContentMigrationRequest struct {
	WorkspaceID      string
	KnowledgeBaseID  string
	SourceID         string
	SourceRevisionID string
	RepositoryRole   string
	RepositoryID     string
	CanonicalBranch  string
	CommitSHA        string
	ExpectedDigest   string
	// ExpectedHeadVersionID orders historical imports on one source lineage.
	// The first import uses an empty head; each later import names the exact
	// ContentVersion returned by the previous ordered import.
	ExpectedHeadVersionID string
	Actor                 string
	Limits                contentpayload.Limits
}

type KBSourceContentMigrationResult struct {
	ContentID        string
	ContentVersionID string
	RootDigest       string
	FileCount        int
	SizeBytes        int64
	Replayed         bool
}

// RunExact imports one caller-selected SourceRevision. Deployment may
// perform a full pass and one later delta pass by invoking this method for each
// bounded target; it never scans a workspace or waits for AGS to quiesce.
func (service *KBSourceContentMigrationService) RunExact(ctx context.Context, request KBSourceContentMigrationRequest) (KBSourceContentMigrationResult, error) {
	if service == nil || !service.enabled {
		return KBSourceContentMigrationResult{}, invalidArgument("KB source content migration is disabled")
	}
	request, err := normalizeKBSourceContentMigrationRequest(request)
	if err != nil {
		return KBSourceContentMigrationResult{}, err
	}
	select {
	case service.semaphore <- struct{}{}:
		defer func() { <-service.semaphore }()
	default:
		return KBSourceContentMigrationResult{}, conflict("KB source content migration concurrency limit reached")
	}
	runCtx, cancel := context.WithTimeout(ctx, KBSourceMigrationMaxRunDuration)
	defer cancel()
	runCtx = kbSourceContentContext(runCtx, request.Actor, request.WorkspaceID, request.SourceID, "",
		contentservice.AccessCreateLineage, contentservice.AccessReadMetadata, contentservice.AccessPublish)
	if err := service.authorize(runCtx, request); err != nil {
		return KBSourceContentMigrationResult{}, err
	}
	target, err := service.requireExactTarget(runCtx, request)
	if err != nil {
		return KBSourceContentMigrationResult{}, err
	}

	if mapping, err := service.store.GetSourceContentMapping(runCtx, request.RepositoryID, request.CommitSHA); err == nil {
		if !mappingMatchesKBSourceRequest(mapping, request) {
			return KBSourceContentMigrationResult{}, conflict("KB source content mapping changed")
		}
		published := publishedKBSourceContent{
			ContentID: mapping.ContentID, VersionID: mapping.ContentVersionID, RootDigest: mapping.RootDigest,
			FileCount: mapping.FileCount, SizeBytes: mapping.SizeBytes, SourceCommittedAt: mapping.SourceCommittedAt,
		}
		if err := service.verifyPublishedVersion(runCtx, request, published); err != nil {
			return KBSourceContentMigrationResult{}, err
		}
		if !sourceContentMappingHasSearchProjection(mapping) {
			return KBSourceContentMigrationResult{}, conflict("KB source content search projection is incomplete")
		}
		return kbSourceContentMigrationResult(published, true), nil
	} else if !errors.Is(err, storekb.ErrSourceContentMappingNotFound) {
		return KBSourceContentMigrationResult{}, err
	}

	exported, err := service.exporter.ExportExactKBSource(runCtx, KBSourceContentExportRequest{
		Repository: repos.IdentityFromStore(target.repository), CanonicalBranch: request.CanonicalBranch,
		CommitSHA: request.CommitSHA, Limits: request.Limits,
	})
	if err != nil {
		return KBSourceContentMigrationResult{}, err
	}
	if request.ExpectedDigest != "" && !strings.EqualFold(request.ExpectedDigest, exported.RootDigest) {
		return KBSourceContentMigrationResult{}, conflict("KB source content migration digest changed")
	}
	searchProjection := buildKBSourceSearchProjection(exported.Files)
	if err := service.authorize(runCtx, request); err != nil {
		return KBSourceContentMigrationResult{}, err
	}
	if _, err := service.requireExactTarget(runCtx, request); err != nil {
		return KBSourceContentMigrationResult{}, err
	}
	published, replayed, err := service.publish(runCtx, request, exported)
	if err != nil {
		return KBSourceContentMigrationResult{}, err
	}
	if err := service.verifyPublishedVersion(runCtx, request, published); err != nil {
		return KBSourceContentMigrationResult{}, err
	}
	if err := service.authorize(runCtx, request); err != nil {
		return KBSourceContentMigrationResult{}, err
	}
	if _, err := service.requireExactTarget(runCtx, request); err != nil {
		return KBSourceContentMigrationResult{}, err
	}
	mappingID, err := service.newID("kbcm")
	if err != nil {
		return KBSourceContentMigrationResult{}, fmt.Errorf("generate KB source content mapping id: %w", err)
	}
	if _, err := service.project(runCtx, request, mappingID, published, searchProjection); err != nil {
		return KBSourceContentMigrationResult{}, err
	}
	return kbSourceContentMigrationResult(published, replayed), nil
}

func kbSourceContentContext(ctx context.Context, actor, workspaceID, sourceID, contentID string, actions ...contentservice.AccessAction) context.Context {
	return kbSourceContentPrincipalContext(ctx, actor, kbContentSubjectUser, workspaceID, sourceID, contentID, actions...)
}

type exactKBSourceMigrationTarget struct {
	repository reposstore.ResourceRepository
}

func (service *KBSourceContentMigrationService) requireExactTarget(ctx context.Context, request KBSourceContentMigrationRequest) (exactKBSourceMigrationTarget, error) {
	source, err := service.store.GetSource(ctx, request.WorkspaceID, request.KnowledgeBaseID, request.SourceID)
	if err != nil {
		return exactKBSourceMigrationTarget{}, mapStoreNotFound(err)
	}
	revision, err := service.store.GetSourceRevision(ctx, request.WorkspaceID, request.KnowledgeBaseID, request.SourceID, request.SourceRevisionID)
	if err != nil {
		return exactKBSourceMigrationTarget{}, mapStoreNotFound(err)
	}
	repository, err := service.repositories.Get(ctx, request.WorkspaceID, reposstore.ResourceTypeKBSource, request.SourceID, request.RepositoryRole)
	if err != nil {
		return exactKBSourceMigrationTarget{}, mapStoreNotFound(err)
	}
	branch := firstNonEmptyString(repository.DefaultRef, source.DefaultRef)
	if revision.ID != request.SourceRevisionID || !strings.EqualFold(strings.TrimSpace(revision.SourceCommitSHA), request.CommitSHA) ||
		strings.TrimSpace(repository.AGSRepoID) != request.RepositoryID || strings.TrimSpace(branch) != request.CanonicalBranch ||
		repository.WorkspaceID != request.WorkspaceID || repository.ResourceID != request.SourceID || repository.ResourceType != reposstore.ResourceTypeKBSource {
		return exactKBSourceMigrationTarget{}, conflict("KB source content migration exact target changed")
	}
	if (revision.SourceContentID == "") != (revision.SourceContentVersionID == "") {
		return exactKBSourceMigrationTarget{}, conflict("KB source content projection is partial")
	}
	return exactKBSourceMigrationTarget{repository: repository}, nil
}

func (service *KBSourceContentMigrationService) authorize(ctx context.Context, request KBSourceContentMigrationRequest) error {
	allowed, err := service.authorizer.CanMigrateKBSource(ctx, request.WorkspaceID, request.KnowledgeBaseID, request.SourceID, request.Actor)
	if err != nil {
		return fmt.Errorf("authorize KB source content migration: %w", err)
	}
	if !allowed {
		return forbidden("KB source content migration requires manage access")
	}
	return nil
}

type publishedKBSourceContent struct {
	ContentID         string
	VersionID         string
	RootDigest        string
	FileCount         int
	SizeBytes         int64
	SourceCommittedAt time.Time
}

type kbSourceSearchProjection struct {
	Documents []storekb.SourceSearchDocumentInput
	Digest    string
	TextBytes int64
	Truncated bool
}

func (service *KBSourceContentMigrationService) publish(ctx context.Context, request KBSourceContentMigrationRequest, exported KBSourceContentExport) (publishedKBSourceContent, bool, error) {
	content, found, err := service.content.FindContentByBinding(ctx, request.WorkspaceID, kbSourceContentDomain, request.SourceID)
	if err != nil {
		return publishedKBSourceContent{}, false, err
	}
	if !found {
		if request.ExpectedHeadVersionID != "" {
			return publishedKBSourceContent{}, false, conflict("KB source content expected head changed")
		}
		content, err = service.content.CreateContent(ctx, contentservice.CreateContentRequest{
			WorkspaceID: request.WorkspaceID, Domain: kbSourceContentDomain, BindingKey: request.SourceID,
			Model: contentservice.TreeModel, Actor: request.Actor,
		})
		if err != nil {
			if !contentservice.IsBindingConflict(err) {
				return publishedKBSourceContent{}, false, err
			}
			content, found, err = service.content.FindContentByBinding(ctx, request.WorkspaceID, kbSourceContentDomain, request.SourceID)
			if err != nil || !found {
				return publishedKBSourceContent{}, false, errors.Join(err, contentstore.ErrBindingConflict)
			}
		}
	}
	if content.Model != contentservice.TreeModel {
		return publishedKBSourceContent{}, false, conflict("KB source content binding model changed")
	}
	operationKey := kbSourceMigrationOperationKey(request)
	receipt, found, err := service.content.FindVersionOperation(ctx, request.WorkspaceID, content.ID, operationKey)
	if err != nil {
		return publishedKBSourceContent{}, false, err
	}
	if found {
		if receipt.OperationNamespace != kbSourceMigrationOperationNamespace || receipt.OperationVersion != kbSourceMigrationOperationVersion ||
			!strings.EqualFold(receipt.RootDigest, exported.RootDigest) || receipt.FileCount != len(exported.Files) || receipt.SizeBytes != exported.SizeBytes {
			return publishedKBSourceContent{}, false, conflict("KB source content operation receipt changed")
		}
		return publishedKBSourceContent{ContentID: content.ID, VersionID: receipt.VersionID, RootDigest: receipt.RootDigest, FileCount: receipt.FileCount, SizeBytes: receipt.SizeBytes, SourceCommittedAt: exported.SourceCommittedAt}, true, nil
	}
	if content.HeadVersionID != request.ExpectedHeadVersionID {
		return publishedKBSourceContent{}, false, conflict("KB source content expected head changed")
	}
	files := make([]contentpayload.File, 0, len(exported.Files))
	for _, file := range exported.Files {
		files = append(files, contentpayload.File{Path: file.Path, Size: int64(len(file.Data)), Executable: file.Executable, Content: bytes.NewReader(file.Data)})
	}
	committed, err := service.content.CommitVersion(ctx, contentservice.CommitVersionRequest{
		WorkspaceID: request.WorkspaceID, ContentID: content.ID, ExpectedHeadVersionID: request.ExpectedHeadVersionID,
		ExpectedGeneration: content.Generation, Actor: request.Actor, Model: contentservice.TreeModel, Checkpoint: true,
		Files: files, TreeLimits: request.Limits, OperationKey: operationKey,
		OperationNamespace: kbSourceMigrationOperationNamespace, OperationVersion: kbSourceMigrationOperationVersion,
	})
	if err != nil {
		return publishedKBSourceContent{}, false, err
	}
	if !strings.EqualFold(committed.Version.RootDigest, exported.RootDigest) || committed.Version.FileCount != len(exported.Files) || committed.Version.SizeBytes != exported.SizeBytes {
		return publishedKBSourceContent{}, false, conflict("KB source content publication digest changed")
	}
	return publishedKBSourceContent{ContentID: content.ID, VersionID: committed.Version.ID, RootDigest: committed.Version.RootDigest, FileCount: committed.Version.FileCount, SizeBytes: committed.Version.SizeBytes, SourceCommittedAt: exported.SourceCommittedAt}, false, nil
}

func (service *KBSourceContentMigrationService) verifyPublishedVersion(ctx context.Context, request KBSourceContentMigrationRequest, published publishedKBSourceContent) error {
	content, err := service.content.GetContent(ctx, request.WorkspaceID, published.ContentID)
	if err != nil {
		return err
	}
	if content.Domain != kbSourceContentDomain || content.BindingKey != request.SourceID || content.Model != contentservice.TreeModel {
		return conflict("KB source content binding changed")
	}
	version, err := service.content.GetVersion(ctx, request.WorkspaceID, published.ContentID, published.VersionID)
	if err != nil {
		return err
	}
	if version.ParentVersionID != request.ExpectedHeadVersionID || !strings.EqualFold(version.RootDigest, published.RootDigest) || version.FileCount != published.FileCount || version.SizeBytes != published.SizeBytes {
		return conflict("KB source content version changed")
	}
	return nil
}

func (service *KBSourceContentMigrationService) project(ctx context.Context, request KBSourceContentMigrationRequest, mappingID string, published publishedKBSourceContent, search kbSourceSearchProjection) (storekb.SourceContentMapping, error) {
	mapping, err := service.store.ProjectSourceContentMapping(ctx, storekb.ProjectSourceContentMappingParams{
		ID: mappingID, WorkspaceID: request.WorkspaceID, KnowledgeBaseID: request.KnowledgeBaseID,
		SourceID: request.SourceID, SourceRevisionID: request.SourceRevisionID,
		RepositoryRole: request.RepositoryRole, RepositoryID: request.RepositoryID,
		CanonicalBranch: request.CanonicalBranch, CommitSHA: request.CommitSHA,
		ContentID: published.ContentID, ContentVersionID: published.VersionID, RootDigest: published.RootDigest,
		FileCount: published.FileCount, SizeBytes: published.SizeBytes, SourceCommittedAt: published.SourceCommittedAt, VerifiedAt: service.now().UTC(),
		SearchProjectionDigest: search.Digest, SearchProjectionTruncated: search.Truncated, SearchDocuments: search.Documents,
	})
	if errors.Is(err, storekb.ErrSourceContentMigrationConflict) {
		return storekb.SourceContentMapping{}, conflict("KB source content migration target changed")
	}
	return mapping, err
}

func buildKBSourceSearchProjection(files []KBSourceContentExportFile) kbSourceSearchProjection {
	ordered := append([]KBSourceContentExportFile(nil), files...)
	sort.Slice(ordered, func(i, j int) bool { return ordered[i].Path < ordered[j].Path })
	documents := make([]storekb.SourceSearchDocumentInput, 0, min(len(ordered), storekb.SourceSearchProjectionMaxDocuments))
	remaining := storekb.SourceSearchProjectionMaxTextBytes
	truncated := false
	for _, file := range ordered {
		if !utf8.Valid(file.Data) || bytes.IndexByte(file.Data, 0) >= 0 {
			continue
		}
		if len(documents) >= storekb.SourceSearchProjectionMaxDocuments || remaining <= 0 {
			truncated = true
			continue
		}
		limit := int64(storekb.SourceSearchProjectionMaxDocumentBytes)
		if remaining < limit {
			limit = remaining
		}
		text := truncateUTF8ProjectionText(file.Data, int(limit))
		if len(text) < len(file.Data) {
			truncated = true
		}
		documents = append(documents, storekb.SourceSearchDocumentInput{
			Path: strings.Trim(strings.TrimSpace(file.Path), "/"), Title: path.Base(file.Path), SearchText: text,
		})
		remaining -= int64(len(text))
	}
	return kbSourceSearchProjection{
		Documents: documents, Digest: storekb.SourceSearchProjectionDigest(documents, truncated),
		TextBytes: storekbSourceSearchDocumentBytes(documents), Truncated: truncated,
	}
}

func truncateUTF8ProjectionText(data []byte, limit int) string {
	if limit <= 0 {
		return ""
	}
	if len(data) <= limit {
		return string(data)
	}
	end := limit
	for end > 0 && !utf8.Valid(data[:end]) {
		end--
	}
	return string(data[:end])
}

func storekbSourceSearchDocumentBytes(documents []storekb.SourceSearchDocumentInput) int64 {
	var total int64
	for _, document := range documents {
		total += int64(len(document.SearchText))
	}
	return total
}

func sourceContentMappingHasSearchProjection(mapping storekb.SourceContentMapping) bool {
	digest := strings.TrimSpace(mapping.SearchProjectionDigest)
	decoded, err := hex.DecodeString(digest)
	return err == nil && len(decoded) == sha256.Size && mapping.SearchDocumentCount >= 0 && mapping.SearchTextBytes >= 0 && mapping.SearchProjectedAt != nil
}

func normalizeKBSourceContentMigrationRequest(request KBSourceContentMigrationRequest) (KBSourceContentMigrationRequest, error) {
	request.WorkspaceID = strings.TrimSpace(request.WorkspaceID)
	request.KnowledgeBaseID = strings.TrimSpace(request.KnowledgeBaseID)
	request.SourceID = strings.TrimSpace(request.SourceID)
	request.SourceRevisionID = strings.TrimSpace(request.SourceRevisionID)
	request.RepositoryRole = strings.TrimSpace(request.RepositoryRole)
	request.RepositoryID = strings.TrimSpace(request.RepositoryID)
	request.CanonicalBranch = strings.TrimSpace(request.CanonicalBranch)
	request.CommitSHA = strings.ToLower(strings.TrimSpace(request.CommitSHA))
	request.ExpectedDigest = strings.ToLower(strings.TrimSpace(request.ExpectedDigest))
	request.ExpectedHeadVersionID = strings.TrimSpace(request.ExpectedHeadVersionID)
	request.Actor = strings.TrimSpace(request.Actor)
	limits, err := contentpayload.NormalizeLimits(request.Limits)
	if err != nil {
		return request, err
	}
	validRole := request.RepositoryRole == reposstore.RepoRolePrimary || request.RepositoryRole == reposstore.RepoRoleMirror
	validDigest := request.ExpectedDigest == ""
	if !validDigest && len(request.ExpectedDigest) == sha256.Size*2 {
		_, err = hex.DecodeString(request.ExpectedDigest)
		validDigest = err == nil
	}
	if request.WorkspaceID == "" || request.KnowledgeBaseID == "" || request.SourceID == "" || request.SourceRevisionID == "" || !validRole ||
		request.RepositoryID == "" || request.CanonicalBranch == "" || !fullKBSourceCommitSHA(request.CommitSHA) || request.Actor == "" ||
		!validDigest || limits != request.Limits || limits.MaxFiles > KBSourceMigrationMaxFiles || limits.MaxFileBytes > KBSourceMigrationMaxFileBytes ||
		limits.MaxTotalBytes > KBSourceMigrationMaxTotalBytes || limits.MaxManifestBytes > KBSourceMigrationMaxManifestBytes {
		return request, invalidArgument("KB source content migration exact target and hard bounds are invalid")
	}
	return request, nil
}

func kbSourceMigrationOperationKey(request KBSourceContentMigrationRequest) string {
	sum := sha256.Sum256([]byte(strings.Join([]string{
		request.WorkspaceID, request.KnowledgeBaseID, request.SourceID, request.SourceRevisionID,
		request.RepositoryRole, request.RepositoryID, request.CanonicalBranch, request.CommitSHA, request.ExpectedHeadVersionID,
	}, "\x00")))
	return kbSourceMigrationOperationNamespace + ":" + hex.EncodeToString(sum[:])
}

func mappingMatchesKBSourceRequest(mapping storekb.SourceContentMapping, request KBSourceContentMigrationRequest) bool {
	return mapping.WorkspaceID == request.WorkspaceID && mapping.KnowledgeBaseID == request.KnowledgeBaseID && mapping.SourceID == request.SourceID &&
		mapping.SourceRevisionID == request.SourceRevisionID && mapping.RepositoryRole == request.RepositoryRole && mapping.RepositoryID == request.RepositoryID &&
		mapping.CanonicalBranch == request.CanonicalBranch && strings.EqualFold(mapping.CommitSHA, request.CommitSHA) && mapping.VerifiedAt != nil &&
		sourceContentMappingHasSearchProjection(mapping) &&
		(request.ExpectedDigest == "" || strings.EqualFold(mapping.RootDigest, request.ExpectedDigest))
}

func kbSourceContentMigrationResult(published publishedKBSourceContent, replayed bool) KBSourceContentMigrationResult {
	return KBSourceContentMigrationResult{
		ContentID: published.ContentID, ContentVersionID: published.VersionID, RootDigest: published.RootDigest,
		FileCount: published.FileCount, SizeBytes: published.SizeBytes, Replayed: replayed,
	}
}
