package roles

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	"gorm.io/gorm"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	"github.com/tidbcloud/AgentTeam/internal/service/skills"
	rolesstore "github.com/tidbcloud/AgentTeam/internal/store/resource/roles"
)

const (
	roleGrantOwner  = "owner"
	roleGrantManage = "manage"
	roleGrantEdit   = "edit"
	roleGrantView   = "view"
)

// Service owns MeshX Role metadata and delegates referenced resource
// realization back to each resource's owning service.
type Service struct {
	store *rolesstore.Store
	now   func() time.Time

	contentStore      RolePackageContentStore
	contentAuthority  rolePackageAuthorityReader
	migrationWorker   roleContentMigrationWorker
	migrationExporter roleContentMigrationExporter
	skills            SkillBinder
	kb                KnowledgeBaseGrantor
	ownership         AgentOwnershipSource
}

// Options wires resource-owning services used during Role realization.
type Options struct {
	Now               func() time.Time
	ContentStore      RolePackageContentStore
	ContentAuthority  rolePackageAuthorityReader
	MigrationWorker   roleContentMigrationWorker
	MigrationExporter roleContentMigrationExporter
	Skills            SkillBinder
	KB                KnowledgeBaseGrantor
	Ownership         AgentOwnershipSource
}

func New(store *rolesstore.Store, options Options) *Service {
	now := options.Now
	if now == nil {
		now = func() time.Time { return time.Now().UTC() }
	}
	contentStore := options.ContentStore
	if contentStore == nil {
		contentStore = FailClosedRolePackageContentStore{}
	}
	ownership := options.Ownership
	if ownership == nil {
		ownership = ClaimsAgentOwnershipSource{}
	}
	return &Service{
		store: store, now: now, contentStore: contentStore, contentAuthority: options.ContentAuthority,
		migrationWorker: options.MigrationWorker, migrationExporter: options.MigrationExporter,
		skills: options.Skills, kb: options.KB, ownership: ownership,
	}
}

type Capabilities struct {
	Enabled                   bool
	PresetRoles               bool
	MyRoles                   bool
	Realization               bool
	RolePackage               bool
	RolePackageDisabledReason string
	DisabledReason            string
}

type Page struct {
	Items   []Role
	Page    int
	PerPage int
	Total   int64
	HasMore bool
}

type Role struct {
	ID                    string
	WorkspaceID           string
	Source                string
	Visibility            string
	Slug                  string
	Name                  string
	Description           string
	Locale                string
	Scenario              string
	Icon                  string
	Tags                  []string
	Definition            string
	RuntimeHints          RuntimeHints
	RolePackageManifest   RolePackageManifest
	RoleVersionID         string
	SourcePresetID        string
	SourcePresetVersion   int
	SourcePresetVersionID string
	SourceAgentID         string
	IsActive              bool
	CreatedBy             string
	UpdatedBy             string
	CreatedAt             time.Time
	UpdatedAt             time.Time
	Role                  string
	CanManage             bool
	OwnerID               string
	Editable              bool
	Provenance            RoleProvenance
	RolePackage           RolePackage
	contentID             string
	contentVersionID      string
}

type RolePackageManifest struct {
	Items []RolePackageManifestItem
}

type RolePackageManifestItem struct {
	Type        string
	Name        string
	Description string
	Required    bool
	Action      string
	ResourceID  string
	AccessLevel string
	Branch      string
	Content     string
}

type RoleProvenance struct {
	Kind    string
	Ref     string
	Version int
}

// RolePackage captures the portable, user-visible role sections and any
// unresolved resource references.
type RolePackage struct {
	Portable    bool
	Sections    []string
	NonPortable []string
	Unresolved  []RolePackageUnresolved
}

// RolePackageUnresolved describes one role package section that still needs a
// concrete resource resolution step.
type RolePackageUnresolved struct {
	Section  string
	Name     string
	Reason   string
	Required bool
}

type RuntimeHints struct {
	Runtime         string `json:"runtime,omitempty"`
	Model           string `json:"model,omitempty"`
	ReasoningEffort string `json:"reasoningEffort,omitempty"`
	SuggestedName   string `json:"suggestedName,omitempty"`
}

type Attachment struct {
	Type        string          `json:"type"`
	Name        string          `json:"name,omitempty"`
	Description string          `json:"description,omitempty"`
	Required    bool            `json:"required,omitempty"`
	Realization string          `json:"realization,omitempty"`
	ResourceID  string          `json:"resourceId,omitempty"`
	AccessLevel string          `json:"accessLevel,omitempty"`
	Branch      string          `json:"branch,omitempty"`
	Content     string          `json:"content,omitempty"`
	Metadata    json.RawMessage `json:"metadata,omitempty"`
}

type Mutation struct {
	Name         string
	Description  string
	Locale       string
	Scenario     string
	Icon         string
	Tags         []string
	Definition   string
	RuntimeHints RuntimeHints
	Attachments  []Attachment
	IsActive     *bool
}

type AddPresetRequest struct {
	PresetID    string
	Locale      string
	DisplayName string
}

type RealizationRequest struct {
	AgentID        string
	RoleVersionID  string
	DryRun         bool
	Apply          bool
	SkipOptional   bool
	StorageScope   string
	TemplateLocale string
	Actor          repos.ResourceRepositoryActor
}

type RealizationResult struct {
	RoleID  string
	AgentID string
	DryRun  bool
	Apply   bool
	Items   []RealizationItem

	realizationID string
	roleVersionID string
}

// RealizationImpact describes the resource deltas a Role apply would create.
// It is intentionally about MeshX resource facts only; billing and quota
// decisions stay with AgentTeam.
type RealizationImpact struct {
	ManagedMemories   int64
	SkillRepositories int64
}

type RealizationItem struct {
	ManifestItemType string
	ManifestItemName string
	Action           string
	Status           string
	ResourceID       string
	AccessLevel      string
	Branch           string
	Message          string
	Required         bool
}

func (s *Service) Capabilities(ctx context.Context, workspaceID, subject string) (Capabilities, error) {
	if s == nil || s.store == nil {
		return Capabilities{Enabled: false, DisabledReason: "roles service is not configured"}, nil
	}
	if err := validateScope(workspaceID, subject); err != nil {
		return Capabilities{}, err
	}
	rolePackage := s.rolePackageContentAvailable()
	capabilities := Capabilities{Enabled: true, PresetRoles: true, MyRoles: true, Realization: true, RolePackage: rolePackage}
	if !rolePackage {
		capabilities.RolePackageDisabledReason = "role package content storage is required"
	}
	return capabilities, nil
}

func (s *Service) rolePackageContentAvailable() bool {
	if s == nil || s.contentStore == nil {
		return false
	}
	switch s.contentStore.(type) {
	case FailClosedRolePackageContentStore, *FailClosedRolePackageContentStore:
		return false
	default:
		return true
	}
}

func (s *Service) ListRoles(ctx context.Context, workspaceID, subject, view, query string, page, perPage int) (Page, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return Page{}, err
	}
	page, perPage = normalizePagination(page, perPage)
	listView := normalizeRoleListView(view)
	rows, total, err := s.store.ListRoles(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(subject), listView, query, page, perPage)
	if err != nil {
		return Page{}, err
	}
	grants, err := s.store.ListRoleGrantsForSubject(ctx, strings.TrimSpace(workspaceID), "user", strings.TrimSpace(subject), roleIDs(rows))
	if err != nil {
		return Page{}, err
	}
	grantByRoleID := make(map[string]string, len(grants))
	for _, grant := range grants {
		grantByRoleID[grant.RoleID] = normalizeRoleGrant(grant.Role)
	}
	activeManifestItemsByRoleID, err := s.store.ListActiveManifestItemsByRoleIDs(ctx, strings.TrimSpace(workspaceID), roleIDs(rows))
	if err != nil {
		return Page{}, err
	}
	items := make([]Role, 0, len(rows))
	for _, row := range rows {
		// List/first-paint reads use only the lightweight Role and manifest
		// projection. Exact payload reads are reserved for detail/realization.
		role := roleFromStore(row, activeManifestItemsByRoleID[row.ID], nil, SourceMyRole)
		role.Role = roleGrantForList(row, strings.TrimSpace(subject), grantByRoleID)
		role.CanManage = roleCanManage(role.Role)
		role.Editable = role.CanManage
		items = append(items, role)
	}
	return Page{Items: items, Page: page, PerPage: perPage, Total: total, HasMore: int64(page*perPage) < total}, nil
}

