package automation

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	contentstoreservice "github.com/tidbcloud/AgentTeam/internal/service/content"
	contentstorestore "github.com/tidbcloud/AgentTeam/internal/store/content"
	contentmigrationstore "github.com/tidbcloud/AgentTeam/internal/store/content/migration"
	resourcerepostore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

const automationContentCutoverCheckpointPrefix = "automation-cutover:v1|"

const automationContentFenceDuration = 5 * time.Minute

type AutomationContentActivationRequest struct {
	RepositoryID                string
	FullCommitSHA               string
	FullContentVersionID        string
	DeltaCommitSHA              string
	DeltaContentVersionID       string
	DeltaObservedAt             time.Time
	ExpectedAuthorityGeneration uint64
}

type AutomationContentActivationResult struct {
	AuthorityGeneration   uint64
	FullContentVersionID  string
	DeltaContentVersionID string
	DeltaObservedAt       time.Time
	CutoffAt              time.Time
	Replayed              bool
}

type automationContentCutoverCheckpoint struct {
	fullVersionID   string
	deltaVersionID  string
	deltaObservedAt time.Time
}

// ActivateNativeAutomationContent is the exact-binding cutover owner. Both
// the bounded full import and the one later delta observation must already be
// durable exact migration mappings. The authority CAS is also the legacy AGS
// ingress cutoff, so no native routing is visible before every preflight has
// succeeded.
func (service *AutomationsService) ActivateNativeAutomationContent(ctx context.Context, workspaceID, subject, owner, repository string, request AutomationContentActivationRequest) (AutomationContentActivationResult, error) {
	if service == nil || service.migrationMappings == nil || service.migrationContent == nil || service.contentAuthority == nil || service.repositoryStore == nil || service.store == nil || service.now == nil {
		return AutomationContentActivationResult{}, invalidArgument("Automation content activation is not configured")
	}
	service.contentCutoverMu.Lock()
	defer service.contentCutoverMu.Unlock()
	workspaceID = strings.TrimSpace(workspaceID)
	subject = strings.TrimSpace(subject)
	owner = strings.TrimSpace(owner)
	repository = strings.TrimSpace(repository)
	request = normalizeAutomationContentActivationRequest(request)
	if err := service.validateScope(workspaceID, subject); err != nil {
		return AutomationContentActivationResult{}, err
	}
	if err := validateAutomationRepository(owner, repository); err != nil || validateAutomationContentActivationRequest(request) != nil {
		return AutomationContentActivationResult{}, invalidArgument("Automation content activation exact evidence is invalid")
	}
	if request.DeltaObservedAt.After(service.now().UTC()) {
		return AutomationContentActivationResult{}, invalidArgument("Automation delta observation time is in the future")
	}
	if err := service.automationAuthorizer().CanWriteAutomationRepo(ctx, service.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{RepositoryOwner: owner, RepositoryName: repository})); err != nil {
		return AutomationContentActivationResult{}, err
	}
	bindingKey := automationRepositoryBindingKey(owner, repository)
	if err := service.verifyAutomationContentActivationTarget(ctx, workspaceID, subject, bindingKey, owner, repository, request); err != nil {
		return AutomationContentActivationResult{}, err
	}
	authority, found, err := service.contentAuthority.GetAuthority(ctx, workspaceID, "automation", bindingKey)
	if err != nil {
		return AutomationContentActivationResult{}, fmt.Errorf("resolve Automation activation authority: %w", err)
	}
	if authority.State == contentstorestore.AuthorityStateNativeActive {
		return replayAutomationContentActivation(authority, request)
	}
	if authority.State != contentstorestore.AuthorityStateLegacyActive || automationAuthorityGeneration(found, authority) != request.ExpectedAuthorityGeneration {
		return AutomationContentActivationResult{}, contentstorestore.ErrAuthorityConflict
	}
	fence, err := service.acquireAutomationContentFence(ctx, workspaceID, bindingKey, subject)
	if err != nil {
		return AutomationContentActivationResult{}, err
	}
	defer service.releaseAutomationContentFence(ctx, fence)
	// Recheck live ACL and all exact evidence immediately before the one-way
	// cutoff. Failures leave the binding legacy-active and retryable.
	if err := service.automationAuthorizer().CanWriteAutomationRepo(ctx, service.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{RepositoryOwner: owner, RepositoryName: repository})); err != nil {
		return AutomationContentActivationResult{}, err
	}
	if err := service.verifyAutomationContentActivationTarget(ctx, workspaceID, subject, bindingKey, owner, repository, request); err != nil {
		return AutomationContentActivationResult{}, err
	}
	checkpoint, err := encodeAutomationContentCutoverCheckpoint(request)
	if err != nil {
		return AutomationContentActivationResult{}, err
	}
	cutoffAt := service.now().UTC()
	if cutoffAt.Before(request.DeltaObservedAt) {
		return AutomationContentActivationResult{}, invalidArgument("Automation delta observation time is in the future")
	}
	activated, err := service.contentAuthority.ActivateNativeAuthority(ctx, contentstorestore.ActivateNativeAuthorityParams{
		WorkspaceID: workspaceID, Domain: "automation", BindingKey: bindingKey,
		ExpectedGeneration: request.ExpectedAuthorityGeneration, ExpectedCheckpoint: fence.Checkpoint, Checkpoint: checkpoint,
		Actor: subject, Now: cutoffAt,
	})
	if errors.Is(err, contentstorestore.ErrAuthorityConflict) {
		current, currentFound, loadErr := service.contentAuthority.GetAuthority(ctx, workspaceID, "automation", bindingKey)
		if loadErr == nil && currentFound && current.State == contentstorestore.AuthorityStateNativeActive {
			return replayAutomationContentActivation(current, request)
		}
		return AutomationContentActivationResult{}, contentstorestore.ErrAuthorityConflict
	}
	if err != nil {
		return AutomationContentActivationResult{}, err
	}
	return automationContentActivationResult(activated, request, false), nil
}

