package skills

import (
	"archive/zip"
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"strings"
	"time"

	reposservice "github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/memory"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	store "github.com/tidbcloud/AgentTeam/internal/store/resource/skills"
	"github.com/tidbcloud/AgentTeam/presets"
	"gorm.io/gorm"
)

const officialSkillDefaultTemplateLocale = presets.LocaleEnglish

const (
	retiredTeamCollaborationSkillLibrarySlug = "agentteam-team-collaboration-skills"
	officialSkillSystemSubject               = "agentteam-system"
	officialSkillLibraryReadyPolls           = 8
	officialSkillLibraryReadyPollBackoff     = 20 * time.Millisecond
)

var retiredOfficialSkillLibraryIdentities = []store.SkillIdentity{
	{Slug: retiredTeamCollaborationSkillLibrarySlug, CreatedBy: officialSkillSystemSubject},
}

type officialSkillLibraryDefinition struct {
	DefaultInfo presets.SkillLibraryInfo
	SkillNames  []string
}

type officialSkillLibraryMaterialization struct {
	Skill           store.Skill
	Repository      *reposstore.ResourceRepository
	AutoAgentAccess bool
}

var (
	officialSkillLibraryDefinitions       []officialSkillLibraryDefinition
	officialSkillLibraryDefinitionsBySlug map[string]officialSkillLibraryDefinition
)

func init() {
	infos, err := presets.OfficialSkillLibraries(officialSkillDefaultTemplateLocale)
	if err != nil {
		panic(fmt.Sprintf("load official skill libraries: %v", err))
	}
	officialSkillLibraryDefinitions = make([]officialSkillLibraryDefinition, 0, len(infos))
	officialSkillLibraryDefinitionsBySlug = make(map[string]officialSkillLibraryDefinition, len(infos))
	for _, info := range infos {
		slugs, err := presets.OfficialSkillSlugs(info.Slug)
		if err != nil {
			panic(fmt.Sprintf("load official skill library %q slugs: %v", info.Slug, err))
		}
		definition := officialSkillLibraryDefinition{
			DefaultInfo: info,
			SkillNames:  slugs,
		}
		if _, duplicate := officialSkillLibraryDefinitionsBySlug[info.Slug]; duplicate {
			panic(fmt.Sprintf("duplicate official skill library slug %q", info.Slug))
		}
		officialSkillLibraryDefinitions = append(officialSkillLibraryDefinitions, definition)
		officialSkillLibraryDefinitionsBySlug[info.Slug] = definition
	}
}

func (s *Service) ensureOfficialSkillLibraries(ctx context.Context, workspaceID, templateLocale string) ([]officialSkillLibraryMaterialization, error) {
	if s == nil || !s.officialSkillLibrariesEnabled {
		return nil, nil
	}
	nativeSkills, err := s.officialSkillLibraryNativeReads(ctx, workspaceID, officialSkillLibraryDefinitions)
	if err != nil {
		return nil, err
	}
	if err := s.retireRemovedOfficialSkillLibraries(ctx, workspaceID); err != nil {
		return nil, err
	}
	out := make([]officialSkillLibraryMaterialization, 0, len(officialSkillLibraryDefinitions))
	for _, definition := range officialSkillLibraryDefinitions {
		skill, repo, err := s.ensureOfficialSkillLibrary(ctx, workspaceID, templateLocale, definition, nativeSkills)
		if err != nil {
			return nil, err
		}
		out = append(out, officialSkillLibraryMaterialization{
			Skill:           skill,
			Repository:      repo,
			AutoAgentAccess: definition.DefaultInfo.AutoAgentAccess,
		})
	}
	return out, nil
}

func (s *Service) ensureOfficialAutoAgentAccessSkillLibraries(ctx context.Context, workspaceID, templateLocale string) ([]officialSkillLibraryMaterialization, error) {
	if s == nil || !s.officialSkillLibrariesEnabled {
		return nil, nil
	}
	autoDefinitions := make([]officialSkillLibraryDefinition, 0, len(officialSkillLibraryDefinitions))
	for _, definition := range officialSkillLibraryDefinitions {
		if definition.DefaultInfo.AutoAgentAccess {
			autoDefinitions = append(autoDefinitions, definition)
		}
	}
	nativeSkills, err := s.officialSkillLibraryNativeReads(ctx, workspaceID, autoDefinitions)
	if err != nil {
		return nil, err
	}
	if err := s.retireRemovedOfficialSkillLibraries(ctx, workspaceID); err != nil {
		return nil, err
	}
	out := make([]officialSkillLibraryMaterialization, 0, len(autoDefinitions))
	for _, definition := range autoDefinitions {
		skill, repo, err := s.ensureOfficialSkillLibrary(ctx, workspaceID, templateLocale, definition, nativeSkills)
		if err != nil {
			return nil, err
		}
		out = append(out, officialSkillLibraryMaterialization{
			Skill:           skill,
			Repository:      repo,
			AutoAgentAccess: definition.DefaultInfo.AutoAgentAccess,
		})
	}
	return out, nil
}