func (s *Service) ListPresetRoles(ctx context.Context, workspaceID, subject, locale string) ([]Role, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return nil, err
	}
	selected, err := selectPresetTemplates(locale)
	if err != nil {
		return nil, err
	}
	out := make([]Role, 0, len(selected))
	for _, preset := range selected {
		out = append(out, roleFromPreset(preset, strings.TrimSpace(workspaceID)))
	}
	return out, nil
}

func (s *Service) GetRole(ctx context.Context, workspaceID, subject, id string) (Role, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return Role{}, err
	}
	row, err := s.store.GetRole(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(id))
	if err != nil {
		return Role{}, mapStoreNotFound(err)
	}
	activeManifestItemsByRoleID, err := s.store.ListActiveManifestItemsByRoleIDs(ctx, row.WorkspaceID, []string{row.ID})
	if err != nil {
		return Role{}, err
	}
	grant, err := s.roleGrant(ctx, row, subject)
	if err != nil {
		return Role{}, err
	}
	if grant == "" {
		return Role{}, repos.ErrForbidden
	}
	version, err := s.store.GetActiveRoleVersion(ctx, row.WorkspaceID, row.ID)
	if err != nil {
		return Role{}, mapStoreNotFound(err)
	}
	contentByRef, err := s.resolveManifestItemContentsForRoleRead(ctx, subject, row, version, activeManifestItemsByRoleID[row.ID])
	if err != nil {
		return Role{}, err
	}
	role := roleFromStore(row, activeManifestItemsByRoleID[row.ID], contentByRef, SourceMyRole)
	role.contentID = strings.TrimSpace(version.ContentID)
	role.contentVersionID = strings.TrimSpace(version.ContentVersionID)
	role.Role = grant
	role.CanManage = roleCanManage(role.Role)
	role.Editable = role.CanManage
	return role, nil
}

func (s *Service) GetRoleVersion(ctx context.Context, workspaceID, subject, id string, versionID string) (Role, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return Role{}, err
	}
	versionID = strings.TrimSpace(versionID)
	if versionID == "" {
		return Role{}, invalidRoleArgument("role version id is required")
	}
	row, err := s.store.GetRole(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(id))
	if err != nil {
		return Role{}, mapStoreNotFound(err)
	}
	version, err := s.store.GetRoleVersion(ctx, row.WorkspaceID, row.ID, versionID)
	if err != nil {
		return Role{}, mapStoreNotFound(err)
	}
	row.Definition = version.Definition
	row.RuntimeHintsJSON = version.RuntimeHintsJSON
	manifestItems, err := s.store.ListManifestItemsByRoleVersion(ctx, row.WorkspaceID, row.ID, versionID)
	if err != nil {
		return Role{}, err
	}
	if len(manifestItems) == 0 {
		return Role{}, fmt.Errorf("role package manifest for version %q is missing: %w", versionID, repos.ErrInvalidArgument)
	}
	grant, err := s.roleGrant(ctx, row, subject)
	if err != nil {
		return Role{}, err
	}
	if grant == "" {
		return Role{}, repos.ErrForbidden
	}
	contentByRef, err := s.resolveManifestItemContentsForRoleRead(ctx, subject, row, version, manifestItems)
	if err != nil {
		return Role{}, err
	}
	role := roleFromStore(row, manifestItems, contentByRef, SourceMyRole)
	role.RoleVersionID = versionID
	role.contentID = strings.TrimSpace(version.ContentID)
	role.contentVersionID = strings.TrimSpace(version.ContentVersionID)
	role.Role = grant
	role.CanManage = roleCanManage(role.Role)
	role.Editable = role.CanManage
	return role, nil
}

func (s *Service) GetPresetRole(ctx context.Context, workspaceID, subject, presetID, locale string) (Role, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return Role{}, err
	}
	preset, ok, err := selectPresetTemplate(presetID, locale)
	if err != nil {
		return Role{}, err
	}
	if !ok {
		return Role{}, repos.ErrNotFound
	}
	return roleFromPreset(preset, strings.TrimSpace(workspaceID)), nil
}

func (s *Service) CreateRole(ctx context.Context, workspaceID, subject string, input Mutation) (Role, error) {
	return s.createRole(ctx, workspaceID, subject, input, "", 0, "")
}

func (s *Service) createRole(ctx context.Context, workspaceID, subject string, input Mutation, sourcePresetID string, sourcePresetVersion int, sourceAgentID string) (Role, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return Role{}, err
	}
	name := strings.TrimSpace(input.Name)
	if name == "" {
		return Role{}, invalidRoleArgument("role name is required")
	}
	definition := strings.TrimSpace(input.Definition)
	if definition == "" {
		return Role{}, invalidRoleArgument("role definition is required")
	}
	portable, manifestItems, err := canonicalizePortableRole(input.Definition, input.RuntimeHints, input.Attachments)
	if err != nil {
		return Role{}, err
	}
	input.Definition = portable.Definition
	input.RuntimeHints = portable.RuntimeHints
	input.Attachments = attachmentsFromRoleManifest(portable.RolePackageManifest)
	now := s.now()
	id := newRoleID()
	slug, err := s.availableSlug(ctx, workspaceID, name, id)
	if err != nil {
		return Role{}, err
	}
	row, err := storeRoleFromMutation(id, strings.TrimSpace(workspaceID), strings.TrimSpace(subject), slug, input, now)
	if err != nil {
		return Role{}, err
	}
	row.SourcePresetID = strings.TrimSpace(sourcePresetID)
	row.SourcePresetVersion = sourcePresetVersion
	row.SourceAgentID = strings.TrimSpace(sourceAgentID)
	manifestItems, err = s.persistRolePackageContent(ctx, row.WorkspaceID, row.ID, subject, "", "", false, manifestItems)
	if err != nil {
		return Role{}, err
	}
	row.IsActive = true
	if input.IsActive != nil {
		row.IsActive = *input.IsActive
	}
	var created rolesstore.Role
	mutationErr := s.store.Transaction(ctx, func(tx *rolesstore.Store) error {
		var err error
		created, err = tx.CreateRole(ctx, row)
		if err != nil {
			return mapStoreRoleConflict(err)
		}
		versionRole := created
		versionRole.Definition = input.Definition
		published, publishErr := tx.PublishRoleVersion(ctx, versionRole, sourcePresetID, sourcePresetVersion, sourceAgentID, manifestItems, now)
		if publishErr != nil {
			return publishErr
		}
		created.ActiveVersionID = published.RoleVersion.ID
		manifestItems = published.ManifestItems
		_, err = tx.UpsertRoleGrant(ctx, rolesstore.RoleGrant{ID: newRoleGrantID(), WorkspaceID: row.WorkspaceID, RoleID: row.ID, SubjectType: "user", SubjectID: row.CreatedBy, Role: roleGrantOwner, CreatedBy: row.CreatedBy, UpdatedBy: row.CreatedBy, CreatedAt: now, UpdatedAt: now})
		return err
	})
	if mutationErr != nil {
		return Role{}, mutationErr
	}
	contentByRef, err := s.resolveManifestItemContents(ctx, subject, manifestItems)
	if err != nil {
		return Role{}, err
	}
	out := roleFromStore(created, manifestItems, contentByRef, SourceMyRole)
	out.Role = roleGrantOwner
	out.CanManage = true
	out.Editable = true
	return out, nil
}

func (s *Service) CreateRoleFromAgent(ctx context.Context, workspaceID, subject string, input AgentDerivedMutation) (Role, error) {
	agentID := strings.TrimSpace(input.AgentID)
	if agentID == "" {
		return Role{}, invalidRoleArgument("source agent id is required")
	}
	return s.createRole(ctx, workspaceID, subject, input.Mutation, "", 0, agentID)
}

func (s *Service) AddPresetToMyRoles(ctx context.Context, workspaceID, subject string, input AddPresetRequest) (Role, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return Role{}, err
	}
	preset, ok, err := selectPresetTemplate(input.PresetID, input.Locale)
	if err != nil {
		return Role{}, err
	}
	if !ok {
		return Role{}, repos.ErrNotFound
	}
	name := strings.TrimSpace(input.DisplayName)
	if name == "" {
		name = preset.Name
	}
	return s.createRoleFromPreset(ctx, workspaceID, subject, preset, name)
}

func (s *Service) UpdateRole(ctx context.Context, workspaceID, subject, id string, input Mutation) (Role, error) {
	return s.updateRole(ctx, workspaceID, subject, id, input, "")
}

