package skills

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

	"github.com/tidbcloud/AgentTeam/internal/service/memory"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/access"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/activity"
	reposservice "github.com/tidbcloud/AgentTeam/internal/service/resource/repos"

	activitystore "github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
	memorystore "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"
	"golang.org/x/sync/singleflight"
	"gorm.io/gorm"
)

var (
	ErrUnauthorized                   = errors.New("unauthorized")
	ErrInvalidArgument                = errors.New("invalid argument")
	ErrForbidden                      = errors.New("forbidden")
	ErrNotFound                       = errors.New("not found")
	ErrConflict                       = errors.New("conflict")
	ErrSkillRepositorySnapshotChanged = codedConflict(SkillRepositoryErrorSnapshotChanged, "skill repository snapshot changed")
)

type domainError struct {
	kind    error
	msg     string
	code    string
	details any
}

func (e domainError) Error() string { return e.msg }
func (e domainError) Unwrap() error { return e.kind }

func invalidArgument(message string) error {
	return domainError{kind: ErrInvalidArgument, msg: message}
}
func unauthorized(message string) error { return domainError{kind: ErrUnauthorized, msg: message} }
func notFound(message string) error     { return domainError{kind: ErrNotFound, msg: message} }
func conflict(message string) error     { return domainError{kind: ErrConflict, msg: message} }
func forbidden(message string) error    { return domainError{kind: ErrForbidden, msg: message} }
func codedInvalidArgument(code string, message string) error {
	return domainError{kind: ErrInvalidArgument, msg: message, code: strings.TrimSpace(code)}
}
func codedConflict(code string, message string) error {
	return domainError{kind: ErrConflict, msg: message, code: strings.TrimSpace(code)}
}
func codedForbidden(code string, message string) error {
	return domainError{kind: ErrForbidden, msg: message, code: strings.TrimSpace(code)}
}

func Code(err error) string {
	var domain domainError
	if errors.As(err, &domain) {
		return domain.code
	}
	return ""
}

func Details(err error) any {
	var domain domainError
	if errors.As(err, &domain) {
		return domain.details
	}
	return nil
}

func HTTPStatus(err error) int {
	switch {
	case errors.Is(err, ErrUnauthorized):
		return 401
	case errors.Is(err, ErrInvalidArgument):
		return 400
	case errors.Is(err, ErrForbidden):
		return 403
	case errors.Is(err, ErrNotFound):
		return 404
	case errors.Is(err, ErrConflict), errors.Is(err, store.ErrRepositoryBranchConflict):
		return 409
	default:
		return 500
	}
}

type RepositoryContentManager interface {
	InitializeSkillRepository(context.Context, SkillRepositoryInitializeRequest) error
	ListSkillRepositorySkills(context.Context, reposservice.RepositoryIdentity) (SkillRepositoryScanData, error)
	ReadSkillRepositorySkills(context.Context, reposservice.RepositoryIdentity, SkillRepositoryContentsRequest) (SkillRepositoryContents, error)
	ImportSkillRepository(context.Context, SkillRepositoryImportRequest) (SkillRepositoryImportResult, error)
}

type ServiceOptions struct {
	RepositoryContentManager     RepositoryContentManager
	RepositoryDeleter            reposservice.RepositoryDeleter
	RepositoryBackups            memory.MemoryRepositoryBackupStore
	AgentOwnershipSource         reposservice.AgentOwnershipSource
	AssetAccessOutbox            *memorystore.Store
	SyncTokenIssuer              reposservice.RepositorySyncTokenIssuer
	ActivityStore                *activitystore.Store
	ActivityProjector            activity.ResourceActivityProjectorRefresher
	ActivityRefreshAfter         time.Duration
	EnableOfficialSkillLibraries bool
	ContentAuthority             skillAuthorityReader
	NativeContent                NativeSkillContent
	NativeContentEnabled         bool
	MigrationWorker              skillContentMigrationWorker
}

type Service struct {
	store                         *store.Store
	repositoryStore               *reposstore.Store
	repositories                  reposservice.RepositoryEnsurer
	repositoryDeleter             reposservice.RepositoryDeleter
	repositoryContent             RepositoryContentManager
	repositoryBackups             memory.MemoryRepositoryBackupStore
	agentOwnershipSource          reposservice.AgentOwnershipSource
	assetAccessOutbox             *memorystore.Store
	syncTokenIssuer               reposservice.RepositorySyncTokenIssuer
	activityStore                 *activitystore.Store
	activityProjector             activity.ResourceActivityProjectorRefresher
	activityRefreshAfter          time.Duration
	officialSkillLibrariesEnabled bool
	contentAuthority              skillAuthorityReader
	nativeContent                 NativeSkillContent
	nativeContentEnabled          bool
	migrationWorker               skillContentMigrationWorker
	officialSkillLibraryFlights   singleflight.Group
	now                           func() time.Time
}

type SkillCreateMutation struct {
	Name                  string
	RepoNameOverride      string
	Description           string
	StorageScope          string
	VisibilityScope       string
	TemplateLocale        string
	RepositoryVisibility  string
	RepositoryUserVisible *bool
	IsActive              *bool
	Actor                 reposservice.ResourceRepositoryActor

	allowExistingRepository      bool
	skipRepositoryInitialization bool
}

type SkillUpdateMutation struct {
	Name            *string
	Slug            *string
	Description     *string
	IsActive        *bool
	VisibilityScope *string
	Actor           reposservice.ResourceRepositoryActor
}

type SkillContentImportMutation struct {
	SourceFilePath string
	SourceName     string
	SourceURL      string
	ReplaceSkills  bool
}

type SkillContentImportResult struct {
	Skill          store.Skill
	Repositories   SkillRepositories
	CommitSHA      string
	VersionID      string
	SkillCount     int
	ImportedSkills []SkillRepositoryDetectedSkill
	Warnings       []SkillImportWarning
}

type AgentSkillBindingMutation struct {
	// Branch is an explicit repository branch selected by the public Skill API.
	// Native Skills reject it because repository identity is not part of their
	// binding contract.
	Branch string
	// LegacyBranchHint is used by internal callers that are replaying a legacy
	// projection (for example Role realization). The Skill owner decides under
	// its selected authority route whether the hint is still applicable; native
	// bindings ignore it instead of leaking a repository branch.
	LegacyBranchHint string
}

type agentSkillBindingTarget struct {
	native      bool
	primary     *reposstore.ResourceRepository
	branch      string
	activityRef string
}

type AgentSkillBindingResult struct {
	AgentID string
	SkillID string
	Branch  string
	Unbound bool
}