func (service *AutomationsService) verifyAutomationContentActivationTarget(ctx context.Context, workspaceID, subject, bindingKey, owner, repository string, request AutomationContentActivationRequest) error {
	repo, err := service.repositoryStore.Get(ctx, workspaceID, resourcerepostore.ResourceTypeAutomationRepository, resourcerepostore.AutomationRepositoryResourceID(bindingKey), resourcerepostore.RepoRolePrimary)
	if err != nil || strings.TrimSpace(repo.AGSRepoID) != request.RepositoryID || !strings.EqualFold(strings.TrimSpace(repo.RepoFullName), bindingKey) {
		return invalidArgument("Automation activation repository identity does not match primary binding")
	}
	full, err := service.migrationMappings.GetMapping(ctx, request.RepositoryID, request.FullCommitSHA)
	if err != nil || !automationContentActivationMappingMatches(full, workspaceID, request.RepositoryID, request.FullCommitSHA, request.FullContentVersionID) {
		return invalidArgument("Automation full import evidence is incomplete")
	}
	delta, err := service.migrationMappings.GetMapping(ctx, request.RepositoryID, request.DeltaCommitSHA)
	if err != nil || !automationContentActivationMappingMatches(delta, workspaceID, request.RepositoryID, request.DeltaCommitSHA, request.DeltaContentVersionID) || delta.ContentID != full.ContentID {
		return invalidArgument("Automation delta observation evidence is incomplete")
	}
	if delta.VerifiedAt.Before(*full.VerifiedAt) || request.DeltaObservedAt.Before(delta.VerifiedAt.UTC()) {
		return invalidArgument("Automation delta observation ordering is invalid")
	}
	content, found, err := service.migrationContent.FindContentByBinding(
		WithAutomationContentAccess(ctx, workspaceID, bindingKey, "", 0, subject, contentstoreservice.AccessReadMetadata),
		workspaceID, "automation", bindingKey,
	)
	if err != nil || !found || content.ID != delta.ContentID || content.HeadVersionID != delta.ContentVersionID {
		return invalidArgument("Automation delta observation is not the exact native head")
	}
	deltaVersion, err := service.migrationContent.GetVersion(
		WithAutomationContentAccess(ctx, workspaceID, bindingKey, delta.ContentID, 0, subject, contentstoreservice.AccessReadMetadata),
		workspaceID, delta.ContentID, delta.ContentVersionID,
	)
	if err != nil || (delta.ContentVersionID != full.ContentVersionID && deltaVersion.ParentVersionID != full.ContentVersionID) {
		return invalidArgument("Automation delta observation does not follow the full import")
	}
	for _, mapping := range []contentmigrationstore.Mapping{full, delta} {
		publication, loadErr := service.store.GetContentPublication(ctx, workspaceID, mapping.ContentID, mapping.ContentVersionID)
		if loadErr != nil || publication.RepositoryOwner != owner || publication.RepositoryName != repository {
			return invalidArgument("Automation migration projection evidence is incomplete")
		}
	}
	current, err := service.store.GetCurrentContentRepository(ctx, workspaceID, owner, repository)
	if err != nil || current.ContentID != delta.ContentID || current.ContentVersionID != delta.ContentVersionID {
		return invalidArgument("Automation delta observation is not the exact current projection")
	}
	return nil
}