// officialSkillLibraryNativeReads resolves the fixed product inventory in one
// authority batch. Missing official Skills are implicit legacy and are not
// materialized in the authority table merely because a list endpoint ran.
func (s *Service) officialSkillLibraryNativeReads(ctx context.Context, workspaceID string, definitions []officialSkillLibraryDefinition) (map[string]bool, error) {
	if s == nil || s.contentAuthority == nil || len(definitions) == 0 {
		return map[string]bool{}, nil
	}
	skillIDs := make([]string, 0, len(definitions))
	for _, definition := range definitions {
		skill, err := s.store.GetSkillBySlugAndCreator(ctx, workspaceID, definition.DefaultInfo.Slug, definition.DefaultInfo.SystemSubject)
		if errors.Is(err, gorm.ErrRecordNotFound) {
			continue
		}
		if err != nil {
			return nil, mapStoreError(err)
		}
		skillIDs = append(skillIDs, skill.ID)
	}
	return s.nativeSkillReads(ctx, workspaceID, skillIDs)
}

// retireRemovedOfficialSkillLibraries permanently revokes an official library
// that no longer exists in the embedded registry. Identity matching uses both
// slug and creator so a user-created collection reusing the old slug survives.
//
// Access and database state are removed even when AGS repository deletion
// fails. This makes the next batch access response authoritative immediately,
// allowing upgraded daemons to remove their managed clone and runtime copies;
// the failed AGS cleanup remains recorded in repository lifecycle history.
func (s *Service) retireRemovedOfficialSkillLibraries(ctx context.Context, workspaceID string) error {
	if s == nil || !s.officialSkillLibrariesEnabled {
		return nil
	}
	workspaceID = strings.TrimSpace(workspaceID)
	if workspaceID == "" {
		return invalidArgument("workspace id is required")
	}
	if s.store == nil {
		return invalidArgument("official skill retirement requires the skill store to be configured")
	}
	for _, identity := range retiredOfficialSkillLibraryIdentities {
		skill, err := s.store.GetSkillBySlugAndCreatorIncludingDeleted(ctx, workspaceID, identity.Slug, identity.CreatedBy)
		if errors.Is(err, gorm.ErrRecordNotFound) {
			continue
		}
		if err != nil {
			return mapStoreError(err)
		}
		if s.repositoryStore == nil {
			return invalidArgument("official skill retirement requires skill repositories to be configured")
		}
		if err := s.retireOfficialSkillLibrary(ctx, skill, identity.CreatedBy); err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) retireOfficialSkillLibrary(ctx context.Context, skill store.Skill, actor string) (err error) {
	writeRoute, err := s.beginSkillWrite(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return err
	}
	repos, err := s.repositoryStore.ListForResource(
		ctx,
		skill.WorkspaceID,
		reposstore.ResourceTypeSkillRepository,
		skill.ID,
	)
	if err != nil {
		return err
	}
	maxBindingRows := 0
	if s.contentAuthority != nil {
		maxBindingRows = store.MaxAgentAccessibleSkillRows
	}
	bindings, err := s.store.ListSkillAgentBindings(ctx, skill.WorkspaceID, skill.ID, maxBindingRows)
	if err != nil {
		return err
	}
	affectedAgents := make([]string, 0)
	for _, binding := range bindings {
		affectedAgents = append(affectedAgents, binding.AgentID)
	}

	now := s.now()
	if !writeRoute.native {
		if deleteErr := reposservice.DeleteManagedResourceRepositories(ctx, s.repositoryStore, s.repositoryDeleter, repos, actor, now); deleteErr != nil {
			s.recordSkillRepoLifecycleFailedActivity(ctx, repos, actor, now, deleteErr)
		}
	}
	repositoryBranches := make([]store.SkillRepositoryBranch, 0, len(repos))
	for _, repo := range repos {
		repositoryBranches = append(repositoryBranches, store.SkillRepositoryBranch{
			WorkspaceID: repo.WorkspaceID,
			RepoOwner:   repo.RepoOwner,
			RepoName:    repo.RepoName,
		})
	}

	err = s.store.Transaction(ctx, func(tx *store.Store) error {
		for _, agentID := range uniqueNonEmptyStrings(affectedAgents) {
			if err := s.enqueueAgentAssetAccessChange(ctx, tx, skill.WorkspaceID, agentID, memory.AssetTypeSkill, now); err != nil {
				return err
			}
		}
		if err := tx.DeleteSkillAgentBindingsForSkill(ctx, skill.WorkspaceID, skill.ID); err != nil {
			return err
		}
		if err := tx.DeleteSkillGrantsForSkill(ctx, skill.WorkspaceID, skill.ID); err != nil {
			return err
		}
		if err := tx.DeleteSkillRepositoryBranchesForRepositories(ctx, repositoryBranches); err != nil {
			return err
		}
		if _, err := s.repositoryStore.WithDB(tx.DB()).DeleteForResource(ctx, skill.WorkspaceID, reposstore.ResourceTypeSkillRepository, skill.ID); err != nil {
			return err
		}
		return tx.PurgeSkill(ctx, skill.WorkspaceID, skill.ID)
	})
	if err != nil && !writeRoute.native {
		_ = reposservice.RecordResourceRepositoryLifecycleFailure(
			ctx,
			s.repositoryStore,
			repos,
			reposstore.ResourceRepositoryLifecycleOperationDeleteCleanup,
			actor,
			now,
			err,
		)
	}
	return mapStoreError(err)
}

func (s *Service) ensureOfficialSkillLibrary(ctx context.Context, workspaceID, templateLocale string, definition officialSkillLibraryDefinition, nativeSkills map[string]bool) (store.Skill, *reposstore.ResourceRepository, error) {
	if s == nil || !s.officialSkillLibrariesEnabled {
		return store.Skill{}, nil, nil
	}
	workspaceID = strings.TrimSpace(workspaceID)
	if workspaceID == "" {
		return store.Skill{}, nil, invalidArgument("workspace id is required")
	}
	if s.store == nil {
		return store.Skill{}, nil, invalidArgument("official skill libraries require the skill store to be configured")
	}
	info, err := presets.OfficialSkillLibrary(definition.DefaultInfo.Slug, templateLocale)
	if err != nil {
		return store.Skill{}, nil, err
	}
	key := workspaceID + ":" + info.Slug
	resultCh := s.officialSkillLibraryFlights.DoChan(key, func() (any, error) {
		return s.ensureOfficialSkillLibraryMaterialized(ctx, workspaceID, info, definition, nativeSkills)
	})
	select {
	case <-ctx.Done():
		return store.Skill{}, nil, ctx.Err()
	case result := <-resultCh:
		if result.Err != nil {
			return store.Skill{}, nil, result.Err
		}
		materialized, ok := result.Val.(officialSkillLibraryMaterialization)
		if !ok {
			return store.Skill{}, nil, invalidArgument("official skill library materialization failed")
		}
		return materialized.Skill, materialized.Repository, nil
	}
}

func (s *Service) ensureOfficialSkillLibraryMaterialized(ctx context.Context, workspaceID string, info presets.SkillLibraryInfo, definition officialSkillLibraryDefinition, nativeSkills map[string]bool) (officialSkillLibraryMaterialization, error) {
	skill, err := s.store.GetSkillBySlugAndCreator(ctx, workspaceID, info.Slug, info.SystemSubject)
	if err != nil {
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return officialSkillLibraryMaterialization{}, mapStoreError(err)
		}
		exists, existsErr := s.store.VisibilitySlugExists(
			ctx,
			workspaceID,
			SkillVisibilityScopeWorkspace,
			workspaceID,
			info.Slug,
		)
		if existsErr != nil {
			return officialSkillLibraryMaterialization{}, existsErr
		}
		if exists {
			return officialSkillLibraryMaterialization{}, conflict(fmt.Sprintf("reserved AgentTeam official skill slug %q is already used in this workspace", info.Slug))
		}
		if err := s.requireOfficialSkillRepositoryDependencies(); err != nil {
			return officialSkillLibraryMaterialization{}, err
		}
		skill, err = s.createOfficialSkillLibrary(ctx, workspaceID, info)
		if err != nil {
			if !isOfficialSkillLibraryCompetitionError(err) {
				return officialSkillLibraryMaterialization{}, err
			}
			ready, repo, readyErr := s.waitForOfficialSkillLibraryReady(ctx, workspaceID, info, definition, false)
			if readyErr == nil {
				return officialSkillLibraryMaterialization{Skill: ready, Repository: repo}, nil
			}
			if !errors.Is(readyErr, errOfficialSkillLibraryNotReady) {
				return officialSkillLibraryMaterialization{}, readyErr
			}
			if raced, loadErr := s.store.GetSkillBySlugAndCreator(ctx, workspaceID, info.Slug, info.SystemSubject); loadErr == nil {
				skill = raced
			} else if errors.Is(loadErr, gorm.ErrRecordNotFound) {
				return officialSkillLibraryMaterialization{}, err
			} else {
				return officialSkillLibraryMaterialization{}, mapStoreError(loadErr)
			}
		}
	}
	if strings.TrimSpace(skill.CreatedBy) != info.SystemSubject {
		return officialSkillLibraryMaterialization{}, conflict(fmt.Sprintf("reserved AgentTeam official skill slug %q is already used in this workspace", info.Slug))
	}

	native := nativeSkills[skill.ID]
	if nativeSkills == nil {
		native, err = s.nativeSkillRead(ctx, skill.WorkspaceID, skill.ID)
		if err != nil {
			return officialSkillLibraryMaterialization{}, err
		}
	}
	info = resolveOfficialSkillLibraryInfo(info, skill)
	if shouldImportOfficialSkillLibrary(skill, definition, info) {
		if ready, repo, readyErr := s.waitForOfficialSkillLibraryReady(ctx, workspaceID, info, definition, native); readyErr == nil {
			return officialSkillLibraryMaterialization{Skill: ready, Repository: repo}, nil
		} else if !errors.Is(readyErr, errOfficialSkillLibraryNotReady) {
			return officialSkillLibraryMaterialization{}, readyErr
		}
		return s.mutateOfficialSkillLibrary(ctx, skill, info, true)
	}
	if native {
		return officialSkillLibraryMaterialization{Skill: skill}, nil
	}
	if err := s.requireOfficialSkillRepositoryDependencies(); err != nil {
		return officialSkillLibraryMaterialization{}, err
	}
	repo, err := s.repositoryStore.Get(ctx, skill.WorkspaceID, reposstore.ResourceTypeSkillRepository, skill.ID, reposstore.RepoRolePrimary)
	if err == nil && strings.TrimSpace(repo.Visibility) == info.Visibility && repo.UserVisible {
		return officialSkillLibraryMaterialization{Skill: skill, Repository: &repo}, nil
	}
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return officialSkillLibraryMaterialization{}, err
	}
	return s.mutateOfficialSkillLibrary(ctx, skill, info, false)
}