type AgentSkillBindingSummary struct {
	ID          string
	WorkspaceID string
	AgentID     string
	SkillID     string
	Branch      string
	CreatedBy   string
	UpdatedBy   string
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

type AgentSkillBindingSkill struct {
	ID               string
	WorkspaceID      string
	Slug             string
	Name             string
	Description      string
	ContentID        string
	ContentVersionID string
	IsActive         bool
	Role             string
	CanManage        bool
	CanBind          bool
	Repository       *reposstore.ResourceRepository
	UpdatedAt        time.Time
}

type AgentSkillBindingsSummary struct {
	OwnedAgents []reposservice.AgentOwnership
	Bindings    []AgentSkillBindingSummary
	Skills      []AgentSkillBindingSkill
}

type AgentAccessibleSkill struct {
	ID               string
	WorkspaceID      string
	Name             string
	Description      string
	ContentID        string
	ContentVersionID string
	Repository       *reposstore.ResourceRepository
	Branch           string
	PermissionSetAt  time.Time
	UpdatedAt        time.Time
}

type AgentSkillAccessBatch struct {
	Agents           map[string][]AgentAccessibleSkill
	SyncToken        string
	SyncTokenExpires time.Time
}

type SkillRepositories struct {
	Primary *reposstore.ResourceRepository
}

type SkillListOptions struct {
	Query          string
	Page           int
	PerPage        int
	TemplateLocale string
	View           string
}

type SkillPage struct {
	Items   []store.Skill
	Total   int64
	Page    int
	PerPage int
	HasMore bool
}

const (
	SkillVisibilityScopePrivate   = "private"
	SkillVisibilityScopeWorkspace = "workspace"
)

type resolvedSkillCreateVisibility struct {
	VisibilityScope   string
	VisibilityOwnerID string
	StorageScope      string
}

type AgentSkillBindingsOptions struct {
	TemplateLocale string
}

type AgentSkillAccessBatchOptions struct {
	AgentIDs       []string
	TemplateLocale string
}

func New(store *store.Store, repositoryStore *reposstore.Store, repositories reposservice.RepositoryEnsurer, now func() time.Time) *Service {
	options := ServiceOptions{}
	return NewWithOptions(store, repositoryStore, repositories, now, options)
}

func NewWithOptions(store *store.Store, repositoryStore *reposstore.Store, repositories reposservice.RepositoryEnsurer, now func() time.Time, options ServiceOptions) *Service {
	if now == nil {
		now = func() time.Time { return time.Now().UTC() }
	}
	deleter := options.RepositoryDeleter
	if deleter == nil {
		if resolved, ok := repositories.(reposservice.RepositoryDeleter); ok {
			deleter = resolved
		}
	}
	return &Service{
		store:                         store,
		repositoryStore:               repositoryStore,
		repositories:                  repositories,
		repositoryDeleter:             deleter,
		repositoryContent:             options.RepositoryContentManager,
		repositoryBackups:             options.RepositoryBackups,
		agentOwnershipSource:          options.AgentOwnershipSource,
		assetAccessOutbox:             options.AssetAccessOutbox,
		syncTokenIssuer:               options.SyncTokenIssuer,
		activityStore:                 options.ActivityStore,
		activityProjector:             options.ActivityProjector,
		activityRefreshAfter:          activity.NormalizeResourceActivityRefreshAfter(options.ActivityRefreshAfter),
		officialSkillLibrariesEnabled: options.EnableOfficialSkillLibraries,
		contentAuthority:              options.ContentAuthority,
		nativeContent:                 options.NativeContent,
		nativeContentEnabled:          options.NativeContentEnabled,
		migrationWorker:               options.MigrationWorker,
		now:                           now,
	}
}

func (s *Service) CreateSkill(ctx context.Context, workspaceID, subject string, input SkillCreateMutation) (created store.Skill, err error) {
	if s == nil || s.store == nil {
		return store.Skill{}, invalidArgument("skill service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return store.Skill{}, err
	}
	workspace := strings.TrimSpace(workspaceID)
	name := strings.TrimSpace(input.Name)
	if name == "" {
		return store.Skill{}, invalidArgument("skill name is required")
	}
	description := strings.TrimSpace(input.Description)
	if description == "" {
		description = "Skill repository for " + name
	}
	if s.repositories == nil {
		return store.Skill{}, invalidArgument("skill repository provisioner is not configured")
	}
	if s.repositoryStore == nil {
		return store.Skill{}, invalidArgument("resource repository store is not configured")
	}
	if s.repositoryContent == nil {
		return store.Skill{}, invalidArgument("skill repository content manager is not configured")
	}
	repoVisibility, err := normalizeSkillRepositoryVisibility(input.RepositoryVisibility)
	if err != nil {
		return store.Skill{}, err
	}
	repoUserVisible := false
	if input.RepositoryUserVisible != nil {
		repoUserVisible = *input.RepositoryUserVisible
	}
	now := s.now()
	slug, field, err := reposservice.ResolveRepositoryNameStem(name, input.RepoNameOverride, "skill", ".sk", "skill")
	if err != nil {
		return store.Skill{}, err
	}
	visibility, err := resolveSkillCreateVisibility(workspace, subject, input)
	if err != nil {
		return store.Skill{}, err
	}
	storage, err := reposservice.ResolveRepositoryStorage(workspace, subject, visibility.StorageScope)
	if err != nil {
		return store.Skill{}, err
	}
	skill := store.Skill{ID: newSkillID(), WorkspaceID: workspace, Slug: slug, Name: name, Description: description, StorageScope: storage.Scope, StorageOwnerLogin: storage.OwnerLogin, VisibilityScope: visibility.VisibilityScope, VisibilityOwnerID: visibility.VisibilityOwnerID, SkillCount: 1, IsActive: true, CreatedBy: strings.TrimSpace(subject), UpdatedBy: strings.TrimSpace(subject), CreatedAt: now, UpdatedAt: now}
	if input.IsActive != nil {
		skill.IsActive = *input.IsActive
	}
	writeRoute, err := s.beginSkillWrite(ctx, workspace, skill.ID)
	if err != nil {
		return store.Skill{}, err
	}
	if writeRoute.native {
		// Native creation stays disabled until a one-shot exact reservation can
		// bind a server-generated Skill ID. Persistent wildcard policy must not
		// capture future Skills.
		return store.Skill{}, fmt.Errorf("native skill creation requires an exact one-shot reservation")
	}
	repoOwner := storage.OwnerLogin
	repoName := skillRepositoryName(slug)
	suggestedRepoName, err := reposservice.FirstAvailableRepositoryNameStem(ctx, s.repositoryStore, repoOwner, slug, nil, ".sk")
	if err != nil {
		return store.Skill{}, err
	}
	if err := reposservice.EnsureRepositoryFullNameAvailableForField(ctx, s.repositoryStore, repoOwner+"/"+repoName, reposstore.ResourceTypeSkillRepository, skill.ID, reposstore.RepoRolePrimary, field, suggestedRepoName); err != nil {
		return store.Skill{}, err
	}
	repo, err := s.repositories.EnsureRepository(ctx, reposservice.AGSRepositoryEnsureRequest{
		WorkspaceID:       workspace,
		ResourceType:      reposstore.ResourceTypeSkillRepository,
		ResourceID:        skill.ID,
		RepoRole:          reposstore.RepoRolePrimary,
		Owner:             repoOwner,
		Name:              repoName,
		Description:       "AgentTeam Skill repository for " + name,
		Kind:              "skill_repository_canonical",
		Visibility:        repoVisibility,
		UserVisible:       repoUserVisible,
		AutoInit:          false,
		RejectExisting:    !input.allowExistingRepository,
		NameConflictField: field,
		SuggestedRepoName: suggestedRepoName,
		DefaultBranch:     "main",
		Features:          reposservice.AGSRepositoryFeatures{Issues: true, Wiki: true},
		Actor:             input.Actor,
	})
	if err != nil {
		return store.Skill{}, fmt.Errorf("ensure skill repository: %w", err)
	}
	if !input.skipRepositoryInitialization {
		authorName, authorEmail := reposservice.RepositoryCommitAuthorFromActor(input.Actor, "MeshX Skills", "meshx-skills@agentteam.local")
		if err := s.repositoryContent.InitializeSkillRepository(ctx, SkillRepositoryInitializeRequest{
			Repository:  repo,
			Slug:        slug,
			Name:        name,
			Description: description,
			Locale:      input.TemplateLocale,
			Message:     presets.SkillScaffoldInitialCommitMessage(input.TemplateLocale),
			AuthorName:  authorName,
			AuthorEmail: authorEmail,
		}); err != nil {
			failureRepos := skillRepoFailureRecords(skill, storage, repo, subject, now)
			_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, failureRepos, reposstore.ResourceRepositoryLifecycleOperationCreateBind, subject, now, err)
			s.recordSkillRepoLifecycleFailedActivity(ctx, failureRepos, subject, now, err)
			return store.Skill{}, fmt.Errorf("initialize skill repository: %w", err)
		}
	}
	created, err = s.persistSkill(ctx, subject, skill, repo, storage, firstNonEmpty(repo.DefaultBranch, "main"), repoVisibility, repoUserVisible, now, skillActivityCreated, map[string]any{"name": skill.Name, "slug": skill.Slug, "skillCount": skill.SkillCount})
	if err != nil {
		failureRepos := skillRepoFailureRecords(skill, storage, repo, subject, now)
		_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, failureRepos, reposstore.ResourceRepositoryLifecycleOperationCreateBind, subject, now, err)
		s.recordSkillRepoLifecycleFailedActivity(ctx, failureRepos, subject, now, err)
		return store.Skill{}, err
	}
	s.decorateSkillAccess(ctx, &created, subject)
	return created, nil
}