func normalizeAutomationContentActivationRequest(request AutomationContentActivationRequest) AutomationContentActivationRequest {
	request.RepositoryID = strings.TrimSpace(request.RepositoryID)
	request.FullCommitSHA = strings.ToLower(strings.TrimSpace(request.FullCommitSHA))
	request.FullContentVersionID = strings.TrimSpace(request.FullContentVersionID)
	request.DeltaCommitSHA = strings.ToLower(strings.TrimSpace(request.DeltaCommitSHA))
	request.DeltaContentVersionID = strings.TrimSpace(request.DeltaContentVersionID)
	request.DeltaObservedAt = request.DeltaObservedAt.UTC().Truncate(time.Millisecond)
	return request
}

func validateAutomationContentActivationRequest(request AutomationContentActivationRequest) error {
	if request.RepositoryID == "" || !automationCommitSHA.MatchString(request.FullCommitSHA) || !automationCommitSHA.MatchString(request.DeltaCommitSHA) ||
		request.FullContentVersionID == "" || len(request.FullContentVersionID) > 64 || request.DeltaContentVersionID == "" || len(request.DeltaContentVersionID) > 64 || request.DeltaObservedAt.IsZero() {
		return fmt.Errorf("Automation content activation evidence is invalid")
	}
	if request.FullCommitSHA == request.DeltaCommitSHA || request.FullContentVersionID == request.DeltaContentVersionID {
		return fmt.Errorf("Automation content activation requires a distinct delta observation")
	}
	return nil
}

func automationContentActivationMappingMatches(mapping contentmigrationstore.Mapping, workspaceID, repositoryID, commitSHA, versionID string) bool {
	return mapping.WorkspaceID == workspaceID && mapping.RepositoryID == repositoryID && strings.EqualFold(mapping.CommitSHA, commitSHA) &&
		mapping.ContentID != "" && mapping.ContentVersionID == versionID && mapping.Model == contentstorestore.ModelTree && mapping.VerifiedAt != nil
}

func encodeAutomationContentCutoverCheckpoint(request AutomationContentActivationRequest) (string, error) {
	checkpoint := automationContentCutoverCheckpointPrefix + request.FullContentVersionID + "|" + request.DeltaContentVersionID + "|" + strconv.FormatInt(request.DeltaObservedAt.UnixMilli(), 10)
	if len(checkpoint) > 255 {
		return "", invalidArgument("Automation content activation checkpoint is too large")
	}
	return checkpoint, nil
}