// mutateOfficialSkillLibrary is the only official-library owner-selection
// seam. It selects legacy or native once for the operation; a concurrent
// one-way activation may overtake a legacy mutation by cutover contract.
func (s *Service) mutateOfficialSkillLibrary(ctx context.Context, skill store.Skill, info presets.SkillLibraryInfo, importContent bool) (materialized officialSkillLibraryMaterialization, err error) {
	writeRoute, err := s.beginSkillWrite(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return officialSkillLibraryMaterialization{}, err
	}
	return s.mutateOfficialSkillLibraryForRoute(ctx, skill, info, importContent, writeRoute)
}

func (s *Service) mutateOfficialSkillLibraryForRoute(ctx context.Context, skill store.Skill, info presets.SkillLibraryInfo, importContent bool, writeRoute skillWriteRoute) (officialSkillLibraryMaterialization, error) {
	if writeRoute.native {
		if importContent {
			updated, err := s.importOfficialSkillContentForRouteWithRoute(ctx, skill, info, writeRoute, nil)
			if err != nil {
				return officialSkillLibraryMaterialization{}, err
			}
			skill = updated
		}
		return officialSkillLibraryMaterialization{Skill: skill}, nil
	}
	if err := s.requireOfficialSkillRepositoryDependencies(); err != nil {
		return officialSkillLibraryMaterialization{}, err
	}
	repo, created, err := s.ensureOfficialSkillRepositoryMapping(ctx, skill, info)
	if err != nil {
		return officialSkillLibraryMaterialization{}, err
	}
	if importContent || created {
		updated, err := s.importOfficialSkillContentForRouteWithRoute(ctx, skill, info, writeRoute, &repo)
		if err != nil {
			return officialSkillLibraryMaterialization{}, err
		}
		skill = updated
	}
	repo, err = s.ensureOfficialSkillRepositoryBrowseable(ctx, repo, info)
	if err != nil {
		return officialSkillLibraryMaterialization{}, err
	}
	return officialSkillLibraryMaterialization{Skill: skill, Repository: &repo}, nil
}

