package memory

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"strings"
	"time"

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

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	"github.com/tidbcloud/AgentTeam/internal/api/meshxapi"
	memoryservice "github.com/tidbcloud/AgentTeam/internal/service/memory"
	memorystore "github.com/tidbcloud/AgentTeam/internal/store/resource/memory"
)

const maxPerPage = 100

// batchAgentIDsLimit bounds the number of agent IDs a single batch request can
// carry. It exists to keep the request URL/body and per-agent SQL fan-out
// bounded when a workspace hosts many agents. Desktop refreshes agent lists
// frequently, so the limit only needs to cover realistic agent counts per
// workspace.
const batchAgentIDsLimit = 200

type Service interface {
	CreateMemory(ctx context.Context, workspaceID, subject string, input memoryservice.MemoryCreateMutation) (memorystore.Memory, error)
	ForkMemory(ctx context.Context, workspaceID, subject, id string, input memoryservice.MemoryForkMutation) (memorystore.Memory, error)
	GetMemory(ctx context.Context, workspaceID, subject, id string) (memorystore.Memory, error)
	GetMemoryRepositories(ctx context.Context, workspaceID, subject, id string) (memoryservice.MemoryRepositories, error)
	ListMemories(ctx context.Context, workspaceID, subject, view, query string, page, perPage int) (memoryservice.MemoryCollectionPage, error)
	CountWorkspaceMemories(ctx context.Context, workspaceID string, subject string) (int64, error)
	ListMemoryActivity(ctx context.Context, workspaceID, subject string, opts activity.ResourceActivityListOptions) (activity.ResourceActivityPage, error)
	GetAgentAccessibleMemory(ctx context.Context, workspaceID, subject, agentID string) (*memoryservice.AgentAccessibleMemory, error)
	BatchListAgentAccessibleMemories(ctx context.Context, workspaceID, subject string, agentIDs []string) (memoryservice.AgentMemoryAccessBatch, error)
	GetAgentCloudMemory(ctx context.Context, workspaceID, subject, agentID string) (memoryservice.AgentCloudMemory, error)
	EnableAgentCloudMemory(ctx context.Context, workspaceID, subject, agentID string, input memoryservice.AgentCloudMemoryEnableMutation) (memoryservice.AgentCloudMemory, error)
	DisableAgentCloudMemory(ctx context.Context, workspaceID, subject, agentID string) (memoryservice.AgentCloudMemory, error)
	ListAgentCloudMemoryCommits(ctx context.Context, workspaceID, subject, agentID string, opts memoryservice.AgentCloudMemoryCommitListOptions) (json.RawMessage, error)
	GetAgentCloudMemoryCompare(ctx context.Context, workspaceID, subject, agentID string, opts memoryservice.AgentCloudMemoryCompareOptions) (json.RawMessage, error)
	ListAgentCloudMemoryBackups(ctx context.Context, workspaceID, subject, agentID, query string, page, perPage int) (memoryservice.AgentCloudMemoryBackupPage, error)
	CreateAgentCloudMemoryBackup(ctx context.Context, workspaceID, subject, agentID string, input memoryservice.AgentCloudMemoryBackupCreateMutation) (memoryservice.AgentCloudMemoryBackup, error)
	RestoreAgentCloudMemoryBackup(ctx context.Context, workspaceID, subject, agentID, tagName string) (memoryservice.AgentCloudMemoryBackup, error)
	BindAgentMemory(ctx context.Context, workspaceID, subject, agentID string, input memoryservice.AgentMemoryBindingMutation) (memoryservice.AgentMemoryBindingResult, error)
	UnbindAgentMemory(ctx context.Context, workspaceID, subject, agentID string) (memoryservice.AgentMemoryBindingResult, error)
	ListAgentMemoryBindings(ctx context.Context, workspaceID, subject string) (memoryservice.AgentMemoryBindingsSummary, error)
	LeaseAgentAssetAccessEvents(ctx context.Context, opts memoryservice.AgentAssetAccessEventLeaseOptions) ([]memoryservice.AgentAssetAccessEvent, error)
	MarkAgentAssetAccessEventDelivered(ctx context.Context, eventID, leaseOwner string, deliveredAt time.Time) (bool, error)
	MarkAgentAssetAccessEventFailed(ctx context.Context, failure memoryservice.AgentAssetAccessEventFailure) (memorystore.AgentAssetAccessAttemptResult, error)
	UpdateMemory(ctx context.Context, workspaceID, subject, id string, input memoryservice.MemoryUpdateMutation) (memorystore.Memory, error)
	DeleteMemory(ctx context.Context, workspaceID, subject, id string) error
	PinMemory(ctx context.Context, workspaceID, subject, id string) error
	UnpinMemory(ctx context.Context, workspaceID, subject, id string) error
	ListMemoryPermissions(ctx context.Context, workspaceID, subject, memoryID string) (memoryservice.MemoryPermissionsSummary, error)
	UpsertMemoryPermission(ctx context.Context, workspaceID, subject, memoryID string, input memoryservice.MemoryPermissionGrantMutation) (memoryservice.MemoryPermissionGrant, error)
	DeleteMemoryPermission(ctx context.Context, workspaceID, subject, memoryID, grantID string) error
}