func (s *Service) UpdateRoleVisibility(ctx context.Context, workspaceID, subject, id, visibility string) (Role, error) {
	current, err := s.GetRole(ctx, workspaceID, subject, id)
	if err != nil {
		return Role{}, err
	}
	if !roleCanManage(current.Role) {
		return Role{}, repos.ErrForbidden
	}
	visibility, err = normalizeRoleVisibility(visibility)
	if err != nil {
		return Role{}, err
	}
	updated, err := s.store.UpdateRoleVisibility(ctx, current.WorkspaceID, current.ID, visibility, strings.TrimSpace(subject), s.now())
	if err != nil {
		return Role{}, mapStoreNotFound(err)
	}
	activeManifestItemsByRoleID, err := s.store.ListActiveManifestItemsByRoleIDs(ctx, updated.WorkspaceID, []string{updated.ID})
	if err != nil {
		return Role{}, err
	}
	version, err := s.store.GetActiveRoleVersion(ctx, updated.WorkspaceID, updated.ID)
	if err != nil {
		return Role{}, mapStoreNotFound(err)
	}
	contentByRef, err := s.resolveManifestItemContentsForRoleRead(ctx, subject, updated, version, activeManifestItemsByRoleID[updated.ID])
	if err != nil {
		return Role{}, err
	}
	out := roleFromStore(updated, activeManifestItemsByRoleID[updated.ID], contentByRef, SourceMyRole)
	out.Role = current.Role
	out.CanManage = true
	out.Editable = true
	return out, nil
}

func (s *Service) DeleteRole(ctx context.Context, workspaceID, subject, id string) error {
	current, err := s.GetRole(ctx, workspaceID, subject, id)
	if err != nil {
		return err
	}
	if !roleCanManage(current.Role) {
		return repos.ErrForbidden
	}
	return mapStoreNotFound(s.store.DeleteRole(ctx, current.WorkspaceID, current.ID, s.now()))
}

// UpdateRoleFromAgent updates an existing Role with portable data sourced from
// a specific agent.
func (s *Service) UpdateRoleFromAgent(ctx context.Context, workspaceID, subject, id string, input AgentDerivedMutation) (Role, error) {
	agentID := strings.TrimSpace(input.AgentID)
	if agentID == "" {
		return Role{}, invalidRoleArgument("source agent id is required")
	}
	return s.updateRole(ctx, workspaceID, subject, id, input.Mutation, agentID)
}

func (s *Service) updateRole(ctx context.Context, workspaceID, subject, id string, input Mutation, sourceAgentID string) (Role, error) {
	current, err := s.GetRole(ctx, workspaceID, subject, id)
	if err != nil {
		return Role{}, err
	}
	if !roleCanManage(current.Role) {
		return Role{}, repos.ErrForbidden
	}
	return s.updateRoleFromCurrent(ctx, subject, current, input, sourceAgentID, "", false)
}

// updateRoleFromCurrent is the single public product/content finalize owner
// shared by ordinary updates and history rollback. Callers must supply the
// exact active Role snapshot that authorizes the product and Content-head CAS.
func (s *Service) updateRoleFromCurrent(ctx context.Context, subject string, current Role, input Mutation, sourceAgentID, contentOperationKey string, requireNative bool) (Role, error) {
	portable, manifestItems, err := canonicalizePortableRole(input.Definition, input.RuntimeHints, input.Attachments)
	if err != nil {
		return Role{}, err
	}
	input.Definition = portable.Definition
	input.RuntimeHints = portable.RuntimeHints
	input.Attachments = attachmentsFromRoleManifest(portable.RolePackageManifest)
	now := s.now()
	row, err := storeRoleFromMutation(current.ID, current.WorkspaceID, strings.TrimSpace(subject), current.Slug, input, now)
	if err != nil {
		return Role{}, err
	}
	row.SourcePresetID = current.SourcePresetID
	row.SourcePresetVersion = current.SourcePresetVersion
	row.SourceAgentID = current.SourceAgentID
	row.Visibility = current.Visibility
	if strings.TrimSpace(sourceAgentID) != "" {
		row.SourceAgentID = strings.TrimSpace(sourceAgentID)
	}
	if strings.TrimSpace(contentOperationKey) == "" {
		packageDigest, err := rolePackageManifestDigest(manifestItems)
		if err != nil {
			return Role{}, err
		}
		contentOperationKey = rolePackageStableOperationKey("update", current.RoleVersionID, packageDigest)
	}
	manifestItems, err = s.persistRolePackageContent(ctx, row.WorkspaceID, row.ID, subject, contentOperationKey, current.contentVersionID, requireNative, manifestItems)
	if err != nil {
		return Role{}, err
	}
	if input.IsActive == nil {
		row.IsActive = current.IsActive
	} else {
		row.IsActive = *input.IsActive
	}
	var updated rolesstore.Role
	mutationErr := s.store.Transaction(ctx, func(tx *rolesstore.Store) error {
		var err error
		updated, err = tx.UpdateRoleIfActiveVersion(ctx, row, current.RoleVersionID)
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return ErrRoleVersionConflict
		}
		if err != nil {
			return err
		}
		versionRole := updated
		versionRole.Definition = input.Definition
		published, publishErr := tx.PublishRoleVersion(ctx, versionRole, row.SourcePresetID, row.SourcePresetVersion, row.SourceAgentID, manifestItems, now)
		if publishErr != nil {
			return publishErr
		}
		updated.ActiveVersionID = published.RoleVersion.ID
		manifestItems = published.ManifestItems
		return nil
	})
	if mutationErr != nil {
		return Role{}, mutationErr
	}
	contentByRef, err := s.resolveManifestItemContents(ctx, subject, manifestItems)
	if err != nil {
		return Role{}, err
	}
	out := roleFromStore(updated, manifestItems, contentByRef, SourceMyRole)
	out.Role = current.Role
	out.CanManage = true
	out.Editable = true
	return out, nil
}

func (s *Service) RealizeRoleForAgent(ctx context.Context, workspaceID, subject, id string, input RealizationRequest) (RealizationResult, error) {
	var (
		role Role
		err  error
	)
	if versionID := strings.TrimSpace(input.RoleVersionID); versionID != "" {
		role, err = s.GetRoleVersion(ctx, workspaceID, subject, id, versionID)
	} else {
		role, err = s.GetRole(ctx, workspaceID, subject, id)
	}
	if err != nil {
		return RealizationResult{}, err
	}
	return s.realize(ctx, workspaceID, subject, role, input)
}

func (s *Service) PlanRoleRealizationImpact(ctx context.Context, workspaceID, subject, id string, input RealizationRequest) (RealizationImpact, error) {
	var (
		role Role
		err  error
	)
	if versionID := strings.TrimSpace(input.RoleVersionID); versionID != "" {
		role, err = s.GetRoleVersion(ctx, workspaceID, subject, id, versionID)
	} else {
		role, err = s.GetRole(ctx, workspaceID, subject, id)
	}
	if err != nil {
		return RealizationImpact{}, err
	}
	return s.realizationImpact(ctx, subject, role, input)
}

func (s *Service) RealizePresetRoleForAgent(ctx context.Context, workspaceID, subject string, input AddPresetRequest, realization RealizationRequest) (Role, RealizationResult, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return Role{}, RealizationResult{}, err
	}
	if err := validateRealizationMode(realization); err != nil {
		return Role{}, RealizationResult{}, err
	}
	preset, ok, err := selectPresetTemplate(input.PresetID, input.Locale)
	if err != nil {
		return Role{}, RealizationResult{}, err
	}
	if !ok {
		return Role{}, RealizationResult{}, repos.ErrNotFound
	}
	name := strings.TrimSpace(input.DisplayName)
	if name == "" {
		name = preset.Name
	}
	if realization.DryRun {
		role := roleFromPreset(preset, strings.TrimSpace(workspaceID))
		role.Name = name
		result, err := s.realize(ctx, workspaceID, subject, role, realization)
		return role, result, err
	}
	role, err := s.createRoleFromPreset(ctx, workspaceID, subject, preset, name)
	if err != nil {
		return Role{}, RealizationResult{}, err
	}
	result, err := s.realize(ctx, workspaceID, subject, role, realization)
	return role, result, err
}

func (s *Service) PlanPresetRoleRealizationImpact(ctx context.Context, workspaceID, subject string, input AddPresetRequest, realization RealizationRequest) (RealizationImpact, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return RealizationImpact{}, err
	}
	if err := validateRealizationMode(realization); err != nil {
		return RealizationImpact{}, err
	}
	preset, ok, err := selectPresetTemplate(input.PresetID, input.Locale)
	if err != nil {
		return RealizationImpact{}, err
	}
	if !ok {
		return RealizationImpact{}, repos.ErrNotFound
	}
	name := strings.TrimSpace(input.DisplayName)
	if name == "" {
		name = preset.Name
	}
	role := roleFromPreset(preset, strings.TrimSpace(workspaceID))
	role.Name = name
	return s.realizationImpact(ctx, subject, role, realization)
}

