package memory

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

	"github.com/tidbcloud/AgentTeam/internal/service/resource/access"
	reposservice "github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	"github.com/tidbcloud/AgentTeam/internal/store/content"
	memorystore "github.com/tidbcloud/AgentTeam/internal/store/resource/memory"
	resourcerepostore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"gorm.io/gorm"
)

const (
	MemorySyncProtocolLegacyGitV1     = "legacy_git_v1"
	MemorySyncProtocolNativeContentV1 = "native_content_v1"
)

func (s *MemoryService) GetAgentAccessibleMemory(ctx context.Context, workspaceID, subject, agentID string) (*AgentAccessibleMemory, error) {
	if s.store == nil {
		return nil, invalidArgument("memory service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return nil, err
	}
	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
	}
	row, err := s.store.GetAgentAccessibleMemory(ctx, strings.TrimSpace(workspaceID), agentID, false)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, err
	}
	authorities, err := s.memorySyncAuthorities(ctx, workspaceID, []string{row.Memory.ID})
	if err != nil {
		return nil, err
	}
	protocol, err := s.memorySyncProtocol(row.Memory, authorities[row.Memory.ID])
	if err != nil {
		return nil, err
	}
	var repository *resourcerepostore.ResourceRepository
	if protocol == MemorySyncProtocolLegacyGitV1 {
		repositories, err := s.repositoriesByMemoryID(ctx, workspaceID, []string{row.Memory.ID})
		if err != nil {
			return nil, err
		}
		repository = repositories[row.Memory.ID]
		latest, err := s.store.LatestMemoryCommitTimesForBranches(ctx, workspaceID, map[string]string{row.Memory.ID: row.Binding.Branch})
		if err != nil {
			return nil, err
		}
		row.LastSyncAt = latest[row.Memory.ID]
	}
	converted := agentAccessibleMemoryFromRow(*row, repository, protocol)
	return &converted, nil
}