func (s *Service) requireOfficialSkillRepositoryDependencies() error {
	if s.repositoryStore == nil || s.repositories == nil || s.repositoryContent == nil {
		return invalidArgument("official skill libraries require skill repositories to be configured")
	}
	return nil
}

func shouldImportOfficialSkillLibrary(skill store.Skill, definition officialSkillLibraryDefinition, info presets.SkillLibraryInfo) bool {
	if skill.SkillCount != len(definition.SkillNames) {
		return true
	}
	return strings.TrimSpace(skill.OfficialSourceFingerprint) != strings.TrimSpace(info.SourceFingerprint)
}

func (s *Service) createOfficialSkillLibrary(ctx context.Context, workspaceID string, info presets.SkillLibraryInfo) (store.Skill, error) {
	created, err := s.CreateSkill(ctx, workspaceID, info.SystemSubject, SkillCreateMutation{
		Name:                  info.Name,
		RepoNameOverride:      info.Slug,
		Description:           info.Description,
		StorageScope:          reposservice.RepositoryStorageScopeWorkspace,
		VisibilityScope:       SkillVisibilityScopeWorkspace,
		TemplateLocale:        info.TemplateLocale,
		RepositoryVisibility:  info.Visibility,
		RepositoryUserVisible: boolPointer(true),
		Actor:                 officialSkillSystemActor(info.SystemSubject),

		allowExistingRepository:      true,
		skipRepositoryInitialization: true,
	})
	if err != nil {
		return store.Skill{}, err
	}
	return s.importOfficialSkillContent(ctx, workspaceID, created.ID, info)
}