func (s *Service) createRoleFromPreset(ctx context.Context, workspaceID, subject string, preset presetTemplate, name string) (Role, error) {
	mutation := Mutation{
		Name:         name,
		Description:  preset.Description,
		Locale:       preset.Locale,
		Scenario:     preset.Scenario,
		Icon:         preset.Icon,
		Tags:         append([]string(nil), preset.Tags...),
		Definition:   preset.Definition,
		RuntimeHints: preset.RuntimeHints,
		Attachments:  cloneAttachments(preset.Attachments),
	}
	return s.createRole(ctx, workspaceID, subject, mutation, preset.ID, preset.Version, "")
}

func (s *Service) realizationImpact(ctx context.Context, actor string, role Role, input RealizationRequest) (RealizationImpact, error) {
	if strings.TrimSpace(input.AgentID) == "" {
		return RealizationImpact{}, repos.ErrInvalidArgument
	}
	if err := validateRealizationMode(input); err != nil {
		return RealizationImpact{}, err
	}
	_, manifestItems, contentByRef, err := s.realizationManifest(ctx, actor, role, input.RoleVersionID)
	if err != nil {
		return RealizationImpact{}, err
	}
	if len(manifestItems) > 0 {
		role.RolePackageManifest = rolePackageManifestFromStoreItems(manifestItems, contentByRef)
	}
	var impact RealizationImpact
	for _, attachment := range attachmentsFromRoleManifest(role.RolePackageManifest) {
		if input.SkipOptional && !attachment.Required {
			continue
		}
		switch attachment.Type {
		case AttachmentSkill:
			if realizationAction(attachment) == RealizationCreate {
				impact.SkillRepositories++
			}
		}
	}
	return impact, nil
}

func (s *Service) realize(ctx context.Context, workspaceID, subject string, role Role, input RealizationRequest) (RealizationResult, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	subject = strings.TrimSpace(subject)
	agentID := strings.TrimSpace(input.AgentID)
	if agentID == "" {
		return RealizationResult{}, repos.ErrInvalidArgument
	}
	if err := validateRealizationMode(input); err != nil {
		return RealizationResult{}, err
	}
	if input.Apply && !s.rolePackageContentAvailable() {
		return RealizationResult{}, fmt.Errorf("role apply requires role package content storage: %w", repos.ErrForbidden)
	}
	versionID, manifestItems, contentByRef, err := s.realizationManifest(ctx, subject, role, input.RoleVersionID)
	if err != nil {
		return RealizationResult{}, err
	}
	if len(manifestItems) > 0 {
		role.RolePackageManifest = rolePackageManifestFromStoreItems(manifestItems, contentByRef)
		role = decorateRoleContract(role)
	}
	items := make([]RealizationItem, 0, len(role.RolePackageManifest.Items))
	items = append(items, RealizationItem{ManifestItemType: ManifestItemRoleDefinitionAsset, ManifestItemName: role.Name, Action: RealizationCopy, Status: plannedOrRealized(input.DryRun), ResourceID: role.ID, Message: roleDefinitionRealizationMessage(input.DryRun), Required: true})
	for _, attachment := range attachmentsFromRoleManifest(role.RolePackageManifest) {
		item := RealizationItem{ManifestItemType: attachment.Type, ManifestItemName: attachment.Name, Action: realizationAction(attachment), ResourceID: attachment.ResourceID, AccessLevel: attachment.AccessLevel, Branch: attachment.Branch, Required: attachment.Required}
		if input.SkipOptional && !attachment.Required {
			item.Status = RealizationStatusSkipped
			item.Message = "optional attachment skipped"
			items = append(items, item)
			continue
		}
		if input.DryRun {
			item.Status = RealizationStatusPlanned
			item.Message = plannedMessage(attachment)
			items = append(items, item)
			continue
		}
		realized, err := s.realizeAttachment(ctx, workspaceID, subject, agentID, attachment, item, input)
		if err != nil {
			if attachment.Required {
				return RealizationResult{}, err
			}
			realized.Status = RealizationStatusUnresolved
			realized.Message = err.Error()
		}
		if attachment.Required && realized.Status == RealizationStatusUnresolved {
			return RealizationResult{}, fmt.Errorf("required role attachment %q is unresolved: %s: %w", firstNonEmpty(attachment.Name, attachment.Type), realized.Message, repos.ErrInvalidArgument)
		}
		items = append(items, realized)
	}
	result := RealizationResult{RoleID: role.ID, AgentID: agentID, DryRun: input.DryRun, Apply: input.Apply, Items: items, roleVersionID: versionID}
	if input.DryRun {
		return result, nil
	}
	realizationID, err := s.persistRealization(ctx, subject, role, agentID, versionID, manifestItems, result)
	if err != nil {
		return RealizationResult{}, err
	}
	result.realizationID = realizationID
	return result, nil
}

func (s *Service) realizeAttachment(ctx context.Context, workspaceID, subject, agentID string, attachment Attachment, item RealizationItem, _ RealizationRequest) (RealizationItem, error) {
	switch attachment.Type {
	case AttachmentPortableKnowledge:
		item.Action = RealizationCopy
		if strings.TrimSpace(attachment.Content) == "" {
			item.Status = RealizationStatusUnresolved
			item.Message = "portable knowledge content is unavailable"
			return item, nil
		}
		item.Status = RealizationStatusRealized
		item.Message = "portable knowledge is materialized by AgentTeam agent creation"
		return item, nil
	case AttachmentSkill:
		if strings.TrimSpace(attachment.ResourceID) == "" {
			item.Action = RealizationCopy
			if strings.TrimSpace(attachment.Content) == "" {
				item.Status = RealizationStatusUnresolved
				item.Message = "skill template content is unresolved"
			} else {
				item.Status = RealizationStatusRealized
				item.Message = "portable skill is materialized by role package"
			}
			return item, nil
		}
		if item.Action != RealizationBind {
			item.Status = RealizationStatusUnresolved
			item.Message = "skill attachments support bind realization only"
			return item, nil
		}
		if s.skills == nil {
			item.Status = RealizationStatusUnresolved
			item.Message = "skills service is not configured"
			return item, nil
		}
		skillID := strings.TrimSpace(attachment.ResourceID)
		if skillID == "" {
			item.Status = RealizationStatusUnresolved
			item.Message = "skill resource is unresolved"
			return item, nil
		}
		result, err := s.skills.BindAgentSkill(ctx, workspaceID, subject, agentID, skillID, skills.AgentSkillBindingMutation{LegacyBranchHint: attachment.Branch})
		if err != nil {
			return item, mapRoleDependencyError(mapSkillError(err))
		}
		item.Status = RealizationStatusRealized
		item.ResourceID = result.SkillID
		item.Branch = result.Branch
		item.Message = "skill bound to agent"
		return item, nil
	case AttachmentKnowledgeBase:
		if item.Action != RealizationGrant {
			item.Status = RealizationStatusUnresolved
			item.Message = "knowledge base attachments support grant realization only"
			return item, nil
		}
		if s.kb == nil {
			item.Status = RealizationStatusUnresolved
			item.Message = "knowledge base service is not configured"
			return item, nil
		}
		kbID := strings.TrimSpace(attachment.ResourceID)
		if kbID == "" {
			item.Status = RealizationStatusUnresolved
			item.Message = "knowledge base reference is unresolved"
			return item, nil
		}
		access := firstNonEmpty(attachment.AccessLevel, "view")
		result, err := s.kb.GrantAgentKnowledgeBaseAccess(ctx, workspaceID, subject, agentID, kbID, access)
		if err != nil {
			return item, mapRoleDependencyError(err)
		}
		item.Status = RealizationStatusRealized
		item.ResourceID = result.KnowledgeBaseID
		item.AccessLevel = result.AccessLevel
		item.Message = "knowledge base view access granted"
		return item, nil
	default:
		item.Status = RealizationStatusUnresolved
		item.Message = "unsupported role attachment type"
		return item, nil
	}
}

func (s *Service) validate(workspaceID, subject string) error {
	if s == nil || s.store == nil {
		return repos.ErrInvalidArgument
	}
	return validateScope(workspaceID, subject)
}

func validateScope(workspaceID, subject string) error {
	if strings.TrimSpace(subject) == "" {
		return repos.ErrUnauthorized
	}
	if strings.TrimSpace(workspaceID) == "" {
		return invalidRoleArgument("workspace id is required")
	}
	return nil
}

