package kb

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	contentservice "github.com/tidbcloud/AgentTeam/internal/service/content"
	"github.com/tidbcloud/AgentTeam/internal/store/content"
	storekb "github.com/tidbcloud/AgentTeam/internal/store/kb"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"gorm.io/gorm"
)

const kbSourceNativeActivationMaxDuration = 2 * time.Minute

type KBSourceNativeActivationStore interface {
	GetSource(context.Context, string, string, string) (storekb.Source, error)
	GetCurrentSourceRevision(context.Context, string, string, string) (storekb.SourceRevision, error)
	GetSourceMirror(context.Context, string, string, string) (storekb.SourceMirror, error)
	ListActiveSourceJobsForSources(context.Context, string, string, []string, []string) ([]storekb.Job, error)
	ListSourceContentMappingsForRevisions(context.Context, string, []string) ([]storekb.SourceContentMapping, error)
}

type KBSourceNativeAuthorityStore interface {
	GetAuthority(context.Context, string, string, string) (content.Authority, bool, error)
	ActivateNativeAuthority(context.Context, content.ActivateNativeAuthorityParams) (content.Authority, error)
}

type KBSourceNativeActivationOptions struct {
	Enabled         bool
	Store           KBSourceNativeActivationStore
	Repositories    KBSourceMigrationRepositoryStore
	Authorizer      KBSourceMigrationAuthorizer
	Content         KBSourceMigrationContentStore
	LegacyDashboard UADashboardRepository
	NativeDashboard UADashboardRepository
	Authority       KBSourceNativeAuthorityStore
	DashboardLimits UADashboardImportLimits
	Now             func() time.Time
}

// KBSourceNativeActivationService is the source-scoped cutover owner for the
// KB/KQA/browser/wiki/UA group. It performs no discovery or background work:
// one invocation validates one exact current source, optionally copies the
// exact five legacy Dashboard files, and then flips the one shared authority
// row. Native publication may safely precede activation because it is not
// addressable by product routing until the checkpoint is committed.
type KBSourceNativeActivationService struct {
	enabled         bool
	store           KBSourceNativeActivationStore
	repositories    KBSourceMigrationRepositoryStore
	authorizer      KBSourceMigrationAuthorizer
	content         KBSourceMigrationContentStore
	legacyDashboard UADashboardRepository
	nativeDashboard UADashboardRepository
	authority       KBSourceNativeAuthorityStore
	dashboardLimits UADashboardImportLimits
	now             func() time.Time
	semaphore       chan struct{}
}

func NewKBSourceNativeActivationService(options KBSourceNativeActivationOptions) (*KBSourceNativeActivationService, error) {
	if options.Store == nil || options.Authorizer == nil || options.Content == nil || options.NativeDashboard == nil || options.Authority == nil {
		return nil, fmt.Errorf("KB source native activation dependencies are required")
	}
	now := options.Now
	if now == nil {
		now = func() time.Time { return time.Now().UTC() }
	}
	return &KBSourceNativeActivationService{
		enabled: options.Enabled, store: options.Store, repositories: options.Repositories,
		authorizer: options.Authorizer, content: options.Content, legacyDashboard: options.LegacyDashboard,
		nativeDashboard: options.NativeDashboard, authority: options.Authority,
		dashboardLimits: normalizedUADashboardLimits(options.DashboardLimits), now: now,
		semaphore: make(chan struct{}, 1),
	}, nil
}

type KBSourceNativeActivationRequest struct {
	WorkspaceID                      string
	KnowledgeBaseID                  string
	SourceID                         string
	SourceRevisionID                 string
	ExpectedSourceContentVersionID   string
	ExpectedUADashboardPublicationID string
	ExpectedAuthorityGeneration      uint64
	Actor                            string
}

type KBSourceNativeActivationResult struct {
	AuthorityGeneration    uint64
	SourceContentVersionID string
	UADashboardVersionID   string
	DashboardMigrated      bool
	Replayed               bool
}