func (s *Service) persistSkill(ctx context.Context, subject string, skill store.Skill, repo reposservice.RepositoryIdentity, storage reposservice.RepositoryStorage, defaultRef string, repoVisibility string, repoUserVisible bool, now time.Time, eventType string, details map[string]any) (store.Skill, error) {
	subject = strings.TrimSpace(subject)
	repoVisibility, err := normalizeSkillRepositoryVisibility(repoVisibility)
	if err != nil {
		return store.Skill{}, err
	}
	var created store.Skill
	err = s.store.Transaction(ctx, func(tx *store.Store) error {
		var err error
		created, err = tx.CreateSkill(ctx, skill)
		if err != nil {
			return err
		}
		if _, err := tx.UpsertSkillGrant(ctx, store.SkillGrant{
			ID:          newSkillGrantID(),
			WorkspaceID: created.WorkspaceID,
			SkillID:     created.ID,
			SubjectType: "user",
			SubjectID:   subject,
			Role:        "owner",
			CreatedBy:   subject,
			UpdatedBy:   subject,
			CreatedAt:   now,
			UpdatedAt:   now,
		}); err != nil {
			return err
		}
		if created.VisibilityScope == SkillVisibilityScopeWorkspace {
			if _, err := tx.UpsertSkillGrant(ctx, store.SkillGrant{
				ID:          newSkillGrantID(),
				WorkspaceID: created.WorkspaceID,
				SkillID:     created.ID,
				SubjectType: "workspace",
				SubjectID:   created.WorkspaceID,
				Role:        "view",
				CreatedBy:   subject,
				UpdatedBy:   subject,
				CreatedAt:   now,
				UpdatedAt:   now,
			}); err != nil {
				return err
			}
		}
		repoTx := s.repositoryStore.WithDB(tx.DB())
		repoUpsert, err := repoTx.Upsert(ctx, reposservice.NewResourceRepository(reposservice.MappingInput{
			ID:           newSkillResourceRepositoryID(),
			WorkspaceID:  created.WorkspaceID,
			ResourceType: reposstore.ResourceTypeSkillRepository,
			ResourceID:   created.ID,
			RepoRole:     reposstore.RepoRolePrimary,
			OwnerKind:    storage.OwnerKind,
			Repository: reposservice.EnsuredRepository{
				AGSRepoID:     repo.ID,
				Owner:         repo.Owner,
				Name:          repo.Name,
				FullName:      repo.FullName,
				DefaultBranch: repo.DefaultBranch,
				Visibility:    repoVisibility,
				UserVisible:   repoUserVisible,
			},
			DefaultRef:  defaultRef,
			Visibility:  repoVisibility,
			UserVisible: repoUserVisible,
			ActorID:     subject,
			Now:         now,
		}))
		if err != nil {
			return err
		}
		if repoUpsert.Created {
			if err := activity.WriteResourceRepositoryLifecycleActivity(ctx, s.skillActivityStore(tx), repoUpsert.Repository, activity.ResourceRepositoryActivityCreated, subject, now); err != nil {
				return err
			}
		}
		return writeSkillActivity(ctx, s.skillActivityStore(tx), created, repo.FullName, eventType, subject, "resource", created.ID, details, now)
	})
	if err != nil {
		return store.Skill{}, mapStoreError(err)
	}
	return created, nil
}