func (s *Service) realizationManifest(ctx context.Context, actor string, role Role, requestedVersionID string) (string, []rolesstore.RolePackageManifestItem, map[string]string, error) {
	requestedVersionID = strings.TrimSpace(requestedVersionID)
	if strings.TrimSpace(role.Source) == SourcePresetRole {
		manifestItems := rolePackageManifestStoreItems(role)
		versionID := ""
		if len(manifestItems) > 0 {
			versionID = strings.TrimSpace(manifestItems[0].RoleVersionID)
		}
		if versionID == "" {
			versionID = newRoleVersionID()
			for i := range manifestItems {
				manifestItems[i].RoleVersionID = versionID
			}
		}
		return versionID, manifestItems, nil, nil
	}
	stored, err := s.store.GetRole(ctx, role.WorkspaceID, role.ID)
	if err != nil {
		return "", nil, nil, mapStoreNotFound(err)
	}
	versionID := firstNonEmpty(requestedVersionID, stored.ActiveVersionID)
	if versionID == "" {
		return "", nil, nil, fmt.Errorf("active role version is missing: %w", repos.ErrInvalidArgument)
	}
	var manifestItems []rolesstore.RolePackageManifestItem
	var version rolesstore.RoleVersion
	if requestedVersionID != "" {
		version, err = s.store.GetRoleVersion(ctx, stored.WorkspaceID, stored.ID, requestedVersionID)
		if err != nil {
			return "", nil, nil, mapStoreNotFound(err)
		}
		stored.Definition = version.Definition
		stored.RuntimeHintsJSON = version.RuntimeHintsJSON
		manifestItems, err = s.store.ListManifestItemsByRoleVersion(ctx, stored.WorkspaceID, stored.ID, requestedVersionID)
		if err != nil {
			return "", nil, nil, err
		}
	} else {
		version, err = s.store.GetActiveRoleVersion(ctx, stored.WorkspaceID, stored.ID)
		if err != nil {
			return "", nil, nil, mapStoreNotFound(err)
		}
		manifestByRole, err := s.store.ListActiveManifestItemsByRoleIDs(ctx, stored.WorkspaceID, []string{stored.ID})
		if err != nil {
			return "", nil, nil, err
		}
		manifestItems = manifestByRole[stored.ID]
	}
	if len(manifestItems) == 0 {
		if requestedVersionID != "" {
			return "", nil, nil, fmt.Errorf("role package manifest for version %q is missing: %w", requestedVersionID, repos.ErrInvalidArgument)
		}
		return "", nil, nil, fmt.Errorf("active role package manifest is missing: %w", repos.ErrInvalidArgument)
	}
	contentByRef, err := s.resolveRequiredManifestItemContents(ctx, actor, stored, version, manifestItems)
	if err != nil {
		return "", nil, nil, err
	}
	return versionID, manifestItems, contentByRef, nil
}

func (s *Service) persistRealization(ctx context.Context, subject string, role Role, agentID, versionID string, manifestItems []rolesstore.RolePackageManifestItem, result RealizationResult) (string, error) {
	now := s.now()
	realizationID := newRoleRealizationID()
	status := realizationStatus(result.Items)
	items := make([]rolesstore.RoleRealizationItem, 0, len(result.Items))
	for idx, item := range result.Items {
		manifestItemID := ""
		if idx < len(manifestItems) {
			manifestItemID = strings.TrimSpace(manifestItems[idx].ID)
		}
		items = append(items, rolesstore.RoleRealizationItem{ID: newRoleRealizationItemID(), RealizationID: realizationID, ManifestItemID: manifestItemID, ManifestItemType: item.ManifestItemType, ManifestItemName: item.ManifestItemName, Action: item.Action, Status: item.Status, ResourceID: item.ResourceID, Branch: item.Branch, AccessLevel: item.AccessLevel, Message: item.Message, Required: item.Required, SortOrder: idx})
	}
	err := s.store.SaveRoleRealization(ctx, rolesstore.RealizationWrite{
		Realization: rolesstore.RoleRealization{ID: realizationID, WorkspaceID: role.WorkspaceID, AgentID: agentID, RoleID: role.ID, RoleVersionID: versionID, Status: status, DryRun: false, CreatedBy: strings.TrimSpace(subject), CreatedAt: now},
		Items:       items,
		Binding:     roleBindingFromRole(role, agentID, versionID, realizationID, subject, now),
	})
	return realizationID, err
}

func realizationStatus(items []RealizationItem) string {
	status := RealizationStatusRealized
	for _, item := range items {
		switch strings.TrimSpace(item.Status) {
		case RealizationStatusFailed:
			return RealizationStatusFailed
		case RealizationStatusUnresolved:
			status = RealizationStatusUnresolved
		case RealizationStatusSkipped:
			if status == RealizationStatusRealized {
				status = RealizationStatusPartial
			}
		}
	}
	return status
}

func roleBindingFromRole(role Role, agentID, versionID, realizationID, subject string, now time.Time) *rolesstore.AgentRoleBinding {
	bindingKind := "my_role"
	sourcePresetRoleID := ""
	sourcePresetVersionID := ""
	activeDefinitionPath := "memory/system/identity.md"
	switch role.Provenance.Kind {
	case "preset":
		bindingKind = "preset_derived"
		sourcePresetRoleID = firstNonEmpty(role.SourcePresetID, role.Provenance.Ref)
		if role.SourcePresetVersion > 0 {
			sourcePresetVersionID = fmt.Sprintf("%d", role.SourcePresetVersion)
		} else if role.Provenance.Version > 0 {
			sourcePresetVersionID = fmt.Sprintf("%d", role.Provenance.Version)
		}
	}
	return &rolesstore.AgentRoleBinding{ID: newAgentRoleBindingID(), WorkspaceID: role.WorkspaceID, AgentID: strings.TrimSpace(agentID), BindingKind: bindingKind, RoleID: role.ID, RoleVersionID: strings.TrimSpace(versionID), SourcePresetRoleID: sourcePresetRoleID, SourcePresetVersionID: sourcePresetVersionID, ActiveDefinitionPath: activeDefinitionPath, RealizationID: strings.TrimSpace(realizationID), CreatedBy: strings.TrimSpace(subject), UpdatedBy: strings.TrimSpace(subject), CreatedAt: now, UpdatedAt: now}
}

func (s *Service) roleGrant(ctx context.Context, role rolesstore.Role, subject string) (string, error) {
	grant, err := s.store.GetRoleGrant(ctx, role.WorkspaceID, role.ID, "user", strings.TrimSpace(subject))
	if err == nil {
		return normalizeRoleGrant(grant.Role), nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return "", err
	}
	if strings.TrimSpace(role.CreatedBy) == strings.TrimSpace(subject) {
		return roleGrantOwner, nil
	}
	if roleVisibilityForRead(role.Visibility) == RoleVisibilityWorkspace {
		return roleGrantView, nil
	}
	return "", nil
}

func roleCanManage(role string) bool {
	switch normalizeRoleGrant(role) {
	case roleGrantOwner, roleGrantManage:
		return true
	default:
		return false
	}
}

func roleGrantForList(role rolesstore.Role, subject string, grantByRoleID map[string]string) string {
	if grant, ok := grantByRoleID[role.ID]; ok {
		return grant
	}
	if strings.TrimSpace(role.CreatedBy) == strings.TrimSpace(subject) {
		return roleGrantOwner
	}
	if roleVisibilityForRead(role.Visibility) == RoleVisibilityWorkspace {
		return roleGrantView
	}
	return ""
}

func normalizeRoleVisibility(visibility string) (string, error) {
	switch strings.ToLower(strings.TrimSpace(visibility)) {
	case RoleVisibilityPrivate:
		return RoleVisibilityPrivate, nil
	case RoleVisibilityWorkspace:
		return RoleVisibilityWorkspace, nil
	default:
		return "", invalidRoleArgument("role visibility must be private or workspace")
	}
}

func roleVisibilityForRead(visibility string) string {
	switch strings.ToLower(strings.TrimSpace(visibility)) {
	case RoleVisibilityWorkspace:
		return RoleVisibilityWorkspace
	default:
		return RoleVisibilityPrivate
	}
}

func normalizeRoleListView(view string) string {
	switch strings.ToLower(strings.TrimSpace(view)) {
	case RoleListViewWorkspace:
		return RoleListViewWorkspace
	case RoleListViewCreatedByMe:
		return RoleListViewCreatedByMe
	case RoleListViewPrivate:
		return RoleListViewPrivate
	default:
		return RoleListViewAll
	}
}