func (service *KBSourceNativeActivationService) RunExact(ctx context.Context, request KBSourceNativeActivationRequest) (KBSourceNativeActivationResult, error) {
	if service == nil || !service.enabled {
		return KBSourceNativeActivationResult{}, invalidArgument("KB source native activation is disabled")
	}
	request, err := normalizeKBSourceNativeActivationRequest(request)
	if err != nil {
		return KBSourceNativeActivationResult{}, err
	}
	select {
	case service.semaphore <- struct{}{}:
		defer func() { <-service.semaphore }()
	default:
		return KBSourceNativeActivationResult{}, conflict("KB source native activation concurrency limit reached")
	}
	runCtx, cancel := context.WithTimeout(ctx, kbSourceNativeActivationMaxDuration)
	defer cancel()
	if err := service.authorize(runCtx, request); err != nil {
		return KBSourceNativeActivationResult{}, err
	}
	target, err := service.requireReadyTarget(runCtx, request)
	if err != nil {
		return KBSourceNativeActivationResult{}, err
	}
	authority, found, err := service.authority.GetAuthority(runCtx, request.WorkspaceID, kbSourceContentDomain, request.SourceID)
	if err != nil {
		return KBSourceNativeActivationResult{}, err
	}
	if authority.State == content.AuthorityStateNativeActive {
		return replayKBSourceNativeActivation(authority, request)
	}
	if authority.State != content.AuthorityStateLegacyActive || authorityGeneration(found, authority) != request.ExpectedAuthorityGeneration {
		return KBSourceNativeActivationResult{}, conflict("KB source authority changed")
	}
	if err := service.verifySourceContent(runCtx, request, target.mapping); err != nil {
		return KBSourceNativeActivationResult{}, err
	}

	dashboardVersionID, migrated, err := service.prepareDashboard(runCtx, request, target)
	if err != nil {
		return KBSourceNativeActivationResult{}, err
	}
	if err := service.authorize(runCtx, request); err != nil {
		return KBSourceNativeActivationResult{}, err
	}
	if _, err := service.requireReadyTarget(runCtx, request); err != nil {
		return KBSourceNativeActivationResult{}, err
	}
	checkpoint, err := encodeKBSourceCutoverCheckpoint(kbSourceCutoverCheckpoint{
		SourceContentVersionID: request.ExpectedSourceContentVersionID,
		UADashboardVersionID:   dashboardVersionID,
	})
	if err != nil {
		return KBSourceNativeActivationResult{}, err
	}
	current, currentFound, err := service.authority.GetAuthority(runCtx, request.WorkspaceID, kbSourceContentDomain, request.SourceID)
	if err != nil {
		return KBSourceNativeActivationResult{}, err
	}
	if current.State == content.AuthorityStateNativeActive {
		return replayKBSourceNativeActivation(current, request)
	}
	if current.State != content.AuthorityStateLegacyActive || authorityGeneration(currentFound, current) != request.ExpectedAuthorityGeneration {
		return KBSourceNativeActivationResult{}, conflict("KB source authority changed")
	}
	activated, err := service.authority.ActivateNativeAuthority(runCtx, content.ActivateNativeAuthorityParams{
		WorkspaceID: request.WorkspaceID, Domain: kbSourceContentDomain, BindingKey: request.SourceID,
		ExpectedGeneration: request.ExpectedAuthorityGeneration, Checkpoint: checkpoint,
		Actor: request.Actor, Now: service.now().UTC(),
	})
	if errors.Is(err, content.ErrAuthorityConflict) {
		return KBSourceNativeActivationResult{}, conflict("KB source authority changed")
	}
	if err != nil {
		return KBSourceNativeActivationResult{}, err
	}
	return KBSourceNativeActivationResult{
		AuthorityGeneration: activated.Generation, SourceContentVersionID: request.ExpectedSourceContentVersionID,
		UADashboardVersionID: dashboardVersionID, DashboardMigrated: migrated,
	}, nil
}

type kbSourceNativeActivationTarget struct {
	source   storekb.Source
	revision storekb.SourceRevision
	mapping  storekb.SourceContentMapping
}