var errOfficialSkillLibraryNotReady = errors.New("official skill library is not ready")

func (s *Service) waitForOfficialSkillLibraryReady(ctx context.Context, workspaceID string, info presets.SkillLibraryInfo, definition officialSkillLibraryDefinition, native bool) (store.Skill, *reposstore.ResourceRepository, error) {
	backoff := officialSkillLibraryReadyPollBackoff
	for attempt := 0; attempt < officialSkillLibraryReadyPolls; attempt++ {
		skill, repo, err := s.loadReadyOfficialSkillLibrary(ctx, workspaceID, info, definition, native)
		if err == nil {
			return skill, repo, nil
		}
		if !errors.Is(err, errOfficialSkillLibraryNotReady) {
			return store.Skill{}, nil, err
		}
		if attempt == officialSkillLibraryReadyPolls-1 {
			break
		}
		timer := time.NewTimer(backoff)
		select {
		case <-ctx.Done():
			timer.Stop()
			return store.Skill{}, nil, ctx.Err()
		case <-timer.C:
		}
	}
	return store.Skill{}, nil, errOfficialSkillLibraryNotReady
}

func (s *Service) loadReadyOfficialSkillLibrary(ctx context.Context, workspaceID string, info presets.SkillLibraryInfo, definition officialSkillLibraryDefinition, native bool) (store.Skill, *reposstore.ResourceRepository, error) {
	skill, err := s.store.GetSkillBySlugAndCreator(ctx, workspaceID, info.Slug, info.SystemSubject)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return store.Skill{}, nil, errOfficialSkillLibraryNotReady
		}
		return store.Skill{}, nil, mapStoreError(err)
	}
	resolvedInfo := resolveOfficialSkillLibraryInfo(info, skill)
	if shouldImportOfficialSkillLibrary(skill, definition, resolvedInfo) {
		return store.Skill{}, nil, errOfficialSkillLibraryNotReady
	}
	if native {
		return skill, nil, nil
	}
	repo, err := s.repositoryStore.Get(ctx, skill.WorkspaceID, reposstore.ResourceTypeSkillRepository, skill.ID, reposstore.RepoRolePrimary)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return store.Skill{}, nil, errOfficialSkillLibraryNotReady
		}
		return store.Skill{}, nil, err
	}
	return skill, &repo, nil
}

func isOfficialSkillLibraryCompetitionError(err error) bool {
	return errors.Is(err, ErrConflict) ||
		errors.Is(err, reposservice.ErrConflict) ||
		errors.Is(err, store.ErrSkillSlugConflict) ||
		errors.Is(err, store.ErrRepositoryBranchConflict)
}

func resolveOfficialSkillLibraryInfo(defaultInfo presets.SkillLibraryInfo, skill store.Skill) presets.SkillLibraryInfo {
	if localized, ok := matchOfficialSkillLibraryInfo(defaultInfo.Slug, skill.Name, skill.Description); ok {
		return localized
	}
	return defaultInfo
}

func matchOfficialSkillLibraryInfo(slug, name, description string) (presets.SkillLibraryInfo, bool) {
	trimmedName := strings.TrimSpace(name)
	trimmedDescription := strings.TrimSpace(description)
	if trimmedName == "" && trimmedDescription == "" {
		return presets.SkillLibraryInfo{}, false
	}
	for _, locale := range []string{
		presets.LocaleEnglish,
		presets.LocaleSimplifiedChinese,
		presets.LocaleJapanese,
	} {
		info, err := presets.OfficialSkillLibrary(slug, locale)
		if err != nil {
			continue
		}
		if trimmedName == strings.TrimSpace(info.Name) &&
			trimmedDescription == strings.TrimSpace(info.Description) {
			return info, true
		}
	}
	return presets.SkillLibraryInfo{}, false
}