func normalizeRoleGrant(role string) string {
	switch strings.ToLower(strings.TrimSpace(role)) {
	case roleGrantOwner, roleGrantManage, roleGrantEdit, roleGrantView:
		return strings.ToLower(strings.TrimSpace(role))
	default:
		return roleGrantView
	}
}

func roleFromStore(row rolesstore.Role, items []rolesstore.RolePackageManifestItem, contentByRef map[string]string, source string) Role {
	source = storedRoleSource(row, source)
	role := Role{
		ID:                    row.ID,
		WorkspaceID:           row.WorkspaceID,
		Source:                source,
		Visibility:            roleVisibilityForRead(row.Visibility),
		Slug:                  row.Slug,
		Name:                  row.Name,
		Description:           row.Description,
		Locale:                row.Locale,
		Scenario:              row.Scenario,
		Icon:                  row.Icon,
		Tags:                  decodeStrings(row.TagsJSON),
		Definition:            row.Definition,
		RuntimeHints:          decodeRuntimeHints(row.RuntimeHintsJSON),
		RolePackageManifest:   rolePackageManifestFromStoreItems(items, contentByRef),
		RoleVersionID:         row.ActiveVersionID,
		SourcePresetID:        row.SourcePresetID,
		SourcePresetVersion:   row.SourcePresetVersion,
		SourcePresetVersionID: sourcePresetVersionID(row.SourcePresetVersion),
		SourceAgentID:         row.SourceAgentID,
		IsActive:              row.IsActive,
		CreatedBy:             row.CreatedBy,
		UpdatedBy:             row.UpdatedBy,
		CreatedAt:             row.CreatedAt,
		UpdatedAt:             row.UpdatedAt,
		Role:                  row.Role,
		CanManage:             row.CanManage,
	}
	return decorateRoleContract(role)
}

func storedRoleSource(row rolesstore.Role, fallback string) string {
	if strings.TrimSpace(row.SourceAgentID) != "" {
		return SourceAgentDerivedRole
	}
	if strings.TrimSpace(fallback) != "" {
		return fallback
	}
	return SourceMyRole
}

func decorateRoleContract(role Role) Role {
	role.OwnerID = firstNonEmpty(role.OwnerID, role.CreatedBy)
	switch role.Source {
	case SourcePresetRole:
		role.OwnerID = firstNonEmpty(role.OwnerID, "system")
	case SourceAgentDerivedRole:
		role.OwnerID = firstNonEmpty(role.OwnerID, role.CreatedBy)
	}
	role.Editable = roleCanManage(role.Role)
	role.Provenance = roleProvenance(role)
	role.RolePackage = rolePackage(role)
	if def := roleDefinitionFromManifest(role.RolePackageManifest); def != "" {
		role.Definition = def
	}
	return role
}

func roleProvenance(role Role) RoleProvenance {
	switch role.Source {
	case SourcePresetRole:
		return RoleProvenance{Kind: "preset", Ref: firstNonEmpty(role.SourcePresetID, role.Slug), Version: role.SourcePresetVersion}
	case SourceAgentDerivedRole:
		return RoleProvenance{Kind: "agent", Ref: firstNonEmpty(role.SourceAgentID, role.CreatedBy)}
	default:
		if strings.TrimSpace(role.SourcePresetID) != "" {
			return RoleProvenance{Kind: "preset", Ref: role.SourcePresetID, Version: role.SourcePresetVersion}
		}
		if strings.TrimSpace(role.SourceAgentID) != "" {
			return RoleProvenance{Kind: "agent", Ref: role.SourceAgentID}
		}
		return RoleProvenance{Kind: "manual", Ref: firstNonEmpty(role.CreatedBy, role.OwnerID)}
	}
}

func sourcePresetVersionID(version int) string {
	if version <= 0 {
		return ""
	}
	return fmt.Sprintf("%d", version)
}

func rolePackage(role Role) RolePackage {
	sections := []string{AttachmentRoleDefinition}
	if strings.TrimSpace(roleDefinitionFromManifest(role.RolePackageManifest)) == "" {
		return RolePackage{Portable: true, Sections: sections, NonPortable: nil, Unresolved: []RolePackageUnresolved{{Section: AttachmentRoleDefinition, Name: "IDENTITY.md", Reason: "role definition content_ref is unresolved", Required: true}}}
	}
	seen := map[string]struct{}{AttachmentRoleDefinition: {}}
	var unresolved []RolePackageUnresolved
	addSection := func(section string) {
		section = strings.TrimSpace(section)
		if section == "" {
			return
		}
		if _, ok := seen[section]; ok {
			return
		}
		seen[section] = struct{}{}
		sections = append(sections, section)
	}
	for _, attachment := range attachmentsFromRoleManifest(role.RolePackageManifest) {
		switch attachment.Type {
		case AttachmentPortableKnowledge:
			addSection(AttachmentPortableKnowledge)
			if strings.TrimSpace(attachment.Content) == "" {
				unresolved = append(unresolved, RolePackageUnresolved{Section: AttachmentPortableKnowledge, Name: firstNonEmpty(attachment.Name, attachment.Type), Reason: "portable knowledge content_ref is unresolved", Required: attachment.Required})
			}
		case AttachmentSkill:
			addSection(AttachmentSkill)
			if strings.TrimSpace(attachment.ResourceID) == "" && strings.TrimSpace(attachment.Content) == "" {
				unresolved = append(unresolved, RolePackageUnresolved{Section: AttachmentSkill, Name: firstNonEmpty(attachment.Name, attachment.Type), Reason: "skill resource or template content_ref is unresolved", Required: attachment.Required})
			}
		case AttachmentKnowledgeBase:
			addSection(AttachmentKnowledgeBase)
			if strings.TrimSpace(attachment.ResourceID) == "" {
				unresolved = append(unresolved, RolePackageUnresolved{Section: AttachmentKnowledgeBase, Name: firstNonEmpty(attachment.Name, attachment.Type), Reason: "knowledge base reference is unresolved", Required: attachment.Required})
			}
		}
	}
	return RolePackage{Portable: true, Sections: sections, NonPortable: nil, Unresolved: unresolved}
}

func validatePortableRoleAttachments(items []Attachment) error {
	for _, item := range normalizeAttachments(items) {
		switch item.Type {
		case AttachmentPortableKnowledge, AttachmentSkill, AttachmentKnowledgeBase, AttachmentMemory:
			continue
		case ManifestItemMessageHistory, ManifestItemAgentLocalMemory, ManifestItemRuntimeSession, ManifestItemWorkspaceFile:
			return invalidPortableAttachmentType(item.Type)
		default:
			return invalidPortableAttachmentType(item.Type)
		}
	}
	return nil
}

func invalidPortableAttachmentType(itemType string) error {
	itemType = strings.TrimSpace(itemType)
	if itemType == "" {
		return invalidRoleArgument("role package item type is required")
	}
	return invalidRoleArgument(fmt.Sprintf("role package item type %q is not portable", itemType))
}

func invalidRoleArgument(message string) error {
	message = strings.TrimSpace(message)
	if message == "" {
		return repos.ErrInvalidArgument
	}
	return fmt.Errorf("%s: %w", message, repos.ErrInvalidArgument)
}

func canonicalizePortableRole(definition string, runtimeHints RuntimeHints, attachments []Attachment) (Role, []rolesstore.RolePackageManifestItem, error) {
	definition = strings.TrimSpace(definition)
	if definition == "" {
		return Role{}, nil, invalidRoleArgument("role definition is required")
	}
	attachments = normalizeAttachments(attachments)
	if err := validatePortableRoleAttachments(attachments); err != nil {
		return Role{}, nil, err
	}
	portable := Role{Definition: definition, RuntimeHints: runtimeHints}
	versionID := newRoleVersionID()
	items := manifestItemsFromRoleDefinitionAndAttachments("", "", versionID, definition, attachments)
	portable.RolePackageManifest = rolePackageManifestFromStoreItems(items, nil)
	return portable, items, nil
}

func roleDefinitionManifestItem(items []rolesstore.RolePackageManifestItem) (rolesstore.RolePackageManifestItem, bool) {
	for _, item := range items {
		if strings.TrimSpace(item.ItemType) == ManifestItemRoleDefinitionAsset {
			return item, true
		}
	}
	return rolesstore.RolePackageManifestItem{}, false
}