type MemoryHandler struct{ service Service }

type RouteMiddleware func(http.Handler) http.Handler

type routeRegistrar interface {
	Handle(pattern string, handler http.Handler)
}

type DisabledHandler struct{}

func (DisabledHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
	meshxapi.WriteError(w, meshxapi.NewHTTPError(http.StatusServiceUnavailable, "Memory API is not configured"))
}

func NewMemoryHandler(service Service) *MemoryHandler { return &MemoryHandler{service: service} }

func RegisterRoutes(router routeRegistrar, handler *MemoryHandler, middleware RouteMiddleware) {
	if router == nil {
		return
	}
	if handler == nil {
		disabled := DisabledHandler{}
		registerMemoryRoute(router, "/api/v1/memory", disabled, middleware)
		registerMemoryRoute(router, "/api/v1/memory/", disabled, middleware)
		return
	}
	contentHandler := newContentHandler(handler.service)
	registerMemoryRoute(router, "POST /api/v1/memory", meshxapi.HandleAuthJSON[dto.MemoryCreateRequest](handler.Create, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory", meshxapi.HandleAuth(handler.List, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/internal/memory/count", meshxapi.HandleAuth(handler.CountWorkspace, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/activity", meshxapi.HandleAuth(handler.ListActivity, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/agents/{agent_id}/accessible", meshxapi.HandleAuth(handler.ListAgentAccessible, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/memory/agents/access:batch", meshxapi.HandleAuthJSON[dto.AgentAccessBatchRequest](handler.AgentAccessBatch, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/agents/bindings", meshxapi.HandleAuth(handler.ListAgentMemoryBindings, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "PUT /api/v1/memory/agents/{agent_id}/binding", meshxapi.HandleAuthJSON[dto.AgentMemoryBindingRequest](handler.BindAgentMemory, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "DELETE /api/v1/memory/agents/{agent_id}/binding", meshxapi.HandleAuth(handler.UnbindAgentMemory, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/agents/{agent_id}/cloud-memory", meshxapi.HandleAuth(handler.GetAgentCloudMemory, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/memory/agents/{agent_id}/cloud-memory", meshxapi.HandleAuthJSON[dto.AgentCloudMemoryEnableRequest](handler.EnableAgentCloudMemory, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "DELETE /api/v1/memory/agents/{agent_id}/cloud-memory", meshxapi.HandleAuth(handler.DisableAgentCloudMemory, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/agents/{agent_id}/cloud-memory/commits", meshxapi.HandleAuth(handler.ListAgentCloudMemoryCommits, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/agents/{agent_id}/cloud-memory/compare", meshxapi.HandleAuth(handler.GetAgentCloudMemoryCompare, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/agents/{agent_id}/cloud-memory/backups", meshxapi.HandleAuth(handler.ListAgentCloudMemoryBackups, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/memory/agents/{agent_id}/cloud-memory/backups", meshxapi.HandleAuthJSON[dto.AgentCloudMemoryBackupCreateRequest](handler.CreateAgentCloudMemoryBackup, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/memory/agents/{agent_id}/cloud-memory/backups:restore", meshxapi.HandleAuthJSON[dto.AgentCloudMemoryBackupRestoreRequest](handler.RestoreAgentCloudMemoryBackup, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/internal/agent-assets/access-events:lease", meshxapi.HandleAuthJSON[dto.AgentAssetAccessEventLeaseRequest](handler.LeaseAgentAssetAccessEvents, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/internal/agent-assets/access-events/{event_id}/delivered", meshxapi.HandleAuthJSON[dto.AgentAssetAccessEventDeliveredRequest](handler.MarkAgentAssetAccessEventDelivered, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/internal/agent-assets/access-events/{event_id}/failed", meshxapi.HandleAuthJSON[dto.AgentAssetAccessEventFailedRequest](handler.MarkAgentAssetAccessEventFailed, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/{memory_id}", meshxapi.HandleAuth(handler.Get, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/memory/{memory_id}/checkpoints", meshxapi.HandleAuthJSONLimit[dto.MemoryCheckpointPublishRequest](maxMemoryCheckpointRequestBytes, contentHandler.PublishCheckpoint, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/{memory_id}/versions", meshxapi.HandleAuth(contentHandler.ListContentVersions, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/{memory_id}/versions:compare", meshxapi.HandleAuth(contentHandler.CompareContentVersions, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/{memory_id}/versions/{version_id}/archive", http.HandlerFunc(contentHandler.OpenContentArchive), middleware)
	registerMemoryRoute(router, "POST /api/v1/memory/{memory_id}/versions:restore", meshxapi.HandleAuthJSON[dto.MemoryContentRestoreRequest](contentHandler.RestoreContentVersion, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/{memory_id}/backups", meshxapi.HandleAuth(contentHandler.ListContentBackups, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/memory/{memory_id}/backups", meshxapi.HandleAuthJSON[dto.MemoryContentBackupCreateRequest](contentHandler.CreateContentBackup, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/memory/{memory_id}/fork", meshxapi.HandleAuthJSON[dto.MemoryForkRequest](handler.Fork, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "PATCH /api/v1/memory/{memory_id}", meshxapi.HandleAuthJSON[dto.MemoryUpdateRequest](handler.Update, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "DELETE /api/v1/memory/{memory_id}/permanent", meshxapi.HandleAuth(handler.Delete, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "PUT /api/v1/memory/{memory_id}/collection-pin", meshxapi.HandleAuth(handler.Pin, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "DELETE /api/v1/memory/{memory_id}/collection-pin", meshxapi.HandleAuth(handler.Unpin, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "GET /api/v1/memory/{memory_id}/permissions", meshxapi.HandleAuth(handler.ListPermissions, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "POST /api/v1/memory/{memory_id}/permissions/grants", meshxapi.HandleAuthJSON[dto.MemoryPermissionGrantRequest](handler.UpsertPermission, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "PATCH /api/v1/memory/{memory_id}/permissions/grants/{grant_id}", meshxapi.HandleAuthJSON[dto.MemoryPermissionGrantRequest](handler.UpsertPermission, memoryErrorHandling()), middleware)
	registerMemoryRoute(router, "DELETE /api/v1/memory/{memory_id}/permissions/grants/{grant_id}", meshxapi.HandleAuth(handler.DeletePermission, memoryErrorHandling()), middleware)
}

func registerMemoryRoute(router routeRegistrar, pattern string, handler http.Handler, middleware RouteMiddleware) {
	if middleware != nil {
		handler = middleware(handler)
	}
	router.Handle(pattern, handler)
}

func (h *MemoryHandler) ensureService() error {
	if h == nil || h.service == nil {
		return meshxapi.NewHTTPError(http.StatusServiceUnavailable, "Memory API is not configured")
	}
	return nil
}

func (h *MemoryHandler) Create(r meshxapi.AuthenticatedJSON[dto.MemoryCreateRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	input := r.Body.Input()
	input.Actor = repos.ResourceRepositoryActorFromAgentTeam(r.Identity)
	m, err := h.service.CreateMemory(r.Context(), r.WorkspaceID(), r.Subject(), input)
	if err != nil {
		return meshxapi.Response{}, err
	}
	repositories, err := h.service.GetMemoryRepositories(r.Context(), r.WorkspaceID(), r.Subject(), m.ID)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.Created(converter.ToMemoryWithRepositories(m, repositories)), nil
}

func (h *MemoryHandler) Fork(r meshxapi.AuthenticatedJSON[dto.MemoryForkRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	input := r.Body.Input()
	input.Actor = repos.ResourceRepositoryActorFromAgentTeam(r.Identity)
	m, err := h.service.ForkMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("memory_id"), input)
	if err != nil {
		return meshxapi.Response{}, err
	}
	repositories, err := h.service.GetMemoryRepositories(r.Context(), r.WorkspaceID(), r.Subject(), m.ID)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.Created(converter.ToMemoryWithRepositories(m, repositories)), nil
}

func (h *MemoryHandler) List(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	page, perPage := pagination(r.Request)
	result, err := h.service.ListMemories(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.URL.Query().Get("view"), r.Request.URL.Query().Get("q"), page, perPage)
	if err != nil {
		return meshxapi.Response{}, err
	}
	repositories, err := h.memoryRepositoriesForItems(r.Context(), r.WorkspaceID(), r.Subject(), result.Items)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToMemoryListResponseWithRepositories(result.Items, repositories, result.Page, result.PerPage, result.Total, result.HasMore)), nil
}

func (h *MemoryHandler) CountWorkspace(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	total, err := h.service.CountWorkspaceMemories(r.Context(), r.WorkspaceID(), r.Subject())
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(map[string]int64{"total": total}), nil
}

func (h *MemoryHandler) ListActivity(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	opts, err := activityListOptions(r.Request)
	if err != nil {
		return meshxapi.Response{}, err
	}
	page, err := h.service.ListMemoryActivity(r.Context(), r.WorkspaceID(), r.Subject(), opts)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToResourceActivityListResponse(page)), nil
}

func (h *MemoryHandler) ListAgentAccessible(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	item, err := h.service.GetAgentAccessibleMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	if item == nil {
		return meshxapi.OK((*dto.AgentAccessibleMemory)(nil)), nil
	}
	converted := converter.ToAgentAccessibleMemory(*item)
	return meshxapi.OK(converted), nil
}

func (h *MemoryHandler) GetAgentCloudMemory(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	item, err := h.service.GetAgentCloudMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentCloudMemory(item)), nil
}

func (h *MemoryHandler) EnableAgentCloudMemory(r meshxapi.AuthenticatedJSON[dto.AgentCloudMemoryEnableRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	input := r.Body.Input()
	input.Actor = repos.ResourceRepositoryActorFromAgentTeam(r.Identity)
	item, err := h.service.EnableAgentCloudMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"), input)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentCloudMemory(item)), nil
}

func (h *MemoryHandler) DisableAgentCloudMemory(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	item, err := h.service.DisableAgentCloudMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentCloudMemory(item)), nil
}

// ListAgentCloudMemoryCommits handles GET /api/v1/memory/agents/{agent_id}/cloud-memory/commits.
// It returns the authorized cloud-memory repository commit history for the agent.
func (h *MemoryHandler) ListAgentCloudMemoryCommits(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	page, perPage := pagination(r.Request)
	query := r.Request.URL.Query()
	payload, err := h.service.ListAgentCloudMemoryCommits(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"), memoryservice.AgentCloudMemoryCommitListOptions{
		Ref:     query.Get("sha"),
		Path:    query.Get("path"),
		Author:  query.Get("author"),
		Since:   query.Get("since"),
		Until:   query.Get("until"),
		Page:    page,
		PerPage: perPage,
		Actor:   repos.ResourceRepositoryActorFromAgentTeam(r.Identity),
	})
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(payload), nil
}

// GetAgentCloudMemoryCompare handles GET /api/v1/memory/agents/{agent_id}/cloud-memory/compare.
// It returns a compare payload for two refs in the agent's cloud-memory repository.
func (h *MemoryHandler) GetAgentCloudMemoryCompare(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	query := r.Request.URL.Query()
	payload, err := h.service.GetAgentCloudMemoryCompare(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"), memoryservice.AgentCloudMemoryCompareOptions{
		Base:  query.Get("base"),
		Head:  query.Get("head"),
		Actor: repos.ResourceRepositoryActorFromAgentTeam(r.Identity),
	})
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(payload), nil
}

func (h *MemoryHandler) ListAgentCloudMemoryBackups(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	page, perPage := pagination(r.Request)
	result, err := h.service.ListAgentCloudMemoryBackups(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"), r.Request.URL.Query().Get("q"), page, perPage)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentCloudMemoryBackupListResponse(result)), nil
}

func (h *MemoryHandler) CreateAgentCloudMemoryBackup(r meshxapi.AuthenticatedJSON[dto.AgentCloudMemoryBackupCreateRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	input := r.Body.Input()
	input.Actor = repos.ResourceRepositoryActorFromAgentTeam(r.Identity)
	item, err := h.service.CreateAgentCloudMemoryBackup(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"), input)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.Created(converter.ToAgentCloudMemoryBackup(item)), nil
}

func (h *MemoryHandler) RestoreAgentCloudMemoryBackup(r meshxapi.AuthenticatedJSON[dto.AgentCloudMemoryBackupRestoreRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	item, err := h.service.RestoreAgentCloudMemoryBackup(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"), r.Body.TagName)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentCloudMemoryBackup(item)), nil
}

// AgentAccessBatch is the pull endpoint Desktop clients call after refreshing
// their agent catalog. Desktop sends the server-known agent IDs it currently
// hosts; MeshX responds with each agent's bound memory plus the
// canonical repository refs needed to clone them.
//
// Auth model: the caller is an authenticated AgentTeam workspace member. MeshX
// trusts AgentTeam's identity and returns bindings scoped to the caller's workspace.
// The response includes a short-lived syncToken whenever bound repositories are
// present. Desktop uses the AgentTeam user token only for this management-plane API
// call; Git clone/fetch/push uses the AGS repository sync lease.
func (h *MemoryHandler) AgentAccessBatch(r meshxapi.AuthenticatedJSON[dto.AgentAccessBatchRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	if len(r.Body.AgentIDs) == 0 {
		return meshxapi.OK(dto.AgentAccessBatchResponse{Agents: map[string]*dto.AgentAccessibleMemory{}}), nil
	}
	if len(r.Body.AgentIDs) > batchAgentIDsLimit {
		return meshxapi.Response{}, meshxapi.NewHTTPError(http.StatusBadRequest, "too many agentIds in batch request")
	}
	batch, err := h.service.BatchListAgentAccessibleMemories(r.Context(), r.WorkspaceID(), r.Subject(), r.Body.AgentIDs)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentAccessBatchResponse(batch)), nil
}

func (h *MemoryHandler) BindAgentMemory(r meshxapi.AuthenticatedJSON[dto.AgentMemoryBindingRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	result, err := h.service.BindAgentMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"), r.Body.Input())
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentMemoryBindingResponse(result)), nil
}

func (h *MemoryHandler) UnbindAgentMemory(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	result, err := h.service.UnbindAgentMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentMemoryBindingResponse(result)), nil
}

func (h *MemoryHandler) ListAgentMemoryBindings(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	summary, err := h.service.ListAgentMemoryBindings(r.Context(), r.WorkspaceID(), r.Subject())
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentMemoryBindingsResponse(summary)), nil
}

func (h *MemoryHandler) LeaseAgentAssetAccessEvents(r meshxapi.AuthenticatedJSON[dto.AgentAssetAccessEventLeaseRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	if err := requireAgentAssetAccessEventService(r.AuthenticatedRequest); err != nil {
		return meshxapi.Response{}, err
	}
	leaseSeconds := r.Body.LeaseSeconds
	if leaseSeconds <= 0 {
		leaseSeconds = 60
	}
	events, err := h.service.LeaseAgentAssetAccessEvents(r.Context(), memoryservice.AgentAssetAccessEventLeaseOptions{
		LeaseOwner:    r.Body.LeaseOwner,
		LeaseDuration: time.Duration(leaseSeconds) * time.Second,
		Limit:         r.Body.Limit,
		WorkspaceIDs:  r.Body.WorkspaceIDs,
	})
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentAssetAccessEventLeaseResponse(events)), nil
}

func (h *MemoryHandler) MarkAgentAssetAccessEventDelivered(r meshxapi.AuthenticatedJSON[dto.AgentAssetAccessEventDeliveredRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	if err := requireAgentAssetAccessEventService(r.AuthenticatedRequest); err != nil {
		return meshxapi.Response{}, err
	}
	updated, err := h.service.MarkAgentAssetAccessEventDelivered(r.Context(), r.Request.PathValue("event_id"), r.Body.LeaseOwner, time.Time{})
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(dto.AgentAssetAccessEventAckResponse{Updated: updated}), nil
}

func (h *MemoryHandler) MarkAgentAssetAccessEventFailed(r meshxapi.AuthenticatedJSON[dto.AgentAssetAccessEventFailedRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	if err := requireAgentAssetAccessEventService(r.AuthenticatedRequest); err != nil {
		return meshxapi.Response{}, err
	}
	var nextAttemptAt time.Time
	if raw := strings.TrimSpace(r.Body.NextAttemptAt); raw != "" {
		parsed, err := time.Parse(time.RFC3339Nano, raw)
		if err != nil {
			return meshxapi.Response{}, meshxapi.NewHTTPError(http.StatusBadRequest, "next_attempt_at must be RFC3339")
		}
		nextAttemptAt = parsed
	}
	result, err := h.service.MarkAgentAssetAccessEventFailed(r.Context(), memoryservice.AgentAssetAccessEventFailure{
		EventID:       r.Request.PathValue("event_id"),
		LeaseOwner:    r.Body.LeaseOwner,
		LastError:     r.Body.LastError,
		NextAttemptAt: nextAttemptAt,
	})
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(dto.AgentAssetAccessEventAckResponse{Updated: result.Updated, Failed: result.Failed}), nil
}

func (h *MemoryHandler) Get(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	m, err := h.service.GetMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("memory_id"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	repositories, err := h.service.GetMemoryRepositories(r.Context(), r.WorkspaceID(), r.Subject(), m.ID)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToMemoryWithRepositories(m, repositories)), nil
}

func requireAgentAssetAccessEventService(r meshxapi.AuthenticatedRequest) error {
	workspaceID := strings.TrimSpace(r.WorkspaceID())
	if workspaceID != "" && !memoryStringClaimContains(r.Identity.RawClaims, "workspace_ids", workspaceID) {
		return meshxapi.NewHTTPError(http.StatusForbidden, "asset access event replay requires workspace-scoped service authority")
	}
	if !memoryBoolClaim(r.Identity.RawClaims, "service_actor", "serviceActor", "meshx_service_actor") ||
		!memoryStringClaimContains(r.Identity.RawClaims, "automation_permissions", "automation:service") {
		return meshxapi.NewHTTPError(http.StatusForbidden, "asset access event replay requires service authority")
	}
	return nil
}

func memoryStringClaimContains(claims map[string]any, key string, want string) bool {
	want = strings.TrimSpace(want)
	if want == "" {
		return false
	}
	raw, ok := claims[key]
	if !ok {
		return false
	}
	switch values := raw.(type) {
	case []any:
		for _, value := range values {
			if s, ok := value.(string); ok && strings.TrimSpace(s) == want {
				return true
			}
		}
	case []string:
		for _, value := range values {
			if strings.TrimSpace(value) == want {
				return true
			}
		}
	case string:
		for _, value := range strings.Split(values, ",") {
			if strings.TrimSpace(value) == want {
				return true
			}
		}
	}
	return false
}

func memoryBoolClaim(claims map[string]any, keys ...string) bool {
	for _, key := range keys {
		raw, ok := claims[key]
		if !ok {
			continue
		}
		switch value := raw.(type) {
		case bool:
			if value {
				return true
			}
		case string:
			if strings.EqualFold(strings.TrimSpace(value), "true") {
				return true
			}
		}
	}
	return false
}

func (h *MemoryHandler) Update(r meshxapi.AuthenticatedJSON[dto.MemoryUpdateRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	m, err := h.service.UpdateMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("memory_id"), r.Body.Input())
	if err != nil {
		return meshxapi.Response{}, err
	}
	repositories, err := h.service.GetMemoryRepositories(r.Context(), r.WorkspaceID(), r.Subject(), m.ID)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToMemoryWithRepositories(m, repositories)), nil
}

func (h *MemoryHandler) memoryRepositoriesForItems(ctx context.Context, workspaceID, subject string, items []memorystore.Memory) (map[string]memoryservice.MemoryRepositories, error) {
	repositories := make(map[string]memoryservice.MemoryRepositories, len(items))
	for _, item := range items {
		refs, err := h.service.GetMemoryRepositories(ctx, workspaceID, subject, item.ID)
		if err != nil {
			return nil, err
		}
		repositories[item.ID] = refs
	}
	return repositories, nil
}

func (h *MemoryHandler) Delete(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	if err := h.service.DeleteMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("memory_id")); err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.NoContent(), nil
}

func (h *MemoryHandler) Pin(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	if err := h.service.PinMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("memory_id")); err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.NoContent(), nil
}

func (h *MemoryHandler) Unpin(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	if err := h.service.UnpinMemory(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("memory_id")); err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.NoContent(), nil
}

func (h *MemoryHandler) ListPermissions(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	grants, err := h.service.ListMemoryPermissions(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("memory_id"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToMemoryPermissionsResponse(grants)), nil
}

func (h *MemoryHandler) UpsertPermission(r meshxapi.AuthenticatedJSON[dto.MemoryPermissionGrantRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	input := r.Body.Input()
	input.GrantID = r.Request.PathValue("grant_id")
	grant, err := h.service.UpsertMemoryPermission(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("memory_id"), input)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToMemoryPermissionGrant(grant)), nil
}

func (h *MemoryHandler) DeletePermission(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	if err := h.service.DeleteMemoryPermission(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("memory_id"), r.Request.PathValue("grant_id")); err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.NoContent(), nil
}

func activityListOptions(r *http.Request) (activity.ResourceActivityListOptions, error) {
	if r == nil || r.URL == nil {
		return activity.ResourceActivityListOptions{Limit: 50}, nil
	}
	query := r.URL.Query()
	limit := parsePositiveInt(query.Get("limit"), 0)
	if limit <= 0 {
		limit = parsePositiveInt(query.Get("per_page"), 50)
	}
	since, err := optionalQueryTime(query.Get("since"), "since")
	if err != nil {
		return activity.ResourceActivityListOptions{}, err
	}
	until, err := optionalQueryTime(query.Get("until"), "until")
	if err != nil {
		return activity.ResourceActivityListOptions{}, err
	}
	return activity.ResourceActivityListOptions{
		AssetID:   query.Get("assetId"),
		EventType: query.Get("eventType"),
		ActorID:   query.Get("actorId"),
		Cursor:    query.Get("cursor"),
		Limit:     limit,
		Since:     since,
		Until:     until,
	}, nil
}

func optionalQueryTime(raw, field string) (*time.Time, error) {
	if strings.TrimSpace(raw) == "" {
		return nil, nil
	}
	parsed, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(raw))
	if err != nil {
		return nil, meshxapi.NewHTTPError(http.StatusBadRequest, "invalid "+field)
	}
	parsed = parsed.UTC()
	return &parsed, nil
}

func pagination(r *http.Request) (int, int) {
	page := parsePositiveInt(r.URL.Query().Get("page"), 1)
	perPage := parsePositiveInt(r.URL.Query().Get("per_page"), 50)
	if perPage > maxPerPage {
		perPage = maxPerPage
	}
	return page, perPage
}

func parsePositiveInt(raw string, fallback int) int {
	value, err := strconv.Atoi(raw)
	if err != nil || value <= 0 {
		return fallback
	}
	return value
}

func memoryErrorHandling() meshxapi.HandlerOption {
	return meshxapi.WithErrorClassifier(func(err error) (meshxapi.ErrorResponse, bool) {
		if errors.Is(err, activity.ErrInvalidArgument) || errors.Is(err, activity.ErrInvalidCursor) {
			return meshxapi.ErrorResponse{Status: http.StatusBadRequest, Message: err.Error()}, true
		}
		if 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) {
			return meshxapi.ErrorResponse{Status: repos.HTTPStatus(err), Message: err.Error(), Code: repos.Code(err), Details: repos.Details(err)}, true
		}
		if errors.Is(err, memoryservice.ErrUnauthorized) || errors.Is(err, memoryservice.ErrInvalidArgument) || errors.Is(err, memoryservice.ErrForbidden) || errors.Is(err, memoryservice.ErrNotFound) || errors.Is(err, memoryservice.ErrConflict) {
			return meshxapi.ErrorResponse{Status: memoryservice.HTTPStatus(err), Message: err.Error(), Code: memoryservice.Code(err), Details: memoryservice.Details(err)}, true
		}
		return meshxapi.ErrorResponse{}, false
	})
}