// BatchListAgentAccessibleMemories returns per-agent memory bindings for the
// requested agent IDs in one call. It is the server-side counterpart of
// Desktop's pull on agent catalog updates: Desktop sends the agent IDs it
// currently hosts, MeshX responds with each agent's bound memory (or nil) and
// the primary repository refs needed to clone it.
//
// The returned map always contains an entry for every requested agent ID;
// agents without bindings map to nil so callers can distinguish "no access"
// from "agent dropped from response".
func (s *MemoryService) BatchListAgentAccessibleMemories(ctx context.Context, workspaceID, subject string, agentIDs []string) (AgentMemoryAccessBatch, error) {
	if s.store == nil {
		return AgentMemoryAccessBatch{}, invalidArgument("memory service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return AgentMemoryAccessBatch{}, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	deduped := access.DeduplicateAgentIDs(agentIDs)
	out := make(map[string]*AgentAccessibleMemory, len(deduped))
	if len(deduped) == 0 {
		return AgentMemoryAccessBatch{Agents: out}, nil
	}
	if err := s.requireOwnedAgents(ctx, workspaceID, subject, deduped); err != nil {
		return AgentMemoryAccessBatch{}, err
	}
	allMemoryIDs := make([]string, 0)
	rowsByAgent, err := s.store.ListAgentAccessibleMemories(ctx, workspaceID, deduped, false)
	if err != nil {
		return AgentMemoryAccessBatch{}, err
	}
	for _, row := range rowsByAgent {
		allMemoryIDs = append(allMemoryIDs, row.Memory.ID)
	}
	authorities, err := s.memorySyncAuthorities(ctx, workspaceID, allMemoryIDs)
	if err != nil {
		return AgentMemoryAccessBatch{}, err
	}
	legacyMemoryIDs := make([]string, 0, len(allMemoryIDs))
	legacyBranches := make(map[string]string, len(allMemoryIDs))
	protocols := make(map[string]string, len(allMemoryIDs))
	for _, row := range rowsByAgent {
		protocol, err := s.memorySyncProtocol(row.Memory, authorities[row.Memory.ID])
		if err != nil {
			return AgentMemoryAccessBatch{}, err
		}
		protocols[row.Memory.ID] = protocol
		if protocol == MemorySyncProtocolLegacyGitV1 {
			legacyMemoryIDs = append(legacyMemoryIDs, row.Memory.ID)
			legacyBranches[row.Memory.ID] = row.Binding.Branch
		}
	}
	latestLegacySync, err := s.store.LatestMemoryCommitTimesForBranches(ctx, workspaceID, legacyBranches)
	if err != nil {
		return AgentMemoryAccessBatch{}, err
	}
	repositories, err := s.repositoriesByMemoryID(ctx, workspaceID, legacyMemoryIDs)
	if err != nil {
		return AgentMemoryAccessBatch{}, err
	}
	for _, agentID := range deduped {
		row, ok := rowsByAgent[agentID]
		if !ok {
			out[agentID] = nil
			continue
		}
		if protocols[row.Memory.ID] == MemorySyncProtocolLegacyGitV1 {
			row.LastSyncAt = latestLegacySync[row.Memory.ID]
		}
		converted := agentAccessibleMemoryFromRow(row, repositories[row.Memory.ID], protocols[row.Memory.ID])
		out[agentID] = &converted
	}
	lease, err := s.createMemorySyncToken(ctx, workspaceID, subject, out)
	if err != nil {
		return AgentMemoryAccessBatch{}, err
	}
	return AgentMemoryAccessBatch{Agents: out, SyncToken: lease.Token, SyncTokenExpires: lease.ExpiresAt}, nil
}

func (s *MemoryService) createMemorySyncToken(ctx context.Context, workspaceID, subject string, agents map[string]*AgentAccessibleMemory) (reposservice.RepositorySyncTokenLease, error) {
	if s.syncTokenIssuer == nil {
		return reposservice.RepositorySyncTokenLease{}, nil
	}
	repositories := repositoryFullNamesFromMemories(agents)
	if len(repositories) == 0 {
		return reposservice.RepositorySyncTokenLease{}, nil
	}
	request := access.NewSyncLeaseRequest(workspaceID, subject, reposservice.RepositorySyncTokenDefaultProviderHost, "Memory sync lease", repositories, []string{
		reposservice.RepositorySyncCapabilityRead,
		reposservice.RepositorySyncCapabilityClone,
		reposservice.RepositorySyncCapabilityWrite,
	})
	return s.syncTokenIssuer.CreateRepositorySyncToken(ctx, reposservice.RepositorySyncTokenRequest{
		WorkspaceID:      request.WorkspaceID,
		ActorPrincipalID: request.ActorPrincipalID,
		ProviderHost:     request.ProviderHost,
		Repositories:     request.Repositories,
		Capabilities:     request.Capabilities,
		DisplayName:      request.DisplayName,
	})
}

func repositoryFullNamesFromMemories(agents map[string]*AgentAccessibleMemory) []string {
	fullNames := make([]string, 0, len(agents))
	for _, memory := range agents {
		if memory == nil || memory.Repository == nil {
			continue
		}
		fullNames = append(fullNames, memory.Repository.RepoFullName)
	}
	return access.UniqueRepositoryFullNames(fullNames)
}

func (s *MemoryService) repositoriesByMemoryID(ctx context.Context, workspaceID string, memoryIDs []string) (map[string]*resourcerepostore.ResourceRepository, error) {
	out := make(map[string]*resourcerepostore.ResourceRepository)
	if s.repositoryStore == nil || len(memoryIDs) == 0 {
		return out, nil
	}
	uniqueIDs := make([]string, 0, len(memoryIDs))
	seen := make(map[string]struct{}, len(memoryIDs))
	for _, id := range memoryIDs {
		id = strings.TrimSpace(id)
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		uniqueIDs = append(uniqueIDs, id)
	}
	if len(uniqueIDs) == 0 {
		return out, nil
	}
	repos, err := s.repositoryStore.ListForResourceBatch(ctx, strings.TrimSpace(workspaceID), resourcerepostore.ResourceTypeMemory, uniqueIDs)
	if err != nil {
		return nil, err
	}
	for i := range repos {
		if repos[i].RepoRole != resourcerepostore.RepoRolePrimary {
			continue
		}
		repo := repos[i]
		if _, exists := out[repo.ResourceID]; !exists {
			out[repo.ResourceID] = &repo
		}
	}
	return out, nil
}

func agentAccessibleMemoryFromRow(row memorystore.AgentAccessibleMemory, repository *resourcerepostore.ResourceRepository, protocol string) AgentAccessibleMemory {
	result := AgentAccessibleMemory{
		ID:                          row.Memory.ID,
		Name:                        row.Memory.Name,
		Description:                 row.Memory.Description,
		AccessLevel:                 "read",
		PermissionSetAt:             row.Binding.UpdatedAt,
		UpdatedAt:                   row.Memory.UpdatedAt,
		LastSyncAt:                  row.LastSyncAt,
		Repository:                  repository,
		Branch:                      row.Binding.Branch,
		ImportExistingLocalContent:  row.Binding.ImportExistingLocalContent,
		ReplaceLocalContentOnSwitch: row.Binding.ReplaceLocalContentOnSwitch,
		SyncProtocol:                protocol,
	}
	if protocol == MemorySyncProtocolNativeContentV1 {
		result.Repository = nil
		result.Branch = ""
		result.ContentID = row.Memory.ContentID
		result.ContentVersionID = row.Memory.ActiveContentVersionID
		result.ContentGeneration = row.Memory.ContentGeneration
		result.AcknowledgedSequence = row.Memory.AcknowledgedSequence
		result.ActiveReplicaID = row.Memory.ActiveReplicaID
	}
	return result
}

func (s *MemoryService) memorySyncAuthorities(ctx context.Context, workspaceID string, memoryIDs []string) (map[string]content.Authority, error) {
	if s == nil || s.contentAuthority == nil || len(memoryIDs) == 0 {
		return map[string]content.Authority{}, nil
	}
	authorities, err := s.contentAuthority.ListAuthoritiesForBindings(ctx, strings.TrimSpace(workspaceID), memoryContentDomain, memoryIDs)
	if err != nil {
		return nil, fmt.Errorf("resolve memory sync authorities: %w", err)
	}
	return authorities, nil
}

func (s *MemoryService) memorySyncProtocol(memory memorystore.Memory, authority content.Authority) (string, error) {
	state := strings.TrimSpace(authority.State)
	if state == "" {
		state = content.AuthorityStateLegacyActive
	}
	switch state {
	case content.AuthorityStateLegacyActive:
		return MemorySyncProtocolLegacyGitV1, nil
	case content.AuthorityStateNativeActive:
		if !s.nativeContentEnabled || s.nativeContent == nil {
			return "", fmt.Errorf("native memory sync is unavailable")
		}
		if strings.TrimSpace(memory.ContentID) == "" || strings.TrimSpace(memory.ActiveContentVersionID) == "" || memory.ContentGeneration == 0 || memory.ContentHistorySequence == 0 {
			return "", fmt.Errorf("native memory sync projection is incomplete")
		}
		return MemorySyncProtocolNativeContentV1, nil
	default:
		return "", fmt.Errorf("unknown memory content authority state %q", state)
	}
}

func agentCloudMemoryFromAccessible(agentID string, item AgentAccessibleMemory) AgentCloudMemory {
	return AgentCloudMemory{
		AgentID:                    strings.TrimSpace(agentID),
		Enabled:                    true,
		MemoryID:                   item.ID,
		Name:                       item.Name,
		Branch:                     item.Branch,
		Repository:                 item.Repository,
		Status:                     "enabled",
		SyncStatus:                 "automatic",
		LastSyncAt:                 item.LastSyncAt,
		ImportExistingLocalContent: item.ImportExistingLocalContent,
	}
}

func disabledAgentCloudMemory(agentID string) AgentCloudMemory {
	return AgentCloudMemory{
		AgentID:      strings.TrimSpace(agentID),
		Enabled:      false,
		Status:       "disabled",
		SyncStatus:   "local",
		LastSyncAt:   time.Time{},
		LastBackupAt: time.Time{},
	}
}

func unavailableAgentCloudMemory(agentID string) AgentCloudMemory {
	return AgentCloudMemory{
		AgentID:      strings.TrimSpace(agentID),
		Enabled:      false,
		Status:       "unavailable",
		SyncStatus:   "local",
		LastSyncAt:   time.Time{},
		LastBackupAt: time.Time{},
	}
}

func (s *MemoryService) canEnableAgentCloudMemory() bool {
	return s != nil &&
		s.repositoryStore != nil &&
		s.repositories != nil &&
		s.repositoryInitializer != nil
}

func (s *MemoryService) GetAgentCloudMemory(ctx context.Context, workspaceID, subject, agentID string) (AgentCloudMemory, error) {
	item, err := s.GetAgentAccessibleMemory(ctx, workspaceID, subject, agentID)
	if err != nil {
		return AgentCloudMemory{}, err
	}
	if item == nil {
		if !s.canEnableAgentCloudMemory() {
			return unavailableAgentCloudMemory(agentID), nil
		}
		return disabledAgentCloudMemory(agentID), nil
	}
	return agentCloudMemoryFromAccessible(agentID, *item), nil
}

func (s *MemoryService) EnableAgentCloudMemory(ctx context.Context, workspaceID, subject, agentID string, input AgentCloudMemoryEnableMutation) (AgentCloudMemory, error) {
	if s.store == nil {
		return AgentCloudMemory{}, invalidArgument("memory service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return AgentCloudMemory{}, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	subject = strings.TrimSpace(subject)
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return AgentCloudMemory{}, invalidArgument("agent id is required")
	}
	ownedAgent, err := s.ownedAgent(ctx, workspaceID, subject, agentID)
	if err != nil {
		return AgentCloudMemory{}, err
	}
	current, err := s.GetAgentAccessibleMemory(ctx, workspaceID, subject, agentID)
	if err != nil {
		return AgentCloudMemory{}, err
	}
	if current != nil {
		return agentCloudMemoryFromAccessible(agentID, *current), nil
	}

	importExisting := true
	if input.ImportExistingLocalContent != nil {
		importExisting = *input.ImportExistingLocalContent
	}
	displayName := firstNonEmptyString(ownedAgent.DisplayName, ownedAgent.Name, ownedAgent.ID)
	memoryName := strings.TrimSpace(displayName + " memory")
	if memoryName == "" {
		memoryName = "Agent memory"
	}
	repoNameOverride := agentCloudMemoryRepoNameOverride(memoryName, agentID)
	created, err := s.CreateMemory(ctx, workspaceID, subject, MemoryCreateMutation{
		Name:             memoryName,
		RepoNameOverride: repoNameOverride,
		Description:      "",
		StorageScope:     "personal",
		TemplateLocale:   input.TemplateLocale,
		Actor:            input.Actor,
	})
	if err != nil {
		return AgentCloudMemory{}, err
	}
	if _, err := s.BindAgentMemory(ctx, workspaceID, subject, agentID, AgentMemoryBindingMutation{
		MemoryID:                   created.ID,
		Branch:                     "main",
		ImportExistingLocalContent: importExisting,
	}); err != nil {
		return AgentCloudMemory{}, s.cleanupCreatedAgentCloudMemory(ctx, workspaceID, subject, created, err)
	}
	enabled, err := s.GetAgentAccessibleMemory(ctx, workspaceID, subject, agentID)
	if err != nil {
		return AgentCloudMemory{}, s.cleanupCreatedAgentCloudMemory(ctx, workspaceID, subject, created, err)
	}
	if enabled == nil {
		return AgentCloudMemory{}, s.cleanupCreatedAgentCloudMemory(ctx, workspaceID, subject, created, invalidArgument("cloud memory was not enabled"))
	}
	return agentCloudMemoryFromAccessible(agentID, *enabled), nil
}

func (s *MemoryService) cleanupCreatedAgentCloudMemory(ctx context.Context, workspaceID, subject string, created memorystore.Memory, cause error) error {
	if cleanupErr := s.DeleteMemory(ctx, workspaceID, subject, created.ID); cleanupErr != nil {
		return errors.Join(cause, fmt.Errorf("cleanup created cloud memory %s: %w", created.ID, cleanupErr))
	}
	return cause
}

func agentCloudMemoryRepoNameOverride(memoryName, agentID string) string {
	return reposservice.RepositoryNameStem(strings.TrimSpace(memoryName) + "-" + strings.TrimSpace(agentID))
}

func (s *MemoryService) DisableAgentCloudMemory(ctx context.Context, workspaceID, subject, agentID string) (AgentCloudMemory, error) {
	if s.store == nil {
		return AgentCloudMemory{}, invalidArgument("memory service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return AgentCloudMemory{}, err
	}
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return AgentCloudMemory{}, invalidArgument("agent id is required")
	}
	current, err := s.GetAgentAccessibleMemory(ctx, workspaceID, subject, agentID)
	if err != nil {
		return AgentCloudMemory{}, err
	}
	if current == nil {
		return disabledAgentCloudMemory(agentID), nil
	}
	if _, err := s.UnbindAgentMemory(ctx, workspaceID, subject, agentID); err != nil {
		return AgentCloudMemory{}, err
	}
	return disabledAgentCloudMemory(agentID), nil
}

func (s *MemoryService) BindAgentMemory(ctx context.Context, workspaceID, subject, agentID string, input AgentMemoryBindingMutation) (AgentMemoryBindingResult, error) {
	if s.store == nil {
		return AgentMemoryBindingResult{}, invalidArgument("memory service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return AgentMemoryBindingResult{}, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	subject = strings.TrimSpace(subject)
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return AgentMemoryBindingResult{}, invalidArgument("agent id is required")
	}
	if err := s.requireOwnedAgent(ctx, workspaceID, subject, agentID); err != nil {
		return AgentMemoryBindingResult{}, err
	}
	memoryID := strings.TrimSpace(input.MemoryID)
	if memoryID == "" {
		return AgentMemoryBindingResult{}, invalidArgument("memory id is required")
	}
	memory, err := s.GetMemory(ctx, workspaceID, subject, memoryID)
	if err != nil {
		return AgentMemoryBindingResult{}, err
	}
	if !roleCanManage(s.memoryRole(ctx, workspaceID, memory, subject)) {
		return AgentMemoryBindingResult{}, forbidden("memory requires manager access")
	}
	branch := strings.TrimSpace(input.Branch)
	if branch == "" {
		return AgentMemoryBindingResult{}, invalidArgument("memory branch is required")
	}
	current, err := s.store.GetMemoryAgentBinding(ctx, workspaceID, agentID)
	currentFound := true
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			currentFound = false
		} else {
			return AgentMemoryBindingResult{}, err
		}
	}
	now := s.now()
	result := AgentMemoryBindingResult{AgentID: agentID, MemoryID: memoryID, Branch: branch, ImportExistingLocalContent: input.ImportExistingLocalContent, ReplaceLocalContentOnSwitch: input.ReplaceLocalContentOnSwitch}
	if currentFound {
		result.PreviousMemoryID = current.MemoryID
	}
	if currentFound && current.MemoryID == memoryID && current.Branch == branch && current.ImportExistingLocalContent == input.ImportExistingLocalContent && current.ReplaceLocalContentOnSwitch == input.ReplaceLocalContentOnSwitch {
		return result, nil
	}
	repoFullName := s.memoryPrimaryRepoFullName(ctx, workspaceID, memoryID)
	if err := s.store.Transaction(ctx, func(tx *memorystore.Store) error {
		// The memory-agent relationship is 1:1: each agent owns at most one memory
		// through memories.agent_id, and a memory with an owner must be displaced
		// before it can move to another agent.
		// If the target memory is currently bound to a different agent, displace that
		// prior binding first so callers can rebind the memory without an explicit
		// unbind step. The displaced agent is notified via the asset-access outbox so
		// its runtime releases the memory, mirroring UnbindAgentMemory.
		existing, err := tx.ListMemoryAgentBindings(ctx, workspaceID, memoryID)
		if err != nil {
			return err
		}
		for _, binding := range existing {
			if binding.AgentID == agentID {
				continue
			}
			if err := tx.DeleteMemoryAgentBinding(ctx, workspaceID, binding.AgentID); err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
				return err
			}
			if _, err := access.EnqueueAccessChange(ctx, tx, memorystore.AgentAssetAccessChangeInput{
				WorkspaceID: workspaceID,
				AgentID:     binding.AgentID,
				AssetType:   access.AssetTypeMemory,
				OccurredAt:  now,
			}); err != nil {
				return err
			}
			if err := writeMemoryActivity(ctx, s.memoryActivityStore(tx), workspaceID, memory, repoFullName, memoryActivityUnbound, subject, "binding", binding.AgentID, map[string]any{
				"agentId":                     binding.AgentID,
				"agentName":                   binding.AgentID,
				"branch":                      binding.Branch,
				"importExistingLocalContent":  binding.ImportExistingLocalContent,
				"replaceLocalContentOnSwitch": binding.ReplaceLocalContentOnSwitch,
				"displacedBy":                 agentID,
			}, now); err != nil {
				return err
			}
		}
		if _, err := tx.UpsertMemoryAgentBinding(ctx, memorystore.MemoryAgentBinding{ID: newMemoryID(), WorkspaceID: workspaceID, MemoryID: memoryID, AgentID: agentID, Branch: branch, ImportExistingLocalContent: input.ImportExistingLocalContent, ReplaceLocalContentOnSwitch: input.ReplaceLocalContentOnSwitch, CreatedBy: subject, UpdatedBy: subject, CreatedAt: now, UpdatedAt: now}); err != nil {
			if errors.Is(err, memorystore.ErrMemoryAgentBindingConflict) {
				return conflict("memory is already bound to another agent")
			}
			return err
		}
		if _, err := access.EnqueueAccessChange(ctx, tx, memorystore.AgentAssetAccessChangeInput{
			WorkspaceID: workspaceID,
			AgentID:     agentID,
			AssetType:   access.AssetTypeMemory,
			OccurredAt:  now,
		}); err != nil {
			return err
		}
		return writeMemoryActivity(ctx, s.memoryActivityStore(tx), workspaceID, memory, repoFullName, memoryActivityBound, subject, "binding", agentID, map[string]any{
			"agentId":                     agentID,
			"agentName":                   agentID,
			"branch":                      branch,
			"previousMemoryId":            result.PreviousMemoryID,
			"importExistingLocalContent":  input.ImportExistingLocalContent,
			"replaceLocalContentOnSwitch": input.ReplaceLocalContentOnSwitch,
		}, now)
	}); err != nil {
		return AgentMemoryBindingResult{}, err
	}
	return result, nil
}

func (s *MemoryService) UnbindAgentMemory(ctx context.Context, workspaceID, subject, agentID string) (AgentMemoryBindingResult, error) {
	if s.store == nil {
		return AgentMemoryBindingResult{}, invalidArgument("memory service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return AgentMemoryBindingResult{}, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	subject = strings.TrimSpace(subject)
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return AgentMemoryBindingResult{}, invalidArgument("agent id is required")
	}
	if err := s.requireOwnedAgent(ctx, workspaceID, subject, agentID); err != nil {
		return AgentMemoryBindingResult{}, err
	}
	current, err := s.store.GetMemoryAgentBinding(ctx, workspaceID, agentID)
	if err != nil {
		return AgentMemoryBindingResult{}, mapStoreNotFound(err)
	}
	memory, err := s.GetMemory(ctx, workspaceID, subject, current.MemoryID)
	if err != nil {
		return AgentMemoryBindingResult{}, err
	}
	if !roleCanManage(s.memoryRole(ctx, workspaceID, memory, subject)) {
		return AgentMemoryBindingResult{}, forbidden("memory requires manager access")
	}
	now := s.now()
	repoFullName := s.memoryPrimaryRepoFullName(ctx, workspaceID, current.MemoryID)
	if err := s.store.Transaction(ctx, func(tx *memorystore.Store) error {
		if err := tx.DeleteMemoryAgentBinding(ctx, workspaceID, agentID); err != nil {
			return mapStoreNotFound(err)
		}
		if _, err := access.EnqueueAccessChange(ctx, tx, memorystore.AgentAssetAccessChangeInput{
			WorkspaceID: workspaceID,
			AgentID:     agentID,
			AssetType:   access.AssetTypeMemory,
			OccurredAt:  now,
		}); err != nil {
			return err
		}
		return writeMemoryActivity(ctx, s.memoryActivityStore(tx), workspaceID, memory, repoFullName, memoryActivityUnbound, subject, "binding", agentID, map[string]any{
			"agentId":                     agentID,
			"agentName":                   agentID,
			"branch":                      current.Branch,
			"importExistingLocalContent":  current.ImportExistingLocalContent,
			"replaceLocalContentOnSwitch": current.ReplaceLocalContentOnSwitch,
		}, now)
	}); err != nil {
		return AgentMemoryBindingResult{}, err
	}
	result := AgentMemoryBindingResult{AgentID: agentID, MemoryID: current.MemoryID, PreviousMemoryID: current.MemoryID, Branch: current.Branch, ImportExistingLocalContent: current.ImportExistingLocalContent, ReplaceLocalContentOnSwitch: current.ReplaceLocalContentOnSwitch, Unbound: true}
	return result, nil
}

func (s *MemoryService) ListAgentMemoryBindings(ctx context.Context, workspaceID, subject string) (AgentMemoryBindingsSummary, error) {
	if s.store == nil {
		return AgentMemoryBindingsSummary{}, invalidArgument("memory service is not configured")
	}
	if err := validateSubject(subject); err != nil {
		return AgentMemoryBindingsSummary{}, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	subject = strings.TrimSpace(subject)
	ownedAgents, err := s.listOwnedAgents(ctx, workspaceID, subject)
	if err != nil {
		return AgentMemoryBindingsSummary{}, err
	}
	ownedAgentNames := make(map[string]string, len(ownedAgents))
	ownedAgentIDs := make([]string, 0, len(ownedAgents))
	for _, agent := range ownedAgents {
		ownedAgentNames[agent.ID] = firstNonEmptyString(agent.DisplayName, agent.Name, agent.ID)
		ownedAgentIDs = append(ownedAgentIDs, agent.ID)
	}
	accessibleByAgent, err := s.store.ListAgentAccessibleMemories(ctx, workspaceID, ownedAgentIDs, true)
	if err != nil {
		return AgentMemoryBindingsSummary{}, err
	}
	bindings := make([]memorystore.MemoryAgentBinding, 0, len(accessibleByAgent))
	for _, agent := range ownedAgents {
		if accessible, ok := accessibleByAgent[agent.ID]; ok {
			bindings = append(bindings, accessible.Binding)
		}
	}
	bindingsByMemory := make(map[string]memorystore.MemoryAgentBinding, len(bindings))
	for _, binding := range bindings {
		bindingsByMemory[binding.MemoryID] = binding
	}
	page, err := s.ListMemories(ctx, workspaceID, subject, "all", "", 1, 10000)
	if err != nil {
		return AgentMemoryBindingsSummary{}, err
	}
	memoryIDs := make([]string, 0, len(page.Items))
	for _, memory := range page.Items {
		memoryIDs = append(memoryIDs, memory.ID)
	}
	repositories, err := s.repositoriesByMemoryID(ctx, workspaceID, memoryIDs)
	if err != nil {
		return AgentMemoryBindingsSummary{}, err
	}
	out := AgentMemoryBindingsSummary{
		OwnedAgents: ownedAgents,
		Bindings:    make([]AgentMemoryBindingSummary, 0, len(bindings)),
		Memories:    make([]AgentMemoryBindingMemory, 0, len(page.Items)),
	}
	for _, binding := range bindings {
		if _, owned := ownedAgentNames[binding.AgentID]; !owned {
			continue
		}
		out.Bindings = append(out.Bindings, agentMemoryBindingSummaryFromStore(binding))
	}
	for _, memory := range page.Items {
		row := AgentMemoryBindingMemory{
			ID:          memory.ID,
			Slug:        memory.Slug,
			Name:        memory.Name,
			Description: memory.Description,
			IsActive:    memory.IsActive,
			Role:        memory.Role,
			CanManage:   memory.CanManage,
			CanBind:     roleCanManage(memory.Role),
			Repository:  repositories[memory.ID],
			UpdatedAt:   memory.UpdatedAt,
		}
		if binding, ok := bindingsByMemory[memory.ID]; ok {
			row.BoundAgentID = binding.AgentID
			row.BoundAgentName = firstNonEmptyString(ownedAgentNames[binding.AgentID], binding.AgentID)
			row.BoundBranch = binding.Branch
			row.ImportExistingLocalContent = binding.ImportExistingLocalContent
			row.ReplaceLocalContentOnSwitch = binding.ReplaceLocalContentOnSwitch
			if binding.AgentID != "" {
				row.CanBind = false
			}
		}
		out.Memories = append(out.Memories, row)
	}
	return out, nil
}

func (s *MemoryService) ListMemoryPermissions(ctx context.Context, workspaceID, subject, memoryID string) (MemoryPermissionsSummary, error) {
	m, err := s.GetMemory(ctx, workspaceID, subject, memoryID)
	if err != nil {
		return MemoryPermissionsSummary{}, err
	}
	myRole := s.memoryRole(ctx, workspaceID, m, subject)
	canGrant := roleCanManage(myRole)
	if !canGrant {
		return MemoryPermissionsSummary{}, forbidden("memory requires manage access")
	}
	grants, err := s.store.ListMemoryGrants(ctx, workspaceID, memoryID)
	if err != nil {
		return MemoryPermissionsSummary{}, err
	}
	out := MemoryPermissionsSummary{MyRole: myRole, CanManage: canGrant, Members: []MemoryPermissionGrant{}}
	for _, grant := range grants {
		if grant.SubjectType != "user" {
			continue
		}
		converted := memoryServiceGrantFromStore(grant)
		if grant.Role == "owner" && out.Owner == nil {
			owner := converted
			out.Owner = &owner
		}
		out.Members = append(out.Members, converted)
	}
	return out, nil
}

func (s *MemoryService) UpsertMemoryPermission(ctx context.Context, workspaceID, subject, memoryID string, input MemoryPermissionGrantMutation) (MemoryPermissionGrant, error) {
	m, err := s.GetMemory(ctx, workspaceID, subject, memoryID)
	if err != nil {
		return MemoryPermissionGrant{}, err
	}
	if !roleCanManage(s.memoryRole(ctx, workspaceID, m, subject)) {
		return MemoryPermissionGrant{}, forbidden("memory requires manage access")
	}
	subjectType := normalizeMemoryGrantSubjectType(input.SubjectType)
	subjectID := strings.TrimSpace(input.SubjectID)
	grantID := strings.TrimSpace(input.GrantID)
	if grantID != "" {
		existing, err := s.store.GetMemoryGrantByID(ctx, workspaceID, memoryID, grantID)
		if err != nil {
			return MemoryPermissionGrant{}, mapStoreNotFound(err)
		}
		subjectType = existing.SubjectType
		subjectID = existing.SubjectID
	}
	role := normalizeMemoryGrantRole(input.Role, subjectType)
	if subjectType == "" {
		return MemoryPermissionGrant{}, invalidArgument("permission subject type must be user")
	}
	if subjectType == "agent" {
		return MemoryPermissionGrant{}, invalidArgument("agent memory access must use agent memory bindings")
	}
	if subjectID == "" {
		return MemoryPermissionGrant{}, invalidArgument("permission subject id is required")
	}
	now := s.now()
	var existing memorystore.MemoryGrant
	existingFound := false
	if grantID != "" {
		existing, err = s.store.GetMemoryGrantByID(ctx, workspaceID, memoryID, grantID)
		if err != nil {
			return MemoryPermissionGrant{}, mapStoreNotFound(err)
		}
		existingFound = true
	} else {
		existing, err = s.store.GetMemoryGrant(ctx, workspaceID, memoryID, subjectType, subjectID)
		if err == nil {
			existingFound = true
		} else if !errors.Is(err, gorm.ErrRecordNotFound) {
			return MemoryPermissionGrant{}, err
		}
	}
	eventType := memoryActivityGrantAdded
	previousRole := ""
	if existingFound {
		previousRole = existing.Role
		if existing.Role == role {
			eventType = ""
		} else {
			eventType = memoryActivityGrantRoleChanged
		}
	}
	repoFullName := s.memoryPrimaryRepoFullName(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(memoryID))
	var grant memorystore.MemoryGrant
	err = s.store.Transaction(ctx, func(tx *memorystore.Store) error {
		var err error
		grant, err = tx.UpsertMemoryGrant(ctx, memorystore.MemoryGrant{ID: newMemoryID(), WorkspaceID: strings.TrimSpace(workspaceID), MemoryID: strings.TrimSpace(memoryID), SubjectType: subjectType, SubjectID: subjectID, Role: role, CreatedBy: strings.TrimSpace(subject), UpdatedBy: strings.TrimSpace(subject), CreatedAt: now, UpdatedAt: now})
		if err != nil {
			return err
		}
		if eventType == "" {
			return nil
		}
		return writeMemoryActivity(ctx, s.memoryActivityStore(tx), workspaceID, m, repoFullName, eventType, strings.TrimSpace(subject), "grant", grant.ID, map[string]any{
			"grantId":      grant.ID,
			"subjectType":  grant.SubjectType,
			"subjectId":    grant.SubjectID,
			"subjectName":  grant.SubjectID,
			"role":         grant.Role,
			"previousRole": previousRole,
		}, now)
	})
	if err != nil {
		return MemoryPermissionGrant{}, err
	}
	return memoryServiceGrantFromStore(grant), nil
}

func (s *MemoryService) DeleteMemoryPermission(ctx context.Context, workspaceID, subject, memoryID, grantID string) error {
	m, err := s.GetMemory(ctx, workspaceID, subject, memoryID)
	if err != nil {
		return err
	}
	if !roleCanManage(s.memoryRole(ctx, workspaceID, m, subject)) {
		return forbidden("memory requires manage access")
	}
	grantID = strings.TrimSpace(grantID)
	if grantID == "" {
		return invalidArgument("permission grant id is required")
	}
	grant, err := s.store.GetMemoryGrantByID(ctx, workspaceID, memoryID, grantID)
	if err != nil {
		return mapStoreNotFound(err)
	}
	if grant.SubjectType == "user" && grant.Role == "owner" {
		return invalidArgument("memory owner grant cannot be removed")
	}
	if grant.SubjectType == "agent" {
		return invalidArgument("agent memory access must use agent memory bindings")
	}
	now := s.now()
	repoFullName := s.memoryPrimaryRepoFullName(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(memoryID))
	return mapStoreNotFound(s.store.Transaction(ctx, func(tx *memorystore.Store) error {
		if err := tx.DeleteMemoryGrantByID(ctx, workspaceID, memoryID, grantID); err != nil {
			return err
		}
		return writeMemoryActivity(ctx, s.memoryActivityStore(tx), workspaceID, m, repoFullName, memoryActivityGrantRemoved, strings.TrimSpace(subject), "grant", grant.ID, map[string]any{
			"grantId":     grant.ID,
			"subjectType": grant.SubjectType,
			"subjectId":   grant.SubjectID,
			"subjectName": grant.SubjectID,
			"role":        grant.Role,
		}, now)
	}))
}

func (s *MemoryService) listOwnedAgents(ctx context.Context, workspaceID, subject string) ([]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
	}
	out := make([]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, nil
}

func (s *MemoryService) requireOwnedAgent(ctx context.Context, workspaceID, subject, agentID string) error {
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return invalidArgument("agent id is required")
	}
	_, err := s.ownedAgent(ctx, workspaceID, subject, agentID)
	return err
}

func (s *MemoryService) ownedAgent(ctx context.Context, workspaceID, subject, agentID string) (AgentOwnership, error) {
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return AgentOwnership{}, invalidArgument("agent id is required")
	}
	agents, err := s.listOwnedAgents(ctx, workspaceID, subject)
	if err != nil {
		return AgentOwnership{}, err
	}
	for _, agent := range agents {
		if agent.ID == agentID {
			return agent, nil
		}
	}
	return AgentOwnership{}, forbidden("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. The batch agent-access read endpoints use this to prevent a
// caller with valid auth from probing other users' agent bindings.
func (s *MemoryService) 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 forbidden("agent must be owned by the current user")
		}
	}
	return nil
}