func rolePackageContentMatchesManifest(item rolesstore.RolePackageManifestItem, content string) bool {
	content = strings.TrimSpace(content)
	if content == "" {
		return false
	}
	hash := strings.TrimSpace(item.ContentHash)
	if hash == "" {
		return true
	}
	return strings.EqualFold(rolePackageContentHash(content), hash)
}

func rolePackageManifestFromStoreItems(items []rolesstore.RolePackageManifestItem, contentByRef map[string]string) RolePackageManifest {
	out := make([]RolePackageManifestItem, 0, len(items))
	for _, item := range items {
		content := ""
		if item.Content != "" && contentByRef == nil {
			content = item.Content
		}
		if strings.TrimSpace(item.ContentRef) != "" && contentByRef != nil {
			if resolved, ok := contentByRef[strings.TrimSpace(item.ContentRef)]; ok {
				content = resolved
			}
		}
		out = append(out, RolePackageManifestItem{Type: item.ItemType, Name: item.Name, Description: item.Description, Required: item.Required, Action: item.RealizationAction, ResourceID: item.SourceResourceID, AccessLevel: item.AccessLevel, Branch: item.Branch, Content: content})
	}
	return RolePackageManifest{Items: out}
}

func rolePackageManifestStoreItems(role Role) []rolesstore.RolePackageManifestItem {
	definition := strings.TrimSpace(roleDefinitionFromManifest(role.RolePackageManifest))
	if definition == "" {
		definition = strings.TrimSpace(role.Definition)
	}
	return manifestItemsFromRoleDefinitionAndAttachments(role.ID, role.WorkspaceID, "", definition, attachmentsFromRoleManifest(role.RolePackageManifest))
}

func roleDefinitionFromManifest(manifest RolePackageManifest) string {
	for _, item := range manifest.Items {
		if strings.TrimSpace(item.Type) == ManifestItemRoleDefinitionAsset {
			return strings.TrimSpace(item.Content)
		}
	}
	return ""
}

func inlineRolePackageAssets(items []rolesstore.RolePackageManifestItem) []RolePackageAsset {
	assets := make([]RolePackageAsset, 0, len(items))
	for _, item := range items {
		if strings.TrimSpace(item.ContentRef) != "" {
			continue
		}
		content := strings.TrimSpace(item.Content)
		if content == "" || roleManifestItemProjection(item) != roleManifestItemProjectionExact {
			continue
		}
		assets = append(assets, RolePackageAsset{ItemType: item.ItemType, Name: item.Name, Content: content})
	}
	return assets
}

func attachmentsFromRoleManifest(manifest RolePackageManifest) []Attachment {
	out := make([]Attachment, 0, len(manifest.Items))
	for _, item := range manifest.Items {
		switch strings.TrimSpace(item.Type) {
		case "", AttachmentRoleDefinition, ManifestItemRoleDefinitionAsset, AttachmentMemory:
			continue
		}
		out = append(out, Attachment{Type: item.Type, Name: item.Name, Description: item.Description, Required: item.Required, Realization: item.Action, ResourceID: item.ResourceID, AccessLevel: item.AccessLevel, Branch: item.Branch, Content: item.Content})
	}
	return out
}

func manifestItemsFromRoleDefinitionAndAttachments(roleID, workspaceID, versionID, definition string, attachments []Attachment) []rolesstore.RolePackageManifestItem {
	if strings.TrimSpace(versionID) == "" {
		versionID = newRoleVersionID()
	}
	items := []rolesstore.RolePackageManifestItem{{ID: newRoleManifestItemID(), RoleVersionID: versionID, RoleID: strings.TrimSpace(roleID), WorkspaceID: strings.TrimSpace(workspaceID), ItemType: ManifestItemRoleDefinitionAsset, Name: "IDENTITY.md", Description: "Canonical role definition", Required: true, RealizationAction: "materialize", SourceKind: "inline", Content: strings.TrimSpace(definition), SortOrder: 0, MetadataJSON: "{}"}}
	for _, attachment := range normalizeAttachments(attachments) {
		if attachment.Type == AttachmentMemory {
			continue
		}
		item := rolesstore.RolePackageManifestItem{ID: newRoleManifestItemID(), RoleVersionID: versionID, RoleID: strings.TrimSpace(roleID), WorkspaceID: strings.TrimSpace(workspaceID), ItemType: attachment.Type, Name: attachment.Name, Description: attachment.Description, Required: attachment.Required, RealizationAction: realizationAction(attachment), SourceKind: manifestItemSourceKind(attachment), SourceResourceID: attachment.ResourceID, SourceRef: attachment.ResourceID, ContentRef: "", ContentHash: "", AccessLevel: attachment.AccessLevel, Branch: attachment.Branch, Content: "", SortOrder: len(items), MetadataJSON: normalizedAttachmentMetadataJSON(attachment.Metadata)}
		switch attachment.Type {
		case AttachmentPortableKnowledge, AttachmentSkill:
			item.Content = attachment.Content
		case AttachmentKnowledgeBase:
			item.Content = ""
		}
		items = append(items, item)
	}
	return items
}

func manifestItemSourceKind(attachment Attachment) string {
	switch attachment.Type {
	case AttachmentPortableKnowledge:
		return "inline"
	case AttachmentSkill:
		return "template"
	default:
		if strings.TrimSpace(attachment.ResourceID) != "" {
			return "resource"
		}
		return "inline"
	}
}

func normalizedAttachmentMetadataJSON(metadata json.RawMessage) string {
	if len(metadata) == 0 {
		return "{}"
	}
	trimmed := strings.TrimSpace(string(metadata))
	if trimmed == "" {
		return "{}"
	}
	return trimmed
}

func roleDefinitionSummary(definition string) string {
	definition = strings.TrimSpace(definition)
	if definition == "" {
		return ""
	}
	const maxSummary = 280
	definition = strings.Join(strings.Fields(definition), " ")
	runes := []rune(definition)
	if len(runes) <= maxSummary {
		return definition
	}
	return strings.TrimSpace(string(runes[:maxSummary]))
}

func storeRoleFromMutation(id, workspaceID, subject, slug string, input Mutation, now time.Time) (rolesstore.Role, error) {
	definition := roleDefinitionSummary(strings.TrimSpace(input.Definition))
	if definition == "" {
		return rolesstore.Role{}, invalidRoleArgument("role definition is required")
	}
	tags, err := encodeJSON(normalizeStringSlice(input.Tags))
	if err != nil {
		return rolesstore.Role{}, err
	}
	hints, err := encodeJSON(input.RuntimeHints)
	if err != nil {
		return rolesstore.Role{}, err
	}
	attachments := normalizeAttachments(input.Attachments)
	for i := range attachments {
		attachments[i].Content = ""
	}
	attachmentsJSON, err := encodeJSON(attachments)
	if err != nil {
		return rolesstore.Role{}, err
	}
	return rolesstore.Role{ID: id, WorkspaceID: workspaceID, OwnerType: "user", OwnerID: subject, Slug: slug, Name: strings.TrimSpace(input.Name), Description: strings.TrimSpace(input.Description), Locale: normalizeLocale(input.Locale), Scenario: strings.TrimSpace(input.Scenario), Icon: strings.TrimSpace(input.Icon), TagsJSON: tags, Definition: definition, RuntimeHintsJSON: hints, AttachmentsJSON: attachmentsJSON, Visibility: RoleVisibilityPrivate, IsActive: true, CreatedBy: subject, UpdatedBy: subject, CreatedAt: now, UpdatedAt: now}, nil
}

func roleFromPreset(preset presetTemplate, workspaceID string) Role {
	id := presetRoleID(preset.ID, preset.Locale)
	versionID := newRoleVersionID()
	role := Role{ID: id, WorkspaceID: workspaceID, Source: SourcePresetRole, Slug: preset.ID, Name: preset.Name, Description: preset.Description, Locale: preset.Locale, Scenario: preset.Scenario, Icon: preset.Icon, Tags: append([]string(nil), preset.Tags...), Definition: preset.Definition, RuntimeHints: preset.RuntimeHints, RolePackageManifest: rolePackageManifestFromStoreItems(manifestItemsFromRoleDefinitionAndAttachments(id, workspaceID, versionID, preset.Definition, cloneAttachments(preset.Attachments)), nil), RoleVersionID: versionID, SourcePresetID: preset.ID, SourcePresetVersion: preset.Version, SourcePresetVersionID: sourcePresetVersionID(preset.Version), IsActive: true}
	return decorateRoleContract(role)
}

func roleIDs(roles []rolesstore.Role) []string {
	out := make([]string, 0, len(roles))
	for _, role := range roles {
		if strings.TrimSpace(role.ID) != "" {
			out = append(out, role.ID)
		}
	}
	return out
}