// IsOfficialBuiltinSkill reports whether skill is a registered AgentTeam-maintained
// official Skill library.
func IsOfficialBuiltinSkill(skill store.Skill) bool {
	definition, ok := officialSkillLibraryDefinitionsBySlug[strings.TrimSpace(skill.Slug)]
	return ok && strings.TrimSpace(skill.CreatedBy) == definition.DefaultInfo.SystemSubject
}

func officialSkillLibraryIdentities() []store.SkillIdentity {
	identities := make([]store.SkillIdentity, 0, len(officialSkillLibraryDefinitions))
	for _, definition := range officialSkillLibraryDefinitions {
		identities = append(identities, store.SkillIdentity{
			Slug:      definition.DefaultInfo.Slug,
			CreatedBy: definition.DefaultInfo.SystemSubject,
		})
	}
	return identities
}

func resolveExistingOfficialSkillLibraryInfo(skill store.Skill) (presets.SkillLibraryInfo, error) {
	definition, ok := officialSkillLibraryDefinitionsBySlug[strings.TrimSpace(skill.Slug)]
	if !ok || strings.TrimSpace(skill.CreatedBy) != definition.DefaultInfo.SystemSubject {
		return presets.SkillLibraryInfo{}, invalidArgument("skill is not a registered official skill library")
	}
	return resolveOfficialSkillLibraryInfo(definition.DefaultInfo, skill), nil
}

// ensureOfficialSkillRepositoryMapping performs only the legacy repository
// mapping repair after the caller selected the legacy route.
func (s *Service) ensureOfficialSkillRepositoryMapping(ctx context.Context, skill store.Skill, info presets.SkillLibraryInfo) (reposstore.ResourceRepository, bool, error) {
	repo, err := s.repositoryStore.Get(
		ctx,
		skill.WorkspaceID,
		reposstore.ResourceTypeSkillRepository,
		skill.ID,
		reposstore.RepoRolePrimary,
	)
	if err == nil {
		return repo, false, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return reposstore.ResourceRepository{}, false, err
	}

	storage, resolveErr := reposservice.ResolveRepositoryStorage(
		skill.WorkspaceID,
		info.SystemSubject,
		skill.StorageScope,
	)
	if resolveErr != nil {
		return reposstore.ResourceRepository{}, false, resolveErr
	}
	repoIdentity, ensureErr := s.repositories.EnsureRepository(ctx, reposservice.AGSRepositoryEnsureRequest{
		WorkspaceID:       skill.WorkspaceID,
		ResourceType:      reposstore.ResourceTypeSkillRepository,
		ResourceID:        skill.ID,
		RepoRole:          reposstore.RepoRolePrimary,
		Owner:             storage.OwnerLogin,
		Name:              skillRepositoryName(info.Slug),
		Description:       "AgentTeam Skill repository for " + skill.Name,
		Kind:              "skill_repository_canonical",
		Visibility:        info.Visibility,
		UserVisible:       true,
		AutoInit:          false,
		RejectExisting:    false,
		NameConflictField: reposservice.RepositoryNameFieldName,
		DefaultBranch:     "main",
		Features:          reposservice.AGSRepositoryFeatures{Issues: true, Wiki: true},
		Actor:             officialSkillSystemActor(info.SystemSubject),
	})
	if ensureErr != nil {
		return reposstore.ResourceRepository{}, false, fmt.Errorf("ensure official skill repository %q: %w", info.Slug, ensureErr)
	}

	now := s.now()
	repoUpsert, upsertErr := s.repositoryStore.Upsert(ctx, reposservice.NewResourceRepository(reposservice.MappingInput{
		ID:           newSkillResourceRepositoryID(),
		WorkspaceID:  skill.WorkspaceID,
		ResourceType: reposstore.ResourceTypeSkillRepository,
		ResourceID:   skill.ID,
		RepoRole:     reposstore.RepoRolePrimary,
		OwnerKind:    storage.OwnerKind,
		Repository: reposservice.EnsuredRepository{
			AGSRepoID:     repoIdentity.ID,
			Owner:         repoIdentity.Owner,
			Name:          repoIdentity.Name,
			FullName:      repoIdentity.FullName,
			DefaultBranch: firstNonEmpty(repoIdentity.DefaultBranch, "main"),
			Visibility:    info.Visibility,
			UserVisible:   true,
		},
		DefaultRef:  firstNonEmpty(repoIdentity.DefaultBranch, "main"),
		Visibility:  info.Visibility,
		UserVisible: true,
		ActorID:     info.SystemSubject,
		Now:         now,
	}))
	if upsertErr != nil {
		return reposstore.ResourceRepository{}, false, upsertErr
	}
	return repoUpsert.Repository, true, nil
}