func (s *Service) ListSkills(ctx context.Context, workspaceID, subject string, options SkillListOptions) (SkillPage, error) {
	if s == nil || s.store == nil {
		return SkillPage{}, invalidArgument("skill service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return SkillPage{}, err
	}
	officialLibraries, err := s.ensureOfficialSkillLibraries(ctx, workspaceID, options.TemplateLocale)
	if err != nil {
		return SkillPage{}, err
	}
	opts := store.SkillListOptions{
		WorkspaceID:    workspaceID,
		SubjectID:      subject,
		Query:          options.Query,
		View:           normalizeSkillListView(options.View),
		OfficialSkills: officialSkillLibraryIdentities(),
		Page:           options.Page,
		PerPage:        options.PerPage,
	}
	if s.officialSkillLibrariesEnabled {
		opts.AlwaysVisibleSkills = officialSkillLibraryIdentities()
	}
	maxVisibleRows := 0
	if s.contentAuthority != nil {
		maxVisibleRows = store.MaxAgentAccessibleSkillRows
	}
	visible, err := s.store.ListVisibleSkills(ctx, workspaceID, subject, true, maxVisibleRows)
	if err != nil {
		return SkillPage{}, err
	}
	for _, library := range officialLibraries {
		visible = appendVisibleSkillIfMissing(visible, library.Skill)
	}
	if maxVisibleRows > 0 && len(visible) > maxVisibleRows {
		return SkillPage{}, fmt.Errorf("visible skill list exceeds %d rows", maxVisibleRows)
	}
	if err := s.refreshLegacySkillActivity(ctx, workspaceID, visible); err != nil {
		return SkillPage{}, err
	}
	result, err := s.store.ListSkills(ctx, opts)
	if err != nil {
		return SkillPage{}, err
	}
	for i := range result.Items {
		s.decorateSkillAccess(ctx, &result.Items[i], subject)
	}
	return SkillPage{Items: result.Items, Total: result.Total, Page: result.Page, PerPage: result.PerPage, HasMore: result.HasMore}, nil
}

func (s *Service) CountWorkspaceSkills(ctx context.Context, workspaceID string, subject string) (int64, error) {
	if s == nil || s.store == nil {
		return 0, invalidArgument("skill service is not configured")
	}
	workspace := strings.TrimSpace(workspaceID)
	if workspace == "" {
		return 0, invalidArgument("workspaceId is required")
	}
	owner := strings.TrimSpace(subject)
	if owner == "" {
		return 0, invalidArgument("subject is required")
	}
	if err := s.retireRemovedOfficialSkillLibraries(ctx, workspace); err != nil {
		return 0, err
	}
	return s.store.CountWorkspaceSkills(ctx, workspace, owner, officialSkillLibraryIdentities())
}

func (s *Service) GetSkill(ctx context.Context, workspaceID, subject, id string) (store.Skill, error) {
	if s == nil || s.store == nil {
		return store.Skill{}, invalidArgument("skill service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return store.Skill{}, err
	}
	skill, err := s.store.GetSkill(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(id))
	if err != nil {
		return store.Skill{}, mapStoreError(err)
	}
	s.decorateSkillAccess(ctx, &skill, subject)
	if strings.TrimSpace(skill.Role) == "" {
		return store.Skill{}, notFound("skill not found")
	}
	return skill, nil
}

func (s *Service) GetSkillRepositories(ctx context.Context, workspaceID, subject, id string) (SkillRepositories, error) {
	skill, err := s.GetSkill(ctx, workspaceID, subject, id)
	if err != nil {
		return SkillRepositories{}, err
	}
	native, err := s.nativeSkillRead(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return SkillRepositories{}, err
	}
	if native {
		return SkillRepositories{}, nil
	}
	if s.repositoryStore == nil {
		return SkillRepositories{}, invalidArgument("resource repository store is not configured")
	}
	repos, err := s.repositoryStore.ListForResource(ctx, skill.WorkspaceID, reposstore.ResourceTypeSkillRepository, skill.ID)
	if err != nil {
		return SkillRepositories{}, err
	}
	out := SkillRepositories{}
	for i := range repos {
		if strings.TrimSpace(repos[i].RepoRole) == reposstore.RepoRolePrimary {
			out.Primary = &repos[i]
		}
	}
	if out.Primary == nil && s.officialSkillLibrariesEnabled && IsOfficialBuiltinSkill(skill) {
		info, err := resolveExistingOfficialSkillLibraryInfo(skill)
		if err != nil {
			return SkillRepositories{}, err
		}
		materialized, err := s.mutateOfficialSkillLibrary(ctx, skill, info, false)
		if err != nil {
			return SkillRepositories{}, err
		}
		if materialized.Repository != nil {
			out.Primary = materialized.Repository
		}
	}
	return out, nil
}

func (s *Service) ListSkillRepositories(ctx context.Context, workspaceID, subject string, skillIDs []string) (map[string]SkillRepositories, error) {
	if s == nil {
		return nil, invalidArgument("skill service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return nil, err
	}
	uniqueSkillIDs := uniqueNonEmptyStrings(skillIDs)
	nativeSkills, err := s.nativeSkillReads(ctx, workspaceID, uniqueSkillIDs)
	if err != nil {
		return nil, err
	}
	legacySkillIDs := make([]string, 0, len(uniqueSkillIDs))
	for _, skillID := range uniqueSkillIDs {
		if !nativeSkills[skillID] {
			legacySkillIDs = append(legacySkillIDs, skillID)
		}
	}
	repositories, err := s.repositoriesBySkillID(ctx, workspaceID, legacySkillIDs)
	if err != nil {
		return nil, err
	}
	out := make(map[string]SkillRepositories, len(repositories))
	for skillID, repository := range repositories {
		out[skillID] = SkillRepositories{Primary: repository}
	}
	return out, nil
}

func (s *Service) ListSkillRepositorySkills(ctx context.Context, workspaceID, subject, id string) (SkillRepositoryScanData, error) {
	if s == nil {
		return SkillRepositoryScanData{}, invalidArgument("skill service is not configured")
	}
	skill, err := s.GetSkill(ctx, workspaceID, subject, id)
	if err != nil {
		return SkillRepositoryScanData{}, err
	}
	native, err := s.nativeSkillRead(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return SkillRepositoryScanData{}, err
	}
	if native {
		return s.nativeContent.List(ctx, nativeSkillRequest(skill.ID, skill.WorkspaceID, skill.ContentID, skill.ActiveContentVersionID, subject))
	}
	if s.repositoryContent == nil {
		return SkillRepositoryScanData{}, invalidArgument("skill repository content manager is not configured")
	}
	repositories, err := s.GetSkillRepositories(ctx, workspaceID, subject, skill.ID)
	if err != nil {
		return SkillRepositoryScanData{}, err
	}
	if repositories.Primary == nil {
		return SkillRepositoryScanData{}, invalidArgument("skill primary repository is not configured")
	}
	repo := reposservice.IdentityFromStore(*repositories.Primary)
	preview, err := s.repositoryContent.ListSkillRepositorySkills(ctx, repo)
	if err != nil && s.officialSkillLibrariesEnabled && IsOfficialBuiltinSkill(skill) && skillRepositoryNeedsRebuild(err) {
		rebuiltRepo, rebuildErr := s.rebuildOfficialSkillRepository(ctx, skill)
		if rebuildErr != nil {
			return SkillRepositoryScanData{}, rebuildErr
		}
		repo = reposservice.IdentityFromStore(rebuiltRepo)
		preview, err = s.repositoryContent.ListSkillRepositorySkills(ctx, repo)
	}
	if err != nil {
		return SkillRepositoryScanData{}, err
	}
	if preview.Limit == 0 {
		preview.Limit = SkillRepositoryScanLimit
	}
	return preview, nil
}

func (s *Service) ReadSkillRepositorySkills(ctx context.Context, workspaceID, subject, id string, request SkillRepositoryContentsRequest) (SkillRepositoryContents, error) {
	if s == nil {
		return SkillRepositoryContents{}, invalidArgument("skill service is not configured")
	}
	skill, err := s.GetSkill(ctx, workspaceID, subject, id)
	if err != nil {
		return SkillRepositoryContents{}, err
	}
	native, err := s.nativeSkillRead(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return SkillRepositoryContents{}, err
	}
	if native {
		return s.nativeContent.Read(ctx, nativeSkillRequest(skill.ID, skill.WorkspaceID, skill.ContentID, skill.ActiveContentVersionID, subject), request)
	}
	if s.repositoryContent == nil {
		return SkillRepositoryContents{}, invalidArgument("skill repository content manager is not configured")
	}
	repositories, err := s.GetSkillRepositories(ctx, workspaceID, subject, skill.ID)
	if err != nil {
		return SkillRepositoryContents{}, err
	}
	if repositories.Primary == nil {
		return SkillRepositoryContents{}, invalidArgument("skill primary repository is not configured")
	}
	return s.repositoryContent.ReadSkillRepositorySkills(ctx, reposservice.IdentityFromStore(*repositories.Primary), request)
}

func skillRepositoryNeedsRebuild(err error) bool {
	if err == nil {
		return false
	}
	switch Code(err) {
	case SkillImportErrorRepositoryUnavailable, SkillImportErrorNoSkillsFound:
		return true
	default:
		return false
	}
}

func (s *Service) ImportSkillContent(ctx context.Context, workspaceID, subject, id string, input SkillContentImportMutation) (output SkillContentImportResult, err error) {
	if s == nil || s.store == nil {
		return SkillContentImportResult{}, invalidArgument("skill service is not configured")
	}
	filePath := strings.TrimSpace(input.SourceFilePath)
	sourceURL := strings.TrimSpace(input.SourceURL)
	if (filePath == "") == (sourceURL == "") {
		return SkillContentImportResult{}, codedInvalidArgument(SkillImportErrorInvalidSource, "exactly one import file or url is required")
	}
	skill, err := s.GetSkill(ctx, workspaceID, subject, id)
	if err != nil {
		return SkillContentImportResult{}, err
	}
	if !skillRoleCanManage(skill.Role) {
		return SkillContentImportResult{}, codedForbidden(SkillImportErrorPermissionDenied, "skill manage permission is required")
	}
	writeRoute, err := s.beginSkillWrite(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return SkillContentImportResult{}, err
	}
	if writeRoute.native {
		return s.importNativeSkillContent(ctx, skill, strings.TrimSpace(subject), input, "")
	}
	if err := s.ensureSkillWriteCurrent(ctx, skill.WorkspaceID, skill.ID, writeRoute); err != nil {
		return SkillContentImportResult{}, err
	}
	if s.repositoryContent == nil {
		return SkillContentImportResult{}, invalidArgument("skill repository content manager is not configured")
	}
	repositories, err := s.GetSkillRepositories(ctx, workspaceID, subject, skill.ID)
	if err != nil {
		return SkillContentImportResult{}, err
	}
	if err := s.ensureSkillWriteCurrent(ctx, skill.WorkspaceID, skill.ID, writeRoute); err != nil {
		return SkillContentImportResult{}, err
	}
	if repositories.Primary == nil {
		return SkillContentImportResult{}, invalidArgument("skill primary repository is not configured")
	}
	repo := reposservice.IdentityFromStore(*repositories.Primary)
	return s.importSkillContentIntoRepository(ctx, skill, strings.TrimSpace(subject), repo, repositories, input, writeRoute)
}

func (s *Service) importNativeSkillContent(ctx context.Context, skill store.Skill, subject string, input SkillContentImportMutation, officialSourceFingerprint string) (SkillContentImportResult, error) {
	result, err := s.nativeContent.Import(ctx, nativeSkillRequest(skill.ID, skill.WorkspaceID, skill.ContentID, skill.ActiveContentVersionID, subject), input)
	if err != nil {
		return SkillContentImportResult{}, err
	}
	beforeCount := skill.SkillCount
	updated, _, err := s.finalizeNativeSkillVersion(ctx, skill, subject, result, skillActivityImported, "resource", skill.ID, map[string]any{
		"sourceName": firstNonEmpty(input.SourceName, input.SourceURL, "uploaded archive"),
		"versionId":  result.VersionID, "skillCount": result.SkillCount, "previousCount": beforeCount,
	}, officialSourceFingerprint)
	if err != nil {
		return SkillContentImportResult{}, err
	}
	s.decorateSkillAccess(ctx, &updated, subject)
	return SkillContentImportResult{
		Skill: updated, VersionID: result.VersionID, SkillCount: result.SkillCount,
		ImportedSkills: result.ImportedSkills, Warnings: result.Warnings,
	}, nil
}

func (s *Service) importSkillContentIntoRepository(ctx context.Context, skill store.Skill, subject string, repo reposservice.RepositoryIdentity, repositories SkillRepositories, input SkillContentImportMutation, writeRoute skillWriteRoute) (SkillContentImportResult, error) {
	if s == nil || s.store == nil {
		return SkillContentImportResult{}, invalidArgument("skill service is not configured")
	}
	if s.repositoryContent == nil {
		return SkillContentImportResult{}, invalidArgument("skill repository content manager is not configured")
	}
	result, err := s.repositoryContent.ImportSkillRepository(ctx, SkillRepositoryImportRequest{
		TargetRepository: repo,
		SourceFilePath:   strings.TrimSpace(input.SourceFilePath),
		SourceName:       input.SourceName,
		SourceURL:        strings.TrimSpace(input.SourceURL),
		ReplaceSkills:    input.ReplaceSkills,
		Message:          skillImportCommitMessage(input),
		AuthorName:       "MeshX Skills",
		AuthorEmail:      "meshx-skills@agentteam.local",
	})
	if err != nil {
		return SkillContentImportResult{}, err
	}
	before := skill
	skill.SkillCount = result.SkillCount
	skill.UpdatedBy = subject
	skill.UpdatedAt = s.now()
	var updated store.Skill
	err = s.store.Transaction(ctx, func(tx *store.Store) error {
		var err error
		updated, err = tx.UpdateSkill(ctx, skill)
		if err != nil {
			return err
		}
		return writeSkillActivity(ctx, s.skillActivityStore(tx), updated, firstNonEmpty(repo.FullName, repo.Owner+"/"+repo.Name), skillActivityImported, subject, "resource", updated.ID, map[string]any{
			"sourceName":    firstNonEmpty(input.SourceName, input.SourceURL, "uploaded archive"),
			"sourceURL":     input.SourceURL,
			"commitSha":     result.CommitSHA,
			"commitSHA":     result.CommitSHA,
			"skillCount":    result.SkillCount,
			"previousCount": before.SkillCount,
		}, skill.UpdatedAt)
	})
	if err != nil {
		return SkillContentImportResult{}, mapStoreError(err)
	}
	s.decorateSkillAccess(ctx, &updated, subject)
	return SkillContentImportResult{
		Skill:          updated,
		Repositories:   repositories,
		CommitSHA:      result.CommitSHA,
		SkillCount:     result.SkillCount,
		ImportedSkills: result.ImportedSkills,
		Warnings:       result.Warnings,
	}, nil
}

func (s *Service) UpdateSkill(ctx context.Context, workspaceID, subject, id string, input SkillUpdateMutation) (result store.Skill, err error) {
	skill, err := s.GetSkill(ctx, workspaceID, subject, id)
	if err != nil {
		return store.Skill{}, err
	}
	before := skill
	if !skillRoleCanManage(skill.Role) {
		return store.Skill{}, forbidden("skill requires manage access")
	}
	if input.Name != nil && strings.TrimSpace(*input.Name) != "" {
		skill.Name = strings.TrimSpace(*input.Name)
	}
	if input.Slug != nil && skillSlug(*input.Slug) != skill.Slug {
		return store.Skill{}, invalidArgument("skill URL identifier cannot be changed")
	}
	if input.Description != nil {
		skill.Description = strings.TrimSpace(*input.Description)
	}
	if input.IsActive != nil {
		skill.IsActive = *input.IsActive
	}
	visibilityRequested := input.VisibilityScope != nil
	if visibilityRequested {
		scope, ownerID, err := resolveSkillUpdateVisibility(workspaceID, subject, skill, *input.VisibilityScope)
		if err != nil {
			return store.Skill{}, err
		}
		skill.VisibilityScope = scope
		skill.VisibilityOwnerID = ownerID
	}
	skill.UpdatedBy = strings.TrimSpace(subject)
	skill.UpdatedAt = s.now()
	writeRoute, err := s.beginSkillWrite(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return store.Skill{}, err
	}
	activityRef, err := s.skillActivityRef(ctx, skill, writeRoute.native)
	if err != nil {
		return store.Skill{}, err
	}
	if err := s.ensureSkillWriteCurrent(ctx, skill.WorkspaceID, skill.ID, writeRoute); err != nil {
		return store.Skill{}, err
	}
	var updated store.Skill
	err = s.store.Transaction(ctx, func(tx *store.Store) error {
		var err error
		updated, err = tx.UpdateSkill(ctx, skill)
		if err != nil {
			return err
		}
		if visibilityRequested {
			if err := syncSkillWorkspaceVisibilityGrant(ctx, tx, updated, strings.TrimSpace(subject), skill.UpdatedAt); err != nil {
				return err
			}
		}
		return writeSkillActivity(ctx, s.skillActivityStore(tx), updated, activityRef, skillActivitySettingsChanged, strings.TrimSpace(subject), "settings", updated.ID, skillSettingsDetails(before, updated), skill.UpdatedAt)
	})
	if err != nil {
		return store.Skill{}, mapStoreError(err)
	}
	s.decorateSkillAccess(ctx, &updated, subject)
	return updated, nil
}

func syncSkillWorkspaceVisibilityGrant(ctx context.Context, tx *store.Store, skill store.Skill, subject string, now time.Time) error {
	workspaceID := strings.TrimSpace(skill.WorkspaceID)
	if skill.VisibilityScope == SkillVisibilityScopeWorkspace {
		_, err := tx.UpsertSkillGrant(ctx, store.SkillGrant{
			ID:          newSkillGrantID(),
			WorkspaceID: workspaceID,
			SkillID:     skill.ID,
			SubjectType: "workspace",
			SubjectID:   workspaceID,
			Role:        "view",
			CreatedBy:   subject,
			UpdatedBy:   subject,
			CreatedAt:   now,
			UpdatedAt:   now,
		})
		return err
	}
	grant, err := tx.GetSkillGrant(ctx, workspaceID, skill.ID, "workspace", workspaceID)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil
	}
	if err != nil {
		return err
	}
	return tx.DeleteSkillGrantByID(ctx, workspaceID, skill.ID, grant.ID)
}

// DeleteSkill permanently removes a skill and its dependent projections. The
// exact authority owner selects one path: legacy deletes managed AGS
// repositories, while native removes no remote repository and leaves Content
// Store payload retention to Content Store. A concurrent one-way activation
// may overtake legacy cleanup under the accepted cutover contract.
func (s *Service) DeleteSkill(ctx context.Context, workspaceID, subject, id string) (err error) {
	if s == nil || s.store == nil {
		return invalidArgument("skill service is not configured")
	}
	skill, err := s.GetSkill(ctx, workspaceID, subject, id)
	if err != nil {
		return err
	}
	if !skillRoleCanManage(skill.Role) {
		return forbidden("skill requires manage access")
	}
	writeRoute, err := s.beginSkillWrite(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return err
	}
	now := s.now()
	var repos []reposstore.ResourceRepository
	if !writeRoute.native {
		if s.repositoryStore == nil {
			return invalidArgument("resource repository store is not configured")
		}
		repos, err = s.repositoryStore.ListForResource(ctx, strings.TrimSpace(workspaceID), reposstore.ResourceTypeSkillRepository, skill.ID)
		if err != nil {
			return err
		}
		if err = reposservice.DeleteManagedResourceRepositories(ctx, s.repositoryStore, s.repositoryDeleter, repos, subject, now); err != nil {
			s.recordSkillRepoLifecycleFailedActivity(ctx, repos, subject, now, err)
			return err
		}
	}
	if err := s.ensureSkillWriteCurrent(ctx, skill.WorkspaceID, skill.ID, writeRoute); err != nil {
		return err
	}
	repoFullName := skillPrimaryRepoFullNameFromRepos(repos)
	err = s.store.Transaction(ctx, func(tx *store.Store) error {
		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.DeleteSkillVersionsForSkill(ctx, skill.WorkspaceID, skill.ID); err != nil {
			return err
		}
		if s.repositoryStore != nil {
			repositoryStore := s.repositoryStore.WithDB(tx.DB())
			deletedRepos, err := repositoryStore.DeleteForResource(ctx, skill.WorkspaceID, reposstore.ResourceTypeSkillRepository, skill.ID)
			if err != nil {
				return err
			}
			if writeRoute.native {
				for _, repo := range deletedRepos {
					fullName := firstNonEmpty(repo.RepoFullName, repo.RepoOwner+"/"+repo.RepoName)
					related, err := repositoryStore.ListByRepoFullName(ctx, fullName)
					if err != nil {
						return err
					}
					skillIDs := []string{skill.ID}
					for _, candidate := range related {
						if candidate.WorkspaceID == skill.WorkspaceID && candidate.ResourceType == reposstore.ResourceTypeSkillRepository && candidate.RepoRole == reposstore.RepoRolePrimary {
							skillIDs = append(skillIDs, candidate.ResourceID)
						}
					}
					if err := tx.DeleteSkillRepositoryBranchIfUnused(ctx, store.SkillRepositoryBranch{WorkspaceID: repo.WorkspaceID, RepoOwner: repo.RepoOwner, RepoName: repo.RepoName}, skillIDs); err != nil {
						return err
					}
				}
			} else {
				for _, repo := range deletedRepos {
					if err := activity.WriteResourceRepositoryLifecycleActivity(ctx, s.skillActivityStore(tx), repo, activity.ResourceRepositoryActivityDeleted, strings.TrimSpace(subject), now); err != nil {
						return err
					}
				}
			}
		}
		if err := tx.DeleteSkill(ctx, skill.WorkspaceID, skill.ID); err != nil {
			return err
		}
		details := map[string]any{"name": skill.Name}
		if writeRoute.native {
			details["contentId"] = skill.ContentID
			details["contentVersionId"] = skill.ActiveContentVersionID
		}
		return writeSkillActivity(ctx, s.skillActivityStore(tx), skill, repoFullName, skillActivityDeleted, strings.TrimSpace(subject), "resource", skill.ID, details, now)
	})
	if err != nil && !writeRoute.native {
		_ = reposservice.RecordResourceRepositoryLifecycleFailure(ctx, s.repositoryStore, repos, reposstore.ResourceRepositoryLifecycleOperationDeleteCleanup, subject, now, err)
		s.recordSkillRepoLifecycleFailedActivity(ctx, repos, subject, now, err)
	}
	return mapStoreError(err)
}

func (s *Service) resolveAgentSkillBindingTarget(ctx context.Context, workspaceID string, skill store.Skill, input AgentSkillBindingMutation, native bool) (agentSkillBindingTarget, error) {
	if native {
		if strings.TrimSpace(input.Branch) != "" {
			return agentSkillBindingTarget{}, invalidArgument("native skill binding does not accept a repository branch")
		}
		if strings.TrimSpace(skill.ContentID) == "" || strings.TrimSpace(skill.ActiveContentVersionID) == "" {
			return agentSkillBindingTarget{}, fmt.Errorf("native skill content reference is incomplete")
		}
		return agentSkillBindingTarget{
			native:      true,
			activityRef: "contentstore://" + skill.ContentID + "/" + skill.ActiveContentVersionID,
		}, nil
	}
	if s.repositoryStore == nil {
		return agentSkillBindingTarget{}, invalidArgument("skill primary repository is not configured")
	}
	primary, err := s.repositoryStore.Get(ctx, strings.TrimSpace(workspaceID), reposstore.ResourceTypeSkillRepository, skill.ID, reposstore.RepoRolePrimary)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return agentSkillBindingTarget{}, invalidArgument("skill primary repository is not configured")
		}
		return agentSkillBindingTarget{}, mapStoreError(err)
	}
	return agentSkillBindingTarget{
		primary:     &primary,
		branch:      firstNonEmpty(input.Branch, input.LegacyBranchHint, primary.DefaultRef, "main"),
		activityRef: primary.RepoFullName,
	}, nil
}