func (service *KBSourceNativeActivationService) requireReadyTarget(ctx context.Context, request KBSourceNativeActivationRequest) (kbSourceNativeActivationTarget, error) {
	source, err := service.store.GetSource(ctx, request.WorkspaceID, request.KnowledgeBaseID, request.SourceID)
	if err != nil {
		return kbSourceNativeActivationTarget{}, mapStoreNotFound(err)
	}
	revision, err := service.store.GetCurrentSourceRevision(ctx, request.WorkspaceID, request.KnowledgeBaseID, request.SourceID)
	if err != nil {
		return kbSourceNativeActivationTarget{}, mapStoreNotFound(err)
	}
	if source.WorkspaceID != request.WorkspaceID || source.KnowledgeBaseID != request.KnowledgeBaseID ||
		revision.ID != request.SourceRevisionID || revision.SourceID != request.SourceID ||
		revision.SourceContentVersionID != request.ExpectedSourceContentVersionID || revision.SourceContentVerifiedAt == nil ||
		strings.TrimSpace(revision.UADashboardCommitSHA) != request.ExpectedUADashboardPublicationID {
		return kbSourceNativeActivationTarget{}, conflict("KB source native activation target changed")
	}
	if mirror, mirrorErr := service.store.GetSourceMirror(ctx, request.WorkspaceID, request.KnowledgeBaseID, request.SourceID); mirrorErr == nil {
		if mirror.Enabled {
			return kbSourceNativeActivationTarget{}, conflict("KB source mirror must be disabled before native activation")
		}
	} else if !errors.Is(mirrorErr, gorm.ErrRecordNotFound) {
		return kbSourceNativeActivationTarget{}, mirrorErr
	}
	jobs, err := service.store.ListActiveSourceJobsForSources(ctx, request.WorkspaceID, request.KnowledgeBaseID,
		[]string{request.SourceID}, []string{KBJobTypeSyncSourceMirror, KBJobTypeGenerateWiki})
	if err != nil {
		return kbSourceNativeActivationTarget{}, err
	}
	if len(jobs) != 0 {
		return kbSourceNativeActivationTarget{}, conflict("KB source jobs must be drained before native activation")
	}
	mappings, err := service.store.ListSourceContentMappingsForRevisions(ctx, request.WorkspaceID, []string{request.SourceRevisionID})
	if err != nil {
		return kbSourceNativeActivationTarget{}, err
	}
	if len(mappings) != 1 {
		return kbSourceNativeActivationTarget{}, conflict("KB source native mapping is incomplete")
	}
	mapping := mappings[0]
	if mapping.WorkspaceID != request.WorkspaceID || mapping.KnowledgeBaseID != request.KnowledgeBaseID ||
		mapping.SourceID != request.SourceID || mapping.SourceRevisionID != request.SourceRevisionID ||
		mapping.ContentID == "" || mapping.ContentID != revision.SourceContentID ||
		mapping.ContentVersionID != request.ExpectedSourceContentVersionID || mapping.VerifiedAt == nil ||
		!sourceContentMappingHasSearchProjection(mapping) || mapping.FileCount <= 0 || mapping.SizeBytes < 0 {
		return kbSourceNativeActivationTarget{}, conflict("KB source native mapping is incomplete")
	}
	return kbSourceNativeActivationTarget{source: source, revision: revision, mapping: mapping}, nil
}

func (service *KBSourceNativeActivationService) verifySourceContent(ctx context.Context, request KBSourceNativeActivationRequest, mapping storekb.SourceContentMapping) error {
	readCtx := kbSourceContentContext(ctx, request.Actor, request.WorkspaceID, request.SourceID, mapping.ContentID,
		contentservice.AccessReadMetadata)
	content, err := service.content.GetContent(readCtx, request.WorkspaceID, mapping.ContentID)
	if err != nil {
		return err
	}
	if content.Domain != kbSourceContentDomain || content.BindingKey != request.SourceID || content.Model != contentservice.TreeModel {
		return conflict("KB source native binding changed")
	}
	version, err := service.content.GetVersion(readCtx, request.WorkspaceID, mapping.ContentID, mapping.ContentVersionID)
	if err != nil {
		return err
	}
	if !strings.EqualFold(version.RootDigest, mapping.RootDigest) || version.FileCount != mapping.FileCount || version.SizeBytes != mapping.SizeBytes {
		return conflict("KB source native version changed")
	}
	return nil
}