func decodeAutomationContentCutoverCheckpoint(value string) (automationContentCutoverCheckpoint, error) {
	if !strings.HasPrefix(value, automationContentCutoverCheckpointPrefix) {
		return automationContentCutoverCheckpoint{}, fmt.Errorf("Automation content activation checkpoint is invalid")
	}
	parts := strings.Split(strings.TrimPrefix(value, automationContentCutoverCheckpointPrefix), "|")
	if len(parts) != 3 || parts[0] == "" || len(parts[0]) > 64 || parts[1] == "" || len(parts[1]) > 64 {
		return automationContentCutoverCheckpoint{}, fmt.Errorf("Automation content activation checkpoint is invalid")
	}
	millis, err := strconv.ParseInt(parts[2], 10, 64)
	if err != nil || millis <= 0 {
		return automationContentCutoverCheckpoint{}, fmt.Errorf("Automation content activation checkpoint is invalid")
	}
	return automationContentCutoverCheckpoint{fullVersionID: parts[0], deltaVersionID: parts[1], deltaObservedAt: time.UnixMilli(millis).UTC()}, nil
}

func replayAutomationContentActivation(authority contentstorestore.Authority, request AutomationContentActivationRequest) (AutomationContentActivationResult, error) {
	checkpoint, err := decodeAutomationContentCutoverCheckpoint(authority.Checkpoint)
	if err != nil || authority.Generation == 0 || authority.Generation-1 != request.ExpectedAuthorityGeneration || checkpoint.fullVersionID != request.FullContentVersionID || checkpoint.deltaVersionID != request.DeltaContentVersionID || !checkpoint.deltaObservedAt.Equal(request.DeltaObservedAt) {
		return AutomationContentActivationResult{}, contentstorestore.ErrAuthorityConflict
	}
	return automationContentActivationResult(authority, request, true), nil
}

func (service *AutomationsService) acquireAutomationContentFence(ctx context.Context, workspaceID, bindingKey, actor string) (contentstorestore.LegacyAuthorityFence, error) {
	token, err := newAutomationContentFenceToken()
	if err != nil {
		return contentstorestore.LegacyAuthorityFence{}, err
	}
	now := service.now().UTC()
	return service.contentAuthority.AcquireLegacyAuthorityFence(ctx, contentstorestore.AcquireLegacyAuthorityFenceParams{
		WorkspaceID: workspaceID, Domain: "automation", BindingKey: bindingKey,
		Token: token, Actor: actor, Now: now, ExpiresAt: now.Add(automationContentFenceDuration),
	})
}

func (service *AutomationsService) releaseAutomationContentFence(ctx context.Context, fence contentstorestore.LegacyAuthorityFence) {
	releaseCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
	defer cancel()
	_ = service.contentAuthority.ReleaseLegacyAuthorityFence(releaseCtx, fence, service.now().UTC())
}

func newAutomationContentFenceToken() (string, error) {
	var raw [16]byte
	if _, err := rand.Read(raw[:]); err != nil {
		return "", fmt.Errorf("generate Automation content fence: %w", err)
	}
	return hex.EncodeToString(raw[:]), nil
}

func automationContentActivationResult(authority contentstorestore.Authority, request AutomationContentActivationRequest, replayed bool) AutomationContentActivationResult {
	return AutomationContentActivationResult{
		AuthorityGeneration: authority.Generation, FullContentVersionID: request.FullContentVersionID,
		DeltaContentVersionID: request.DeltaContentVersionID, DeltaObservedAt: request.DeltaObservedAt,
		CutoffAt: authority.UpdatedAt.UTC(), Replayed: replayed,
	}
}

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

var _ AutomationContentMigrationMappingStore = (*contentmigrationstore.Store)(nil)