func (s *Service) BindAgentSkill(ctx context.Context, workspaceID, subject, agentID, skillID string, input AgentSkillBindingMutation) (result AgentSkillBindingResult, err error) {
	if s == nil || s.store == nil {
		return AgentSkillBindingResult{}, invalidArgument("skill service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return AgentSkillBindingResult{}, err
	}
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return AgentSkillBindingResult{}, invalidArgument("agent id is required")
	}
	if err := s.requireOwnedAgent(ctx, workspaceID, subject, agentID); err != nil {
		return AgentSkillBindingResult{}, err
	}
	skill, err := s.GetSkill(ctx, workspaceID, subject, skillID)
	if err != nil {
		return AgentSkillBindingResult{}, err
	}
	if !s.skillCanBind(skill) {
		return AgentSkillBindingResult{}, forbidden("skill requires manage access")
	}
	writeRoute, err := s.beginSkillWrite(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return AgentSkillBindingResult{}, err
	}
	target, err := s.resolveAgentSkillBindingTarget(ctx, workspaceID, skill, input, writeRoute.native)
	if err != nil {
		return AgentSkillBindingResult{}, err
	}
	if err := s.ensureSkillWriteCurrent(ctx, skill.WorkspaceID, skill.ID, writeRoute); err != nil {
		return AgentSkillBindingResult{}, err
	}
	// Idempotent short-circuit: if this agent is already bound to this skill on
	// the same branch, there is nothing to change and no client notification to
	// emit. Mirrors the memory binding behavior.
	if current, err := s.store.GetSkillAgentBinding(ctx, strings.TrimSpace(workspaceID), agentID, skill.ID); err == nil && strings.TrimSpace(current.Branch) == target.branch {
		return AgentSkillBindingResult{AgentID: current.AgentID, SkillID: current.SkillID, Branch: current.Branch}, nil
	}
	now := s.now()
	current, err := s.store.GetSkillAgentBinding(ctx, strings.TrimSpace(workspaceID), agentID, skill.ID)
	bindingFound := true
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			bindingFound = false
		} else {
			return AgentSkillBindingResult{}, mapStoreError(err)
		}
	}
	branchPinFound := false
	if !target.native && s.store.DB() != nil {
		var existingBranch store.SkillRepositoryBranch
		err = s.store.DB().WithContext(ctx).
			Where("workspace_id = ? AND repo_owner = ? AND repo_name = ?", strings.TrimSpace(workspaceID), target.primary.RepoOwner, target.primary.RepoName).
			First(&existingBranch).Error
		if err == nil {
			branchPinFound = true
		} else if !errors.Is(err, gorm.ErrRecordNotFound) {
			return AgentSkillBindingResult{}, err
		}
	}
	var binding store.SkillAgentBinding
	if err := s.store.Transaction(ctx, func(tx *store.Store) error {
		branchProjection := store.SkillRepositoryBranch{}
		if !target.native {
			branchProjection = store.SkillRepositoryBranch{ID: newSkillRepositoryBranchID(), WorkspaceID: strings.TrimSpace(workspaceID), RepoOwner: target.primary.RepoOwner, RepoName: target.primary.RepoName, Branch: target.branch, CreatedBy: strings.TrimSpace(subject), UpdatedBy: strings.TrimSpace(subject), CreatedAt: now, UpdatedAt: now}
		}
		created, err := tx.UpsertSkillAgentBinding(ctx,
			store.SkillAgentBinding{ID: newSkillAgentBindingID(), WorkspaceID: strings.TrimSpace(workspaceID), AgentID: agentID, SkillID: skill.ID, Branch: target.branch, CreatedBy: strings.TrimSpace(subject), UpdatedBy: strings.TrimSpace(subject), CreatedAt: now, UpdatedAt: now},
			branchProjection,
		)
		if err != nil {
			return err
		}
		binding = created
		if err := s.enqueueAgentAssetAccessChange(ctx, tx, strings.TrimSpace(workspaceID), agentID, memorystore.AssetTypeSkill, now); err != nil {
			return err
		}
		activityStore := s.skillActivityStore(tx)
		if !target.native && !branchPinFound {
			if err := writeSkillActivity(ctx, activityStore, skill, target.activityRef, skillActivityBranchPinned, strings.TrimSpace(subject), "settings", target.branch, map[string]any{
				"branch":   target.branch,
				"repoName": target.primary.RepoName,
			}, now); err != nil {
				return err
			}
		}
		if !bindingFound {
			details := map[string]any{"agentId": agentID, "agentName": agentID}
			if target.native {
				details["contentId"] = skill.ContentID
				details["contentVersionId"] = skill.ActiveContentVersionID
			} else {
				details["branch"] = target.branch
			}
			return writeSkillActivity(ctx, activityStore, skill, target.activityRef, skillActivityBound, strings.TrimSpace(subject), "binding", agentID, details, now)
		}
		if !target.native && current.Branch != target.branch {
			return writeSkillActivity(ctx, activityStore, skill, target.activityRef, skillActivityBranchPinned, strings.TrimSpace(subject), "settings", target.branch, map[string]any{
				"agentId":        agentID,
				"agentName":      agentID,
				"branch":         target.branch,
				"previousBranch": current.Branch,
				"repoName":       target.primary.RepoName,
			}, now)
		}
		return nil
	}); err != nil {
		return AgentSkillBindingResult{}, mapStoreError(err)
	}
	return AgentSkillBindingResult{AgentID: agentID, SkillID: binding.SkillID, Branch: binding.Branch}, nil
}