func (s *Service) rebuildOfficialSkillRepository(ctx context.Context, skill store.Skill) (repo reposstore.ResourceRepository, err error) {
	if s == nil {
		return reposstore.ResourceRepository{}, invalidArgument("skill service is not configured")
	}
	info, err := resolveExistingOfficialSkillLibraryInfo(skill)
	if err != nil {
		return reposstore.ResourceRepository{}, err
	}
	if s.repositoryStore == nil {
		return reposstore.ResourceRepository{}, invalidArgument("resource repository store is not configured")
	}
	writeRoute, err := s.beginSkillWrite(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return reposstore.ResourceRepository{}, err
	}
	if writeRoute.native {
		return reposstore.ResourceRepository{}, invalidArgument("official skill repository is no longer the active owner")
	}
	if _, err := s.repositoryStore.DeleteForResource(ctx, skill.WorkspaceID, reposstore.ResourceTypeSkillRepository, skill.ID); err != nil {
		return reposstore.ResourceRepository{}, err
	}
	materialized, err := s.mutateOfficialSkillLibraryForRoute(ctx, skill, info, false, writeRoute)
	if err != nil {
		return reposstore.ResourceRepository{}, err
	}
	if materialized.Repository == nil {
		return reposstore.ResourceRepository{}, invalidArgument("official skill repository is no longer the active owner")
	}
	return *materialized.Repository, nil
}

func officialSkillSystemActor(subject string) reposservice.ResourceRepositoryActor {
	return reposservice.ResourceRepositoryActor{
		Provider: "agentteam",
		Subject:  subject,
		Login:    subject,
	}
}

type repositoryVisibilityUpdater interface {
	UpdateRepositoryVisibility(context.Context, reposservice.RepositoryIdentity, string) (reposservice.RepositoryIdentity, error)
}

func (s *Service) ensureOfficialSkillRepositoryBrowseable(ctx context.Context, repo reposstore.ResourceRepository, info presets.SkillLibraryInfo) (reposstore.ResourceRepository, error) {
	if strings.TrimSpace(repo.Visibility) == info.Visibility && repo.UserVisible {
		return repo, nil
	}
	updated := repo
	if updater, ok := s.repositories.(repositoryVisibilityUpdater); ok {
		resolved, err := updater.UpdateRepositoryVisibility(ctx, reposservice.RepositoryIdentity{
			ID:            repo.AGSRepoID,
			Owner:         repo.RepoOwner,
			Name:          repo.RepoName,
			FullName:      repo.RepoFullName,
			DefaultBranch: repo.DefaultRef,
		}, info.Visibility)
		if err != nil {
			return reposstore.ResourceRepository{}, err
		}
		if strings.TrimSpace(resolved.ID) != "" {
			updated.AGSRepoID = strings.TrimSpace(resolved.ID)
		}
		if strings.TrimSpace(resolved.Owner) != "" {
			updated.RepoOwner = strings.TrimSpace(resolved.Owner)
		}
		if strings.TrimSpace(resolved.Name) != "" {
			updated.RepoName = strings.TrimSpace(resolved.Name)
		}
		if strings.TrimSpace(resolved.FullName) != "" {
			updated.RepoFullName = strings.TrimSpace(resolved.FullName)
		}
		if strings.TrimSpace(resolved.DefaultBranch) != "" {
			updated.DefaultRef = strings.TrimSpace(resolved.DefaultBranch)
		}
	} else {
		return reposstore.ResourceRepository{}, invalidArgument("official skill repository requires visibility updates to be configured")
	}
	updated.Visibility = info.Visibility
	updated.UserVisible = true
	updated.UpdatedBy = info.SystemSubject
	updated.UpdatedAt = s.now()
	result, err := s.repositoryStore.Upsert(ctx, updated)
	if err != nil {
		return reposstore.ResourceRepository{}, err
	}
	return result.Repository, nil
}