func (service *KBSourceNativeActivationService) prepareDashboard(ctx context.Context, request KBSourceNativeActivationRequest, target kbSourceNativeActivationTarget) (string, bool, error) {
	publicationID := request.ExpectedUADashboardPublicationID
	if publicationID == "" {
		return "", false, nil
	}
	manifest, state, err := classifyUADashboardManifest(target.revision)
	if err != nil || state != "ready" {
		return "", false, conflict("legacy Dashboard publication is not eligible for native activation")
	}
	if validKBSourceCutoverVersionID(publicationID) {
		if err := service.verifyNativeDashboard(ctx, request, publicationID, manifest, nil); err != nil {
			return "", false, err
		}
		return publicationID, false, nil
	}
	if service.repositories == nil || service.legacyDashboard == nil {
		return "", false, conflict("legacy Dashboard migration is unavailable")
	}
	repository, err := service.repositories.Get(ctx, request.WorkspaceID, repos.ResourceTypeKBSource, request.SourceID, repos.RepoRoleInsights)
	if err != nil {
		return "", false, mapStoreNotFound(err)
	}
	files := make(map[string][]byte, len(manifest.Files))
	for _, entry := range manifest.Files {
		file, err := service.legacyDashboard.ReadUADashboardFile(ctx, UADashboardRepositoryFileRequest{
			Repository: repository, WorkspaceID: request.WorkspaceID, SourceID: request.SourceID, Subject: request.Actor,
			DashboardCommitSHA: publicationID, Path: entry.Path, ExpectedBlobSHA: entry.BlobSHA,
			MaxBytes: service.dashboardLimits.FileBytes,
		})
		if err != nil {
			return "", false, fmt.Errorf("read legacy Dashboard file: %w", err)
		}
		if int64(len(file.Content)) != entry.Bytes || strings.TrimSpace(file.BlobSHA) != strings.TrimSpace(entry.BlobSHA) {
			return "", false, conflict("legacy Dashboard file changed")
		}
		files[entry.Path] = file.Content
	}
	if _, err := validateUADashboardPublishFiles(files); err != nil {
		return "", false, err
	}
	if err := service.authorize(ctx, request); err != nil {
		return "", false, err
	}
	if _, err := service.requireReadyTarget(ctx, request); err != nil {
		return "", false, err
	}
	published, err := service.nativeDashboard.PublishUADashboard(ctx, UADashboardRepositoryPublishRequest{
		WorkspaceID: request.WorkspaceID, SourceID: request.SourceID, SourceRevisionID: request.SourceRevisionID,
		PreviousPublicationID: "", Actor: request.Actor, Files: files, Limits: service.dashboardLimits,
	})
	if err != nil {
		return "", false, fmt.Errorf("publish migrated Dashboard: %w", err)
	}
	if !validKBSourceCutoverVersionID(published.CommitSHA) || len(published.FileDigests) != len(files) {
		return "", false, conflict("native Dashboard publication is incomplete")
	}
	if err := service.verifyNativeDashboard(ctx, request, published.CommitSHA, manifest, published.FileDigests); err != nil {
		return "", false, err
	}
	return published.CommitSHA, true, nil
}

func (service *KBSourceNativeActivationService) verifyNativeDashboard(ctx context.Context, request KBSourceNativeActivationRequest, versionID string, manifest UADashboardManifest, digests map[string]string) error {
	for _, entry := range manifest.Files {
		digest := strings.TrimSpace(entry.BlobSHA)
		if digests != nil {
			digest = strings.TrimSpace(digests[entry.Path])
			if digest == "" {
				return conflict("native Dashboard digest is incomplete")
			}
		}
		file, err := service.nativeDashboard.ReadUADashboardFile(ctx, UADashboardRepositoryFileRequest{
			WorkspaceID: request.WorkspaceID, SourceID: request.SourceID, Subject: request.Actor,
			DashboardCommitSHA: versionID, Path: entry.Path, ExpectedBlobSHA: digest,
			MaxBytes: service.dashboardLimits.FileBytes,
		})
		if err != nil {
			return fmt.Errorf("verify native Dashboard file: %w", err)
		}
		if int64(len(file.Content)) != entry.Bytes {
			return conflict("native Dashboard file changed")
		}
	}
	return nil
}

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

func normalizeKBSourceNativeActivationRequest(request KBSourceNativeActivationRequest) (KBSourceNativeActivationRequest, 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.ExpectedSourceContentVersionID = strings.TrimSpace(request.ExpectedSourceContentVersionID)
	request.ExpectedUADashboardPublicationID = strings.TrimSpace(request.ExpectedUADashboardPublicationID)
	request.Actor = strings.TrimSpace(request.Actor)
	if request.WorkspaceID == "" || request.KnowledgeBaseID == "" || request.SourceID == "" || request.SourceRevisionID == "" ||
		!validKBSourceCutoverVersionID(request.ExpectedSourceContentVersionID) || request.Actor == "" ||
		len(request.ExpectedUADashboardPublicationID) > 128 || strings.ContainsRune(request.ExpectedUADashboardPublicationID, '\x00') {
		return request, invalidArgument("KB source native activation exact target is invalid")
	}
	return request, nil
}

func replayKBSourceNativeActivation(authority content.Authority, request KBSourceNativeActivationRequest) (KBSourceNativeActivationResult, error) {
	checkpoint, err := decodeKBSourceCutoverCheckpoint(authority.Checkpoint)
	if err != nil || checkpoint.SourceContentVersionID != request.ExpectedSourceContentVersionID {
		return KBSourceNativeActivationResult{}, conflict("KB source native activation checkpoint changed")
	}
	return KBSourceNativeActivationResult{
		AuthorityGeneration: authority.Generation, SourceContentVersionID: checkpoint.SourceContentVersionID,
		UADashboardVersionID: checkpoint.UADashboardVersionID, Replayed: true,
	}, nil
}

func authorityGeneration(found bool, authority content.Authority) uint64 {
	if !found {
		return 0
	}
	return authority.Generation
}