func (s *Service) UnbindAgentSkill(ctx context.Context, workspaceID, subject, agentID, skillID string) (result AgentSkillBindingResult, err error) {
	if s == nil || s.store == nil {
		return AgentSkillBindingResult{}, invalidArgument("skill service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return AgentSkillBindingResult{}, err
	}
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return AgentSkillBindingResult{}, invalidArgument("agent id is required")
	}
	if err := s.requireOwnedAgent(ctx, workspaceID, subject, agentID); err != nil {
		return AgentSkillBindingResult{}, err
	}
	skill, err := s.GetSkill(ctx, workspaceID, subject, skillID)
	if err != nil {
		return AgentSkillBindingResult{}, err
	}
	if !s.skillCanBind(skill) {
		return AgentSkillBindingResult{}, forbidden("skill requires manage access")
	}
	current, err := s.store.GetSkillAgentBinding(ctx, workspaceID, agentID, skillID)
	if err != nil {
		return AgentSkillBindingResult{}, mapStoreError(err)
	}
	writeRoute, err := s.beginSkillWrite(ctx, skill.WorkspaceID, skill.ID)
	if err != nil {
		return AgentSkillBindingResult{}, err
	}
	native := writeRoute.native
	if err := s.ensureSkillWriteCurrent(ctx, skill.WorkspaceID, skill.ID, writeRoute); err != nil {
		return AgentSkillBindingResult{}, err
	}
	repoFullName := ""
	scope := skillRepositoryBindingScope{}
	if native {
		if strings.TrimSpace(skill.ContentID) == "" || strings.TrimSpace(skill.ActiveContentVersionID) == "" {
			return AgentSkillBindingResult{}, fmt.Errorf("native skill content reference is incomplete")
		}
		repoFullName = "contentstore://" + skill.ContentID + "/" + skill.ActiveContentVersionID
	} else {
		repoFullName = s.skillPrimaryRepoFullName(ctx, workspaceID, current.SkillID)
		scope, err = s.skillRepositoryBindingScope(ctx, workspaceID, current.SkillID)
		if err != nil {
			return AgentSkillBindingResult{}, err
		}
	}
	now := s.now()
	if err := s.store.Transaction(ctx, func(tx *store.Store) error {
		if err := tx.DeleteSkillAgentBinding(ctx, workspaceID, agentID, skillID); err != nil {
			return mapStoreError(err)
		}
		if len(scope.SkillIDs) > 0 {
			if err := tx.DeleteSkillRepositoryBranchIfUnused(ctx, scope.Branch, scope.SkillIDs); err != nil {
				return err
			}
		}
		if err := s.enqueueAgentAssetAccessChange(ctx, tx, strings.TrimSpace(workspaceID), agentID, memorystore.AssetTypeSkill, now); err != nil {
			return err
		}
		details := map[string]any{"agentId": agentID, "agentName": agentID}
		if native {
			details["contentId"] = skill.ContentID
			details["contentVersionId"] = skill.ActiveContentVersionID
		} else {
			details["branch"] = current.Branch
		}
		return writeSkillActivity(ctx, s.skillActivityStore(tx), skill, repoFullName, skillActivityUnbound, strings.TrimSpace(subject), "binding", agentID, details, now)
	}); err != nil {
		return AgentSkillBindingResult{}, mapStoreError(err)
	}
	branch := current.Branch
	if native {
		branch = ""
	}
	return AgentSkillBindingResult{AgentID: current.AgentID, SkillID: current.SkillID, Branch: branch, Unbound: true}, nil
}