func (s *Service) importOfficialSkillContent(ctx context.Context, workspaceID, skillID string, info presets.SkillLibraryInfo) (updated store.Skill, err error) {
	skill, err := s.store.GetSkill(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(skillID))
	if err != nil {
		return store.Skill{}, mapStoreError(err)
	}
	writeRoute, err := s.beginSkillWrite(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return store.Skill{}, err
	}
	var repo *reposstore.ResourceRepository
	if !writeRoute.native {
		primary, loadErr := s.repositoryStore.Get(ctx, skill.WorkspaceID, reposstore.ResourceTypeSkillRepository, skill.ID, reposstore.RepoRolePrimary)
		if loadErr != nil {
			return store.Skill{}, loadErr
		}
		repo = &primary
	}
	return s.importOfficialSkillContentForRouteWithRoute(ctx, skill, info, writeRoute, repo)
}

// importOfficialSkillContentForRoute reuses the already selected authority
// owner. It deliberately does not call ImportSkillContent, which would acquire
// a second authority snapshot and could select a different owner mid-operation.
// importOfficialSkillContentForRoute retains the historical test seam while
// production callers use the generation-carrying route below.
func (s *Service) importOfficialSkillContentForRoute(ctx context.Context, skill store.Skill, info presets.SkillLibraryInfo, native bool, repo *reposstore.ResourceRepository) (store.Skill, error) {
	return s.importOfficialSkillContentForRouteWithRoute(ctx, skill, info, skillWriteRoute{native: native}, repo)
}

func (s *Service) importOfficialSkillContentForRouteWithRoute(ctx context.Context, skill store.Skill, info presets.SkillLibraryInfo, writeRoute skillWriteRoute, repo *reposstore.ResourceRepository) (store.Skill, error) {
	archivePath, cleanup, err := writeOfficialSkillsArchive(info)
	if err != nil {
		return store.Skill{}, err
	}
	defer cleanup()
	input := SkillContentImportMutation{
		SourceFilePath: archivePath,
		SourceName:     info.ArchiveName,
		ReplaceSkills:  true,
	}
	var result SkillContentImportResult
	if writeRoute.native {
		result, err = s.importNativeSkillContent(ctx, skill, info.SystemSubject, input, info.SourceFingerprint)
	} else {
		if repo == nil {
			return store.Skill{}, invalidArgument("skill primary repository is not configured")
		}
		result, err = s.importSkillContentIntoRepository(ctx, skill, info.SystemSubject, reposservice.IdentityFromStore(*repo), SkillRepositories{Primary: repo}, input, writeRoute)
	}
	if err != nil {
		return store.Skill{}, err
	}
	if writeRoute.native || strings.TrimSpace(info.SourceFingerprint) == "" {
		return result.Skill, nil
	}
	updated, err := s.store.UpdateOfficialSourceFingerprint(ctx, skill.WorkspaceID, skill.ID, info.SourceFingerprint, info.SystemSubject, s.now())
	if err != nil {
		return store.Skill{}, err
	}
	return updated, nil
}

func writeOfficialSkillsArchive(info presets.SkillLibraryInfo) (string, func(), error) {
	resolvedLocale := presets.NormalizeLocale(info.TemplateLocale)
	skillFS, err := presets.OfficialSkillLibraryFS(info.Slug, resolvedLocale)
	if err != nil {
		return "", func() {}, err
	}
	file, err := os.CreateTemp("", skillSlug(info.Slug)+"-*.zip")
	if err != nil {
		return "", func() {}, err
	}
	path := file.Name()
	cleanup := func() { _ = os.Remove(path) }
	zw := zip.NewWriter(file)
	fileCount := 0
	walkErr := fs.WalkDir(skillFS, "skills", func(p string, entry fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if entry.IsDir() {
			return nil
		}
		fileInfo, err := entry.Info()
		if err != nil {
			return err
		}
		header, err := zip.FileInfoHeader(fileInfo)
		if err != nil {
			return err
		}
		header.Name = p
		header.Method = zip.Deflate
		writer, err := zw.CreateHeader(header)
		if err != nil {
			return err
		}
		reader, err := skillFS.Open(p)
		if err != nil {
			return err
		}
		_, copyErr := io.Copy(writer, reader)
		closeErr := reader.Close()
		if copyErr != nil {
			return copyErr
		}
		if closeErr != nil {
			return closeErr
		}
		fileCount++
		return nil
	})
	closeZipErr := zw.Close()
	closeFileErr := file.Close()
	if walkErr != nil {
		cleanup()
		return "", func() {}, walkErr
	}
	if closeZipErr != nil {
		cleanup()
		return "", func() {}, closeZipErr
	}
	if closeFileErr != nil {
		cleanup()
		return "", func() {}, closeFileErr
	}
	if fileCount == 0 {
		cleanup()
		return "", func() {}, fmt.Errorf("official skill archive %q is empty for locale %q", info.Slug, resolvedLocale)
	}
	return path, cleanup, nil
}