func (s *Service) availableSlug(ctx context.Context, workspaceID, name, id string) (string, error) {
	base := repos.RepositoryNameStem(firstNonEmpty(name, "role"))
	if base == "" {
		base = "role"
	}
	candidate := base
	for i := 0; i < 100; i++ {
		exists, err := s.store.RoleSlugExists(ctx, strings.TrimSpace(workspaceID), candidate, id)
		if err != nil {
			return "", err
		}
		if !exists {
			return candidate, nil
		}
		candidate = fmt.Sprintf("%s-%d", base, i+2)
	}
	return base + "-" + idSuffix(id), nil
}

func mapStoreNotFound(err error) error {
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return repos.ErrNotFound
	}
	return err
}

func mapStoreRoleConflict(err error) error {
	if errors.Is(err, rolesstore.ErrRoleSlugConflict) {
		return roleConflictError{message: "role name is already used in this workspace"}
	}
	return err
}

type roleConflictError struct {
	message string
}

func (e roleConflictError) Error() string {
	return e.message
}

func (e roleConflictError) Unwrap() error {
	return repos.ErrConflict
}

func mapSkillError(err error) error {
	switch {
	case errors.Is(err, skills.ErrUnauthorized):
		return repos.ErrUnauthorized
	case errors.Is(err, skills.ErrInvalidArgument):
		return repos.ErrInvalidArgument
	case errors.Is(err, skills.ErrForbidden):
		return repos.ErrForbidden
	case errors.Is(err, skills.ErrNotFound):
		return repos.ErrNotFound
	case errors.Is(err, skills.ErrConflict):
		return repos.ErrConflict
	default:
		return err
	}
}

func mapRoleDependencyError(err error) error {
	if err == nil {
		return nil
	}
	switch {
	case errors.Is(err, repos.ErrUnauthorized),
		errors.Is(err, repos.ErrInvalidArgument),
		errors.Is(err, repos.ErrForbidden),
		errors.Is(err, repos.ErrNotFound),
		errors.Is(err, repos.ErrConflict),
		errors.Is(err, skills.ErrUnauthorized),
		errors.Is(err, skills.ErrInvalidArgument),
		errors.Is(err, skills.ErrForbidden),
		errors.Is(err, skills.ErrNotFound),
		errors.Is(err, skills.ErrConflict),
		IsRoleDependencyUnavailable(err):
		return err
	default:
		return fmt.Errorf("%w: %w", errRoleDependencyUnavailable, err)
	}
}

func realizationAction(attachment Attachment) string {
	switch attachment.Type {
	case AttachmentSkill:
		if strings.TrimSpace(attachment.ResourceID) == "" {
			if strings.TrimSpace(attachment.Realization) != "" {
				return strings.TrimSpace(attachment.Realization)
			}
			return RealizationCopy
		}
		return RealizationBind
	case AttachmentKnowledgeBase:
		if strings.TrimSpace(attachment.Realization) != "" {
			return strings.TrimSpace(attachment.Realization)
		}
		return RealizationGrant
	case AttachmentPortableKnowledge:
		if strings.TrimSpace(attachment.Realization) != "" {
			return strings.TrimSpace(attachment.Realization)
		}
		return RealizationCopy
	default:
		return strings.TrimSpace(attachment.Realization)
	}
}

func roleDefinitionRealizationMessage(dryRun bool) string {
	if dryRun {
		return "portable role definition will be materialized on apply; existing history, local memory, runtime state, and workspace files stay untouched"
	}
	return "portable role definition materialized for the current binding; existing history, local memory, runtime state, and workspace files stay untouched"
}

func plannedMessage(attachment Attachment) string {
	switch attachment.Type {
	case AttachmentPortableKnowledge:
		return "portable role assets will be materialized on apply without rewriting existing history, local memory, runtime state, or workspace files"
	case AttachmentSkill:
		if strings.TrimSpace(attachment.ResourceID) == "" {
			return "portable skill will be materialized on apply"
		}
		return "skill will be bound or materialized for the agent"
	case AttachmentKnowledgeBase:
		return "knowledge base view access will be granted after authorization"
	default:
		return "attachment will be resolved by its owning resource service"
	}
}

func validateRealizationMode(input RealizationRequest) error {
	if input.DryRun && input.Apply {
		return fmt.Errorf("realization mode must choose either dryRun preview or apply, not both: %w", repos.ErrInvalidArgument)
	}
	if !input.DryRun && !input.Apply {
		return fmt.Errorf("realization mode must set dryRun or apply explicitly: %w", repos.ErrInvalidArgument)
	}
	return nil
}

func plannedOrRealized(dryRun bool) string {
	if dryRun {
		return RealizationStatusPlanned
	}
	return RealizationStatusRealized
}

func normalizeAttachments(items []Attachment) []Attachment {
	out := make([]Attachment, 0, len(items))
	for _, item := range items {
		item.Type = strings.TrimSpace(item.Type)
		if item.Type == "" {
			continue
		}
		item.Name = strings.TrimSpace(item.Name)
		item.Description = strings.TrimSpace(item.Description)
		item.Realization = strings.TrimSpace(item.Realization)
		item.ResourceID = strings.TrimSpace(item.ResourceID)
		item.AccessLevel = strings.TrimSpace(item.AccessLevel)
		item.Branch = strings.TrimSpace(item.Branch)
		item.Content = strings.TrimSpace(item.Content)
		if item.Realization == "" {
			item.Realization = realizationAction(item)
		}
		out = append(out, item)
	}
	return out
}

func cloneAttachments(items []Attachment) []Attachment {
	out := make([]Attachment, 0, len(items))
	for _, item := range items {
		if item.Metadata != nil {
			item.Metadata = append(json.RawMessage(nil), item.Metadata...)
		}
		out = append(out, item)
	}
	return out
}

func normalizeStringSlice(values []string) []string {
	out := make([]string, 0, len(values))
	seen := map[string]struct{}{}
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	sort.Strings(out)
	return out
}

func normalizeLocale(locale string) string {
	locale = strings.TrimSpace(locale)
	if locale == "" {
		return "en-US"
	}
	return locale
}

func decodeStrings(raw string) []string {
	var out []string
	if err := json.Unmarshal([]byte(firstNonEmpty(raw, "[]")), &out); err != nil {
		return nil
	}
	return out
}

func decodeRuntimeHints(raw string) RuntimeHints {
	var out RuntimeHints
	_ = json.Unmarshal([]byte(firstNonEmpty(raw, "{}")), &out)
	return out
}

func decodeAttachments(raw string) []Attachment {
	var out []Attachment
	if err := json.Unmarshal([]byte(firstNonEmpty(raw, "[]")), &out); err != nil {
		return nil
	}
	return out
}

func encodeJSON(value any) (string, error) {
	data, err := json.Marshal(value)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func newRoleID() string {
	id, err := identifiers.NewEntityID("role")
	if err != nil {
		panic(err)
	}
	return id
}

func newRoleGrantID() string {
	id, err := identifiers.NewEntityID("rgr")
	if err != nil {
		panic(err)
	}
	return id
}

func newRoleVersionID() string {
	id, err := identifiers.NewEntityID("rlv")
	if err != nil {
		panic(err)
	}
	return id
}

func newRoleManifestItemID() string {
	id, err := identifiers.NewEntityID("rpmi")
	if err != nil {
		panic(err)
	}
	return id
}

func newRoleRealizationID() string {
	id, err := identifiers.NewEntityID("rrl")
	if err != nil {
		panic(err)
	}
	return id
}

func newRoleRealizationItemID() string {
	id, err := identifiers.NewEntityID("rri")
	if err != nil {
		panic(err)
	}
	return id
}

func newAgentRoleBindingID() string {
	id, err := identifiers.NewEntityID("arb")
	if err != nil {
		panic(err)
	}
	return id
}

func presetRoleID(id, locale string) string {
	sum := sha1.Sum([]byte(strings.TrimSpace(id) + "\x00" + strings.TrimSpace(locale)))
	return "preset_role_" + hex.EncodeToString(sum[:])[:24]
}

func idSuffix(id string) string {
	parts := strings.Split(strings.TrimSpace(id), "_")
	if len(parts) == 0 {
		return strings.TrimSpace(id)
	}
	last := parts[len(parts)-1]
	if len(last) > 6 {
		return last[:6]
	}
	return last
}

func normalizePagination(page, perPage int) (int, int) {
	if page < 1 {
		page = 1
	}
	if perPage < 1 {
		perPage = 20
	}
	if perPage > 100 {
		perPage = 100
	}
	return page, perPage
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}