func agentMemoryBindingSummaryFromStore(binding memorystore.MemoryAgentBinding) AgentMemoryBindingSummary {
	return AgentMemoryBindingSummary{
		ID:                          binding.ID,
		AgentID:                     binding.AgentID,
		MemoryID:                    binding.MemoryID,
		Branch:                      binding.Branch,
		ImportExistingLocalContent:  binding.ImportExistingLocalContent,
		ReplaceLocalContentOnSwitch: binding.ReplaceLocalContentOnSwitch,
		CreatedBy:                   binding.CreatedBy,
		UpdatedBy:                   binding.UpdatedBy,
		CreatedAt:                   binding.CreatedAt,
		UpdatedAt:                   binding.UpdatedAt,
	}
}

func memoryServiceGrantFromStore(grant memorystore.MemoryGrant) MemoryPermissionGrant {
	return MemoryPermissionGrant{ID: grant.ID, MemoryID: grant.MemoryID, SubjectType: grant.SubjectType, SubjectID: grant.SubjectID, Role: normalizeMemoryGrantRole(grant.Role, grant.SubjectType), CreatedBy: grant.CreatedBy, UpdatedBy: grant.UpdatedBy, CreatedAt: grant.CreatedAt, UpdatedAt: grant.UpdatedAt}
}