func (s *Service) enqueueAgentAssetAccessChange(ctx context.Context, tx *store.Store, workspaceID, agentID, assetType string, occurredAt time.Time) error {
	if s == nil || s.assetAccessOutbox == nil || tx == nil || tx.DB() == nil {
		return nil
	}
	_, err := access.EnqueueAccessChange(ctx, s.assetAccessOutbox.WithDB(tx.DB()), memorystore.AgentAssetAccessChangeInput{
		WorkspaceID: workspaceID,
		AgentID:     agentID,
		AssetType:   strings.TrimSpace(assetType),
		OccurredAt:  occurredAt,
	})
	return err
}

type skillRepositoryBindingScope struct {
	Branch   store.SkillRepositoryBranch
	SkillIDs []string
}

func (s *Service) skillRepositoryBindingScope(ctx context.Context, workspaceID, skillID string) (skillRepositoryBindingScope, error) {
	if s.repositoryStore == nil {
		return skillRepositoryBindingScope{}, nil
	}
	workspaceID = strings.TrimSpace(workspaceID)
	skillID = strings.TrimSpace(skillID)
	repo, err := s.repositoryStore.Get(ctx, workspaceID, reposstore.ResourceTypeSkillRepository, skillID, reposstore.RepoRolePrimary)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return skillRepositoryBindingScope{}, nil
		}
		return skillRepositoryBindingScope{}, err
	}
	fullName := firstNonEmpty(repo.RepoFullName, repo.RepoOwner+"/"+repo.RepoName)
	repos, err := s.repositoryStore.ListByRepoFullName(ctx, fullName)
	if err != nil {
		return skillRepositoryBindingScope{}, err
	}
	skillIDs := make([]string, 0, len(repos))
	for _, candidate := range repos {
		if candidate.WorkspaceID != workspaceID || candidate.ResourceType != reposstore.ResourceTypeSkillRepository || candidate.RepoRole != reposstore.RepoRolePrimary {
			continue
		}
		skillIDs = append(skillIDs, candidate.ResourceID)
	}
	if len(skillIDs) == 0 {
		skillIDs = append(skillIDs, skillID)
	}
	return skillRepositoryBindingScope{
		Branch:   store.SkillRepositoryBranch{WorkspaceID: workspaceID, RepoOwner: repo.RepoOwner, RepoName: repo.RepoName},
		SkillIDs: skillIDs,
	}, nil
}

