package roles

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

	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	contentservice "github.com/tidbcloud/AgentTeam/internal/service/content"
	"github.com/tidbcloud/AgentTeam/internal/service/content/migration"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	contentstore "github.com/tidbcloud/AgentTeam/internal/store/content"
	rolesstore "github.com/tidbcloud/AgentTeam/internal/store/resource/roles"
	"gorm.io/gorm"
)

type roleContentMigrationWorker interface {
	RunExact(context.Context, migration.Request, migration.Exporter, migration.Projector) (migration.Result, error)
}

type roleContentMigrationExporter interface {
	ExportExactRolePackage(context.Context, string, string, string, string, []rolesstore.RolePackageManifestItem, contentpayload.Limits) (migration.Export, error)
}

type RoleContentMigrationRequest struct {
	RoleVersionID            string
	RepositoryID             string
	CommitSHA                string
	ExpectedContentVersionID string
}

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

// RunExactRoleContentMigration migrates one explicitly selected active Role
// package revision into Content Store, records the verified mapping and atomic
// product projection, and leaves authority unchanged. A concurrent one-way
// activation may overtake this best-effort import by contract.
func (s *Service) RunExactRoleContentMigration(ctx context.Context, workspaceID, subject, roleID string, request RoleContentMigrationRequest) (output RoleContentMigrationResult, err error) {
	if s == nil || s.migrationWorker == nil || s.migrationExporter == nil || s.contentAuthority == nil {
		return RoleContentMigrationResult{}, invalidRoleArgument("role content migration worker is not configured")
	}
	if err := s.validate(workspaceID, subject); err != nil {
		return RoleContentMigrationResult{}, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	subject = strings.TrimSpace(subject)
	roleID = strings.TrimSpace(roleID)
	request.RoleVersionID = strings.TrimSpace(request.RoleVersionID)
	request.RepositoryID = strings.TrimSpace(request.RepositoryID)
	request.CommitSHA = strings.ToLower(strings.TrimSpace(request.CommitSHA))
	request.ExpectedContentVersionID = strings.TrimSpace(request.ExpectedContentVersionID)
	if roleID == "" || request.RoleVersionID == "" || request.RepositoryID == "" {
		return RoleContentMigrationResult{}, invalidRoleArgument("role content migration exact target is required")
	}

	role, items, err := s.loadRoleMigrationTarget(ctx, workspaceID, subject, roleID, request.RoleVersionID)
	if err != nil {
		return RoleContentMigrationResult{}, err
	}
	authority, found, err := s.contentAuthority.GetAuthority(ctx, role.WorkspaceID, rolePackageContentDomain, role.ID)
	if err != nil {
		return RoleContentMigrationResult{}, rolePackageDependencyError(fmt.Errorf("resolve role content migration authority: %w", err))
	}
	if found && strings.TrimSpace(authority.State) != contentstore.AuthorityStateLegacyActive {
		return RoleContentMigrationResult{}, invalidRoleArgument("role content migration requires legacy read ownership")
	}
	// Recheck live Role ACL, active-version lineage, and source manifest before
	// any AGS export or Content Store write.
	role, items, err = s.loadRoleMigrationTarget(ctx, workspaceID, subject, roleID, request.RoleVersionID)
	if err != nil {
		return RoleContentMigrationResult{}, err
	}

	limits := contentpayload.Limits{
		MaxFiles: rolePackageMaxFiles, MaxFileBytes: rolePackageMaxFileBytes,
		MaxTotalBytes: rolePackageMaxTotalBytes, MaxManifestBytes: rolePackageMaxManifestBytes,
	}
	result, err := s.migrationWorker.RunExact(ctx, migration.Request{
		WorkspaceID: role.WorkspaceID, BindingKey: role.ID,
		SourceCursor: request.RoleVersionID, RepositoryID: request.RepositoryID, CommitSHA: request.CommitSHA,
		ExpectedHeadVersionID: request.ExpectedContentVersionID, Actor: subject, Limits: limits,
	}, func(exportCtx context.Context, exportLimits contentpayload.Limits) (migration.Export, error) {
		return s.migrationExporter.ExportExactRolePackage(exportCtx, role.WorkspaceID, role.ID, request.RepositoryID, request.CommitSHA, items, exportLimits)
	}, func(projectCtx context.Context, migration migration.Result) error {
		projectedItems, projectionErr := roleMigrationManifestProjection(role.WorkspaceID, role.ID, request.RoleVersionID, migration, items)
		if projectionErr != nil {
			return projectionErr
		}
		projectionErr = s.store.ProjectRoleVersionContent(projectCtx, rolesstore.RoleVersionContentProjection{
			WorkspaceID: role.WorkspaceID, RoleID: role.ID, RoleVersionID: request.RoleVersionID,
			ExpectedContentVersionID: request.ExpectedContentVersionID, ContentID: migration.ContentID,
			ContentVersionID: migration.VersionID, ManifestItems: projectedItems,
		})
		if errors.Is(projectionErr, gorm.ErrRecordNotFound) {
			return ErrRoleVersionConflict
		}
		return projectionErr
	})
	if err != nil {
		return RoleContentMigrationResult{}, err
	}
	return RoleContentMigrationResult{
		ContentID: result.ContentID, ContentVersionID: result.VersionID, RootDigest: result.RootDigest,
		FileCount: result.FileCount, SizeBytes: result.SizeBytes, Replayed: result.Replayed,
	}, nil
}

func (s *Service) loadRoleMigrationTarget(ctx context.Context, workspaceID, subject, roleID, roleVersionID string) (rolesstore.Role, []rolesstore.RolePackageManifestItem, error) {
	role, err := s.store.GetRole(ctx, workspaceID, roleID)
	if err != nil {
		return rolesstore.Role{}, nil, mapStoreNotFound(err)
	}
	grant, err := s.roleGrant(ctx, role, subject)
	if err != nil {
		return rolesstore.Role{}, nil, err
	}
	if !roleCanManage(grant) {
		return rolesstore.Role{}, nil, repos.ErrForbidden
	}
	if strings.TrimSpace(role.ActiveVersionID) != roleVersionID {
		return rolesstore.Role{}, nil, ErrRoleVersionConflict
	}
	_, err = s.store.GetRoleVersion(ctx, role.WorkspaceID, role.ID, roleVersionID)
	if err != nil {
		return rolesstore.Role{}, nil, mapStoreNotFound(err)
	}
	items, err := s.store.ListManifestItemsByRoleVersionBounded(ctx, role.WorkspaceID, role.ID, roleVersionID, rolesstore.MaxRolePackageManifestItems)
	if err != nil {
		return rolesstore.Role{}, nil, err
	}
	if len(items) == 0 {
		return rolesstore.Role{}, nil, invalidRoleArgument("role content migration manifest is missing")
	}
	if len(items) > rolesstore.MaxRolePackageManifestItems {
		return rolesstore.Role{}, nil, invalidRoleArgument("role content migration manifest exceeds item limit")
	}
	return role, items, nil
}

func roleMigrationManifestProjection(workspaceID, roleID, roleVersionID string, migration migration.Result, items []rolesstore.RolePackageManifestItem) ([]rolesstore.RolePackageManifestItem, error) {
	paths := make(map[string]contentservice.Path, len(migration.Paths))
	for _, path := range migration.Paths {
		name := strings.TrimSpace(path.Path)
		if name == "" || strings.TrimSpace(path.Digest) == "" {
			return nil, rolePackageDependencyError(fmt.Errorf("migrated role package path metadata is incomplete"))
		}
		if _, duplicate := paths[name]; duplicate {
			return nil, rolePackageDependencyError(fmt.Errorf("migrated role package contains duplicate path"))
		}
		paths[name] = path
	}
	projected := make([]rolesstore.RolePackageManifestItem, 0, len(items))
	used := 0
	for _, item := range items {
		updated := item
		updated.Content = ""
		switch roleManifestItemProjection(item) {
		case roleManifestItemProjectionExact:
			assetPath, err := rolePackageAssetPath(RolePackageAsset{ItemType: item.ItemType, Name: item.Name})
			if err != nil {
				return nil, err
			}
			path, ok := paths[assetPath]
			if !ok {
				return nil, rolePackageDependencyError(fmt.Errorf("migrated role package asset %q is missing", assetPath))
			}
			updated.ContentID = migration.ContentID
			updated.ContentVersionID = migration.VersionID
			updated.ContentPath = assetPath
			updated.ContentHash = path.Digest
			updated.ContentRef = nativeRolePackageContentRef(workspaceID, migration.ContentID, migration.VersionID, assetPath)
			used++
		case roleManifestItemProjectionExternal:
			updated.ContentRef = ""
			updated.ContentID = ""
			updated.ContentVersionID = ""
			updated.ContentPath = ""
			updated.ContentHash = ""
		default:
			return nil, invalidRolePackageContract("role content migration manifest contains unsupported item")
		}
		updated.WorkspaceID = workspaceID
		updated.RoleID = roleID
		updated.RoleVersionID = roleVersionID
		projected = append(projected, updated)
	}
	if used != len(paths) {
		return nil, rolePackageDependencyError(fmt.Errorf("migrated role package contains unowned paths"))
	}
	if _, err := validateRoleVersionExactProjection(rolesstore.RoleVersion{ContentID: migration.ContentID, ContentVersionID: migration.VersionID}, projected); err != nil {
		return nil, err
	}
	return projected, nil
}