func normalizeMemoryGrantSubjectType(subjectType string) string {
	switch strings.TrimSpace(subjectType) {
	case "user":
		return strings.TrimSpace(subjectType)
	default:
		return ""
	}
}

func normalizeMemoryGrantRole(role, subjectType string) string {
	role = strings.TrimSpace(role)
	switch role {
	case "owner", "manage", "view":
		return role
	case "edit":
		return "view"
	default:
		return "view"
	}
}

func (s *MemoryService) canViewMemory(ctx context.Context, workspaceID string, m memorystore.Memory, subject string) bool {
	subject = strings.TrimSpace(subject)
	if subject == "" {
		return false
	}
	return s.memoryRole(ctx, workspaceID, m, subject) != ""
}

func (s *MemoryService) canManageMemory(ctx context.Context, workspaceID string, m memorystore.Memory, subject string) bool {
	subject = strings.TrimSpace(subject)
	if subject == "" {
		return false
	}
	return roleCanManage(s.memoryRole(ctx, workspaceID, m, subject))
}

func (s *MemoryService) memoryRole(ctx context.Context, workspaceID string, m memorystore.Memory, subject string) string {
	subject = strings.TrimSpace(subject)
	if subject == "" {
		return ""
	}
	if strings.TrimSpace(m.CreatedBy) == subject {
		return "owner"
	}
	grant, err := s.store.GetMemoryGrant(ctx, strings.TrimSpace(workspaceID), m.ID, "user", subject)
	if err != nil {
		return ""
	}
	return normalizeMemoryGrantRole(grant.Role, grant.SubjectType)
}