func (s *Service) ListAgentSkillBindings(ctx context.Context, workspaceID, subject string, options AgentSkillBindingsOptions) (AgentSkillBindingsSummary, error) {
	if s == nil || s.store == nil {
		return AgentSkillBindingsSummary{}, invalidArgument("skill service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return AgentSkillBindingsSummary{}, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	ownedAgents, err := s.listOwnedAgents(ctx, workspaceID, subject)
	if err != nil {
		return AgentSkillBindingsSummary{}, err
	}
	if _, err := s.ensureOfficialSkillLibraries(ctx, workspaceID, options.TemplateLocale); err != nil {
		return AgentSkillBindingsSummary{}, err
	}
	var bindings []store.SkillAgentBinding
	if s.contentAuthority != nil {
		bindings, err = s.store.ListWorkspaceSkillAgentBindings(ctx, workspaceID, store.MaxAgentAccessibleSkillRows)
	} else {
		bindings, err = s.store.ListWorkspaceSkillAgentBindings(ctx, workspaceID, 0)
	}
	if err != nil {
		return AgentSkillBindingsSummary{}, err
	}
	ownedAgentIDs := make(map[string]struct{}, len(ownedAgents))
	for _, agent := range ownedAgents {
		ownedAgentIDs[agent.ID] = struct{}{}
	}
	var skills []store.Skill
	if s.contentAuthority != nil {
		skills, err = s.store.ListAssignableSkillsForSubject(ctx, workspaceID, subject, store.MaxAgentAccessibleSkillRows)
	} else {
		skills, err = s.store.ListAssignableSkillsForSubject(ctx, workspaceID, subject, 0)
	}
	if err != nil {
		return AgentSkillBindingsSummary{}, err
	}
	for i := range skills {
		s.decorateSkillAccess(ctx, &skills[i], subject)
	}
	skillIDs := make([]string, 0, len(skills)+len(bindings))
	for _, skill := range skills {
		skillIDs = append(skillIDs, skill.ID)
	}
	for _, binding := range bindings {
		skillIDs = append(skillIDs, binding.SkillID)
	}
	skillIDs = uniqueNonEmptyStrings(skillIDs)
	if s.contentAuthority != nil && len(skillIDs) > store.MaxAgentAccessibleSkillRows {
		return AgentSkillBindingsSummary{}, fmt.Errorf("agent binding skill authority set exceeds %d rows", store.MaxAgentAccessibleSkillRows)
	}
	nativeSkills, err := s.nativeSkillReads(ctx, workspaceID, skillIDs)
	if err != nil {
		return AgentSkillBindingsSummary{}, err
	}
	legacySkillIDs := make([]string, 0, len(skillIDs))
	for _, skillID := range skillIDs {
		if !nativeSkills[skillID] {
			legacySkillIDs = append(legacySkillIDs, skillID)
		}
	}
	repositories, err := s.repositoriesBySkillID(ctx, workspaceID, legacySkillIDs)
	if err != nil {
		return AgentSkillBindingsSummary{}, err
	}
	out := AgentSkillBindingsSummary{
		OwnedAgents: ownedAgents,
		Bindings:    make([]AgentSkillBindingSummary, 0, len(bindings)),
		Skills:      make([]AgentSkillBindingSkill, 0, len(skills)),
	}
	for _, binding := range bindings {
		if _, owned := ownedAgentIDs[binding.AgentID]; !owned {
			continue
		}
		branch := binding.Branch
		if nativeSkills[binding.SkillID] {
			branch = ""
		}
		out.Bindings = append(out.Bindings, AgentSkillBindingSummary{ID: binding.ID, WorkspaceID: binding.WorkspaceID, AgentID: binding.AgentID, SkillID: binding.SkillID, Branch: branch, CreatedBy: binding.CreatedBy, UpdatedBy: binding.UpdatedBy, CreatedAt: binding.CreatedAt, UpdatedAt: binding.UpdatedAt})
	}
	for _, skill := range skills {
		item := AgentSkillBindingSkill{ID: skill.ID, WorkspaceID: skill.WorkspaceID, Slug: skill.Slug, Name: skill.Name, Description: skill.Description, IsActive: skill.IsActive, Role: skill.Role, CanManage: skill.CanManage, CanBind: s.skillCanBind(skill), UpdatedAt: skill.UpdatedAt}
		if nativeSkills[skill.ID] {
			if strings.TrimSpace(skill.ContentID) == "" || strings.TrimSpace(skill.ActiveContentVersionID) == "" {
				return AgentSkillBindingsSummary{}, fmt.Errorf("native skill content reference is incomplete")
			}
			item.ContentID = skill.ContentID
			item.ContentVersionID = skill.ActiveContentVersionID
		} else {
			item.Repository = repositories[skill.ID]
		}
		out.Skills = append(out.Skills, item)
	}
	return out, nil
}

func (s *Service) ListAgentSkillBindingsForAgent(ctx context.Context, workspaceID, subject, agentID string) ([]AgentSkillBindingSummary, error) {
	if s == nil || s.store == nil {
		return nil, invalidArgument("skill service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return nil, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return nil, invalidArgument("agent id is required")
	}
	if err := s.requireOwnedAgent(ctx, workspaceID, subject, agentID); err != nil {
		return nil, err
	}
	maxRows := 0
	if s.contentAuthority != nil {
		maxRows = store.MaxAgentAccessibleSkillRows
	}
	rows, err := s.store.GetAgentAccessibleSkills(ctx, workspaceID, agentID, maxRows)
	if err != nil {
		return nil, mapStoreError(err)
	}
	skillIDs := make([]string, 0, len(rows))
	for _, row := range rows {
		skillIDs = append(skillIDs, row.Skill.ID)
	}
	nativeSkills, err := s.nativeSkillReads(ctx, workspaceID, skillIDs)
	if err != nil {
		return nil, err
	}
	out := make([]AgentSkillBindingSummary, 0, len(rows))
	for _, row := range rows {
		binding := row.Binding
		branch := binding.Branch
		if nativeSkills[binding.SkillID] {
			branch = ""
		}
		out = append(out, AgentSkillBindingSummary{
			ID:          binding.ID,
			WorkspaceID: binding.WorkspaceID,
			AgentID:     binding.AgentID,
			SkillID:     binding.SkillID,
			Branch:      branch,
			CreatedBy:   binding.CreatedBy,
			UpdatedBy:   binding.UpdatedBy,
			CreatedAt:   binding.CreatedAt,
			UpdatedAt:   binding.UpdatedAt,
		})
	}
	return out, nil
}

func (s *Service) repositoriesBySkillID(ctx context.Context, workspaceID string, skillIDs []string) (map[string]*reposstore.ResourceRepository, error) {
	out := make(map[string]*reposstore.ResourceRepository)
	if s.repositoryStore == nil || len(skillIDs) == 0 {
		return out, nil
	}
	uniqueIDs := uniqueNonEmptyStrings(skillIDs)
	if len(uniqueIDs) == 0 {
		return out, nil
	}
	repos, err := s.repositoryStore.ListForResourceBatch(ctx, strings.TrimSpace(workspaceID), reposstore.ResourceTypeSkillRepository, uniqueIDs)
	if err != nil {
		return nil, err
	}
	for i := range repos {
		if repos[i].RepoRole != reposstore.RepoRolePrimary {
			continue
		}
		repo := repos[i]
		if _, exists := out[repo.ResourceID]; !exists {
			out[repo.ResourceID] = &repo
		}
	}
	return out, nil
}

func (s *Service) listOwnedAgents(ctx context.Context, workspaceID, subject string) ([]reposservice.AgentOwnership, error) {
	if s.agentOwnershipSource == nil {
		return nil, invalidArgument("agent ownership source is not configured")
	}
	agents, err := s.agentOwnershipSource.ListOwnedAgents(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(subject))
	if err != nil {
		return nil, err
	}
	return normalizeAgentOwnerships(agents), nil
}

func (s *Service) requireOwnedAgent(ctx context.Context, workspaceID, subject, agentID string) error {
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return invalidArgument("agent id is required")
	}
	agents, err := s.listOwnedAgents(ctx, workspaceID, subject)
	if err != nil {
		return err
	}
	for _, agent := range agents {
		if agent.ID == agentID {
			return nil
		}
	}
	return codedForbidden(SkillImportErrorPermissionDenied, "agent must be owned by the current user")
}

// requireOwnedAgents is the batch counterpart of requireOwnedAgent. It fetches
// the caller's owned agent set once and fail-closes if any requested agent ID
// is not owned. Used by the batch agent-access read endpoint to prevent
// cross-user agent-binding enumeration.
func (s *Service) requireOwnedAgents(ctx context.Context, workspaceID, subject string, agentIDs []string) error {
	if len(agentIDs) == 0 {
		return nil
	}
	agents, err := s.listOwnedAgents(ctx, workspaceID, subject)
	if err != nil {
		return err
	}
	owned := make(map[string]struct{}, len(agents))
	for _, agent := range agents {
		owned[agent.ID] = struct{}{}
	}
	for _, id := range agentIDs {
		if _, ok := owned[id]; !ok {
			return codedForbidden(SkillImportErrorPermissionDenied, "agent must be owned by the current user")
		}
	}
	return nil
}

func normalizeAgentOwnerships(agents []reposservice.AgentOwnership) []reposservice.AgentOwnership {
	out := make([]reposservice.AgentOwnership, 0, len(agents))
	seen := make(map[string]struct{}, len(agents))
	for _, agent := range agents {
		id := strings.TrimSpace(agent.ID)
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		agent.ID = id
		agent.Name = strings.TrimSpace(agent.Name)
		agent.DisplayName = strings.TrimSpace(agent.DisplayName)
		out = append(out, agent)
	}
	return out
}

func (s *Service) decorateSkillAccess(ctx context.Context, skill *store.Skill, subject string) {
	if skill == nil {
		return
	}
	role := s.skillRole(ctx, *skill, subject)
	skill.Role = role
	skill.CanManage = skillRoleCanManage(role)
}

func skillRoleCanManage(role string) bool {
	switch strings.TrimSpace(role) {
	case "owner", "manage":
		return true
	default:
		return false
	}
}

func (s *Service) skillCanBind(skill store.Skill) bool {
	if skillRoleCanManage(skill.Role) {
		return true
	}
	return s != nil && s.officialSkillLibrariesEnabled && IsOfficialBuiltinSkill(skill)
}

func (s *Service) BatchListAgentAccessibleSkills(ctx context.Context, workspaceID, subject string, options AgentSkillAccessBatchOptions) (AgentSkillAccessBatch, error) {
	if s == nil || s.store == nil {
		return AgentSkillAccessBatch{}, invalidArgument("skill service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return AgentSkillAccessBatch{}, err
	}
	dedupedAgentIDs := access.DeduplicateAgentIDs(options.AgentIDs)
	out := AgentSkillAccessBatch{Agents: make(map[string][]AgentAccessibleSkill, len(dedupedAgentIDs))}
	skillIDs := make([]string, 0)
	for _, agentID := range dedupedAgentIDs {
		out.Agents[agentID] = []AgentAccessibleSkill{}
	}
	if err := s.requireOwnedAgents(ctx, workspaceID, subject, dedupedAgentIDs); err != nil {
		return AgentSkillAccessBatch{}, err
	}
	officialLibraries, err := s.ensureOfficialAutoAgentAccessSkillLibraries(ctx, workspaceID, options.TemplateLocale)
	if err != nil {
		return AgentSkillAccessBatch{}, err
	}
	var rowsByAgent map[string][]store.AgentAccessibleSkill
	if s.contentAuthority != nil {
		rowsByAgent, err = s.store.ListAgentAccessibleSkills(ctx, workspaceID, dedupedAgentIDs, store.MaxAgentAccessibleSkillRows)
	} else {
		rowsByAgent, err = s.store.ListAgentAccessibleSkills(ctx, workspaceID, dedupedAgentIDs, 0)
	}
	if err != nil {
		return AgentSkillAccessBatch{}, err
	}
	for _, rows := range rowsByAgent {
		for _, row := range rows {
			skillIDs = append(skillIDs, row.Skill.ID)
		}
	}
	for _, library := range officialLibraries {
		if library.AutoAgentAccess {
			skillIDs = append(skillIDs, library.Skill.ID)
		}
	}
	skillIDs = uniqueNonEmptyStrings(skillIDs)
	nativeSkills, err := s.nativeSkillReads(ctx, workspaceID, skillIDs)
	if err != nil {
		return AgentSkillAccessBatch{}, err
	}
	legacySkillIDs := make([]string, 0, len(skillIDs))
	for _, skillID := range skillIDs {
		if !nativeSkills[skillID] {
			legacySkillIDs = append(legacySkillIDs, skillID)
		}
	}
	repositories, err := s.repositoriesBySkillID(ctx, workspaceID, legacySkillIDs)
	if err != nil {
		return AgentSkillAccessBatch{}, err
	}
	for _, agentID := range dedupedAgentIDs {
		rows := rowsByAgent[agentID]
		seenSkillIDs := make(map[string]struct{}, len(rows)+len(officialLibraries))
		for _, row := range rows {
			seenSkillIDs[row.Skill.ID] = struct{}{}
			native := nativeSkills[row.Skill.ID]
			if native && (strings.TrimSpace(row.Skill.ContentID) == "" || strings.TrimSpace(row.Skill.ActiveContentVersionID) == "") {
				return AgentSkillAccessBatch{}, fmt.Errorf("native skill content reference is incomplete")
			}
			out.Agents[agentID] = append(out.Agents[agentID], agentAccessibleSkillFromRow(row, repositories[row.Skill.ID], native))
		}
		for _, library := range officialLibraries {
			if !library.AutoAgentAccess {
				continue
			}
			native := nativeSkills[library.Skill.ID]
			if !native && library.Repository == nil {
				continue
			}
			if _, seen := seenSkillIDs[library.Skill.ID]; !seen {
				if native && (strings.TrimSpace(library.Skill.ContentID) == "" || strings.TrimSpace(library.Skill.ActiveContentVersionID) == "") {
					return AgentSkillAccessBatch{}, fmt.Errorf("native skill content reference is incomplete")
				}
				out.Agents[agentID] = append(out.Agents[agentID], agentAccessibleOfficialSkill(library.Skill, library.Repository, native))
			}
		}
	}
	lease, err := s.createSkillSyncToken(ctx, workspaceID, subject, out.Agents)
	if err != nil {
		return AgentSkillAccessBatch{}, err
	}
	out.SyncToken = lease.Token
	out.SyncTokenExpires = lease.ExpiresAt
	return out, nil
}
