package skills

import (
	"context"
	"errors"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

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

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	"github.com/tidbcloud/AgentTeam/internal/api/meshxapi"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	skillservice "github.com/tidbcloud/AgentTeam/internal/service/skills"
	store "github.com/tidbcloud/AgentTeam/internal/store/resource/skills"
)

const maxPerPage = 100
const batchAgentIDsLimit = 200
const multipartImportOverheadBytes int64 = 1 << 20
const skillRepositoryContentsRequestMaxBytes int64 = 256 << 10

type Service interface {
	CreateSkill(ctx context.Context, workspaceID, subject string, input skillservice.SkillCreateMutation) (store.Skill, error)
	ImportSkillContent(ctx context.Context, workspaceID, subject, id string, input skillservice.SkillContentImportMutation) (skillservice.SkillContentImportResult, error)
	ListSkillRepositoryBackups(ctx context.Context, workspaceID, subject, skillID, query string, page, perPage int) (skillservice.SkillRepositoryBackupPage, error)
	CreateSkillRepositoryBackup(ctx context.Context, workspaceID, subject, skillID string, input skillservice.SkillRepositoryBackupCreateMutation) (skillservice.SkillRepositoryBackup, error)
	RestoreSkillRepositoryBackup(ctx context.Context, workspaceID, subject, skillID, tagName string) (skillservice.SkillRepositoryBackup, error)
	ListSkillVersions(ctx context.Context, workspaceID, subject, skillID string, page, perPage int) (skillservice.SkillVersionPage, error)
	RollbackSkillVersion(ctx context.Context, workspaceID, subject, skillID, versionID string) (skillservice.SkillVersion, error)
	ListSkills(ctx context.Context, workspaceID, subject string, options skillservice.SkillListOptions) (skillservice.SkillPage, error)
	CountWorkspaceSkills(ctx context.Context, workspaceID string, subject string) (int64, error)
	ListSkillActivity(ctx context.Context, workspaceID, subject string, opts activity.ResourceActivityListOptions) (activity.ResourceActivityPage, error)
	GetSkill(ctx context.Context, workspaceID, subject, id string) (store.Skill, error)
	GetSkillRepositories(ctx context.Context, workspaceID, subject, id string) (skillservice.SkillRepositories, error)
	ListSkillRepositories(ctx context.Context, workspaceID, subject string, skillIDs []string) (map[string]skillservice.SkillRepositories, error)
	ListSkillRepositorySkills(ctx context.Context, workspaceID, subject, id string) (skillservice.SkillRepositoryScanData, error)
	ReadSkillRepositorySkills(ctx context.Context, workspaceID, subject, id string, input skillservice.SkillRepositoryContentsRequest) (skillservice.SkillRepositoryContents, error)
	UpdateSkill(ctx context.Context, workspaceID, subject, id string, input skillservice.SkillUpdateMutation) (store.Skill, error)
	DeleteSkill(ctx context.Context, workspaceID, subject, id string) error
	BindAgentSkill(ctx context.Context, workspaceID, subject, agentID, skillID string, input skillservice.AgentSkillBindingMutation) (skillservice.AgentSkillBindingResult, error)
	UnbindAgentSkill(ctx context.Context, workspaceID, subject, agentID, skillID string) (skillservice.AgentSkillBindingResult, error)
	ListAgentSkillBindings(ctx context.Context, workspaceID, subject string, options skillservice.AgentSkillBindingsOptions) (skillservice.AgentSkillBindingsSummary, error)
	ListAgentSkillBindingsForAgent(ctx context.Context, workspaceID, subject, agentID string) ([]skillservice.AgentSkillBindingSummary, error)
	BatchListAgentAccessibleSkills(ctx context.Context, workspaceID, subject string, options skillservice.AgentSkillAccessBatchOptions) (skillservice.AgentSkillAccessBatch, error)
	ListSkillPermissions(ctx context.Context, workspaceID, subject, skillID string) (skillservice.SkillPermissionsSummary, error)
	UpsertSkillPermission(ctx context.Context, workspaceID, subject, skillID string, input skillservice.SkillPermissionGrantMutation) (skillservice.SkillPermissionGrant, error)
	DeleteSkillPermission(ctx context.Context, workspaceID, subject, skillID, grantID string) error
}

type SkillsHandler 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, "Skills API is not configured"))
}

func NewSkillsHandler(service Service) *SkillsHandler { return &SkillsHandler{service: service} }

func RegisterRoutes(router routeRegistrar, handler *SkillsHandler, middleware RouteMiddleware) {
	if router == nil {
		return
	}
	if handler == nil {
		disabled := DisabledHandler{}
		registerSkillRoute(router, "/api/v1/skills", disabled, middleware)
		registerSkillRoute(router, "/api/v1/skills/", disabled, middleware)
		return
	}
	registerSkillRoute(router, "POST /api/v1/skills", meshxapi.HandleAuthJSON[dto.SkillCreateRequest](handler.Create, skillErrorHandling()), middleware)
	registerSkillRoute(router, "GET /api/v1/skills", meshxapi.HandleAuth(handler.List, skillErrorHandling()), middleware)
	registerSkillRoute(router, "GET /api/v1/internal/skills/count", meshxapi.HandleAuth(handler.CountWorkspace, skillErrorHandling()), middleware)
	registerSkillRoute(router, "GET /api/v1/skills/activity", meshxapi.HandleAuth(handler.ListActivity, skillErrorHandling()), middleware)
	registerSkillRoute(router, "POST /api/v1/skills/agents/access:batch", meshxapi.HandleAuthJSON[dto.AgentSkillAccessBatchRequest](handler.AgentAccessBatch, skillErrorHandling()), middleware)
	registerSkillRoute(router, "GET /api/v1/skills/agents/bindings", meshxapi.HandleAuth(handler.ListAgentBindings, skillErrorHandling()), middleware)
	registerSkillRoute(router, "GET /api/v1/skills/agents/{agent_id}/bindings", meshxapi.HandleAuth(handler.ListAgentBindingsForAgent, skillErrorHandling()), middleware)
	registerSkillRoute(router, "PUT /api/v1/skills/agents/{agent_id}/bindings/{skill_id}", meshxapi.HandleAuthJSON[dto.AgentSkillBindingRequest](handler.BindAgentSkill, skillErrorHandling()), middleware)
	registerSkillRoute(router, "DELETE /api/v1/skills/agents/{agent_id}/bindings/{skill_id}", meshxapi.HandleAuth(handler.UnbindAgentSkill, skillErrorHandling()), middleware)
	registerSkillRoute(router, "GET /api/v1/skills/{skill_id}/skills", meshxapi.HandleAuth(handler.ListRepositorySkills, skillErrorHandling()), middleware)
	registerSkillRoute(router, "POST /api/v1/skills/{skill_id}/contents:read", meshxapi.HandleAuthJSONLimit[dto.SkillRepositoryContentsReadRequest](skillRepositoryContentsRequestMaxBytes, handler.ReadRepositorySkills, skillErrorHandling()), middleware)
	registerSkillRoute(router, "POST /api/v1/skills/{skill_id}/imports", handler.importHTTP(), middleware)
	registerSkillRoute(router, "GET /api/v1/skills/{skill_id}/backups", meshxapi.HandleAuth(handler.ListRepositoryBackups, skillErrorHandling()), middleware)
	registerSkillRoute(router, "POST /api/v1/skills/{skill_id}/backups", meshxapi.HandleAuthJSON[dto.SkillRepositoryBackupCreateRequest](handler.CreateRepositoryBackup, skillErrorHandling()), middleware)
	registerSkillRoute(router, "POST /api/v1/skills/{skill_id}/backups:restore", meshxapi.HandleAuthJSON[dto.SkillRepositoryBackupRestoreRequest](handler.RestoreRepositoryBackup, skillErrorHandling()), middleware)
	registerSkillRoute(router, "GET /api/v1/skills/{skill_id}/versions", meshxapi.HandleAuth(handler.ListVersions, skillErrorHandling()), middleware)
	registerSkillRoute(router, "POST /api/v1/skills/{skill_id}/versions:rollback", meshxapi.HandleAuthJSON[dto.SkillVersionRollbackRequest](handler.RollbackVersion, skillErrorHandling()), middleware)
	registerSkillRoute(router, "GET /api/v1/skills/{skill_id}", meshxapi.HandleAuth(handler.Get, skillErrorHandling()), middleware)
	registerSkillRoute(router, "PATCH /api/v1/skills/{skill_id}", meshxapi.HandleAuthJSON[dto.SkillUpdateRequest](handler.Update, skillErrorHandling()), middleware)
	registerSkillRoute(router, "DELETE /api/v1/skills/{skill_id}", meshxapi.HandleAuth(handler.Delete, skillErrorHandling()), middleware)
	registerSkillRoute(router, "GET /api/v1/skills/{skill_id}/permissions", meshxapi.HandleAuth(handler.ListPermissions, skillErrorHandling()), middleware)
	registerSkillRoute(router, "POST /api/v1/skills/{skill_id}/permissions/grants", meshxapi.HandleAuthJSON[dto.SkillPermissionGrantRequest](handler.UpsertPermission, skillErrorHandling()), middleware)
	registerSkillRoute(router, "PATCH /api/v1/skills/{skill_id}/permissions/grants/{grant_id}", meshxapi.HandleAuthJSON[dto.SkillPermissionGrantRequest](handler.UpsertPermission, skillErrorHandling()), middleware)
	registerSkillRoute(router, "DELETE /api/v1/skills/{skill_id}/permissions/grants/{grant_id}", meshxapi.HandleAuth(handler.DeletePermission, skillErrorHandling()), middleware)
}

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

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

func (h *SkillsHandler) ListPermissions(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	summary, err := h.service.ListSkillPermissions(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillPermissionsResponse(summary)), nil
}

func (h *SkillsHandler) UpsertPermission(r meshxapi.AuthenticatedJSON[dto.SkillPermissionGrantRequest]) (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.UpsertSkillPermission(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"), input)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillPermissionGrant(grant)), nil
}

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

func (h *SkillsHandler) Create(r meshxapi.AuthenticatedJSON[dto.SkillCreateRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	input := r.Body.Input()
	input.Actor = repos.ResourceRepositoryActorFromAgentTeam(r.Identity)
	skill, err := h.service.CreateSkill(r.Context(), r.WorkspaceID(), r.Subject(), input)
	if err != nil {
		return meshxapi.Response{}, err
	}
	repositories, err := h.service.GetSkillRepositories(r.Context(), r.WorkspaceID(), r.Subject(), skill.ID)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.Created(converter.ToSkillWithRepositories(skill, repositories)), nil
}

func (h *SkillsHandler) importHTTP() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if h == nil || h.service == nil {
			meshxapi.WriteError(w, meshxapi.NewHTTPError(http.StatusServiceUnavailable, "Skills API is not configured"))
			return
		}
		r.Body = http.MaxBytesReader(w, r.Body, skillservice.MaxSkillImportBytes+multipartImportOverheadBytes)
		request, err := meshxapi.NewAuthenticatedRequest(r)
		if err != nil {
			writeSkillHTTPError(w, err)
			return
		}
		if err := r.ParseMultipartForm(skillservice.MaxSkillImportBytes); err != nil {
			var maxBytesErr *http.MaxBytesError
			if errors.As(err, &maxBytesErr) {
				writeSkillHTTPError(w, meshxapi.NewHTTPError(http.StatusRequestEntityTooLarge, "Skill import archive exceeds maximum size"))
				return
			}
			writeSkillHTTPError(w, meshxapi.NewHTTPError(http.StatusBadRequest, "Invalid skill import form"))
			return
		}
		if r.MultipartForm != nil {
			defer r.MultipartForm.RemoveAll()
		}
		sourceURL := strings.TrimSpace(r.FormValue("url"))
		input := skillservice.SkillContentImportMutation{SourceURL: sourceURL}
		file, fileHeader, err := r.FormFile("file")
		if err != nil && !errors.Is(err, http.ErrMissingFile) {
			writeSkillHTTPError(w, meshxapi.NewHTTPError(http.StatusBadRequest, "Invalid skill import file"))
			return
		}
		if file != nil {
			defer file.Close()
			if files := r.MultipartForm.File["file"]; len(files) > 1 {
				writeSkillHTTPError(w, meshxapi.NewHTTPError(http.StatusBadRequest, "Only one skill import file is allowed"))
				return
			}
			temp, err := os.CreateTemp("", "meshx-skill-import-upload-*")
			if err != nil {
				writeSkillHTTPError(w, err)
				return
			}
			tempPath := temp.Name()
			defer os.Remove(tempPath)
			written, copyErr := io.Copy(temp, io.LimitReader(file, skillservice.MaxSkillImportBytes+1))
			closeErr := temp.Close()
			if copyErr != nil {
				writeSkillHTTPError(w, meshxapi.NewHTTPError(http.StatusBadRequest, "Could not read skill import file"))
				return
			}
			if closeErr != nil {
				writeSkillHTTPError(w, closeErr)
				return
			}
			if written > skillservice.MaxSkillImportBytes {
				writeSkillHTTPError(w, meshxapi.NewHTTPError(http.StatusRequestEntityTooLarge, "Skill import archive exceeds maximum size"))
				return
			}
			input.SourceFilePath = tempPath
			if fileHeader != nil {
				input.SourceName = fileHeader.Filename
			}
		}
		if (strings.TrimSpace(input.SourceFilePath) == "") == (sourceURL == "") {
			writeSkillHTTPError(w, meshxapi.NewHTTPError(http.StatusBadRequest, "Provide exactly one skill import file or URL"))
			return
		}
		result, err := h.service.ImportSkillContent(request.Context(), request.WorkspaceID(), request.Subject(), request.Request.PathValue("skill_id"), input)
		if err != nil {
			writeSkillHTTPError(w, err)
			return
		}
		meshxapi.WriteResponse(w, meshxapi.OK(converter.ToSkillImportResponse(result)))
	})
}

func (h *SkillsHandler) 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.ListSkills(r.Context(), r.WorkspaceID(), r.Subject(), skillservice.SkillListOptions{
		Query:          r.Request.URL.Query().Get("q"),
		Page:           page,
		PerPage:        perPage,
		TemplateLocale: r.Request.URL.Query().Get("template_locale"),
		View:           r.Request.URL.Query().Get("view"),
	})
	if err != nil {
		return meshxapi.Response{}, err
	}
	skillIDs := make([]string, 0, len(result.Items))
	for _, item := range result.Items {
		skillIDs = append(skillIDs, item.ID)
	}
	repositories, err := h.service.ListSkillRepositories(r.Context(), r.WorkspaceID(), r.Subject(), skillIDs)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillListResponse(result.Items, repositories, result.Page, result.PerPage, result.Total, result.HasMore)), nil
}

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

func (h *SkillsHandler) 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.ListSkillActivity(r.Context(), r.WorkspaceID(), r.Subject(), opts)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToResourceActivityListResponse(page)), nil
}

func (h *SkillsHandler) Get(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	skill, err := h.service.GetSkill(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	repositories, err := h.service.GetSkillRepositories(r.Context(), r.WorkspaceID(), r.Subject(), skill.ID)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillWithRepositories(skill, repositories)), nil
}

func (h *SkillsHandler) ListRepositorySkills(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	preview, err := h.service.ListSkillRepositorySkills(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillRepositorySkillsResponse(preview)), nil
}

func (h *SkillsHandler) ReadRepositorySkills(r meshxapi.AuthenticatedJSON[dto.SkillRepositoryContentsReadRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	contents, err := h.service.ReadSkillRepositorySkills(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"), r.Body.Input())
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillRepositoryContentsResponse(contents)), nil
}

func (h *SkillsHandler) ListRepositoryBackups(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.ListSkillRepositoryBackups(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"), r.Request.URL.Query().Get("q"), page, perPage)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillRepositoryBackupListResponse(result)), nil
}

func (h *SkillsHandler) CreateRepositoryBackup(r meshxapi.AuthenticatedJSON[dto.SkillRepositoryBackupCreateRequest]) (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.CreateSkillRepositoryBackup(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"), input)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.Created(converter.ToSkillRepositoryBackup(item)), nil
}

func (h *SkillsHandler) RestoreRepositoryBackup(r meshxapi.AuthenticatedJSON[dto.SkillRepositoryBackupRestoreRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	item, err := h.service.RestoreSkillRepositoryBackup(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"), r.Body.TagName)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillRepositoryBackup(item)), nil
}

// ListVersions handles GET /api/v1/skills/{skill_id}/versions.
// It returns one bounded page of immutable native Skill version projections.
func (h *SkillsHandler) ListVersions(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.ListSkillVersions(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"), page, perPage)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillVersionListResponse(result)), nil
}

// RollbackVersion handles POST /api/v1/skills/{skill_id}/versions:rollback.
// It publishes the selected immutable version as a new active native version.
func (h *SkillsHandler) RollbackVersion(r meshxapi.AuthenticatedJSON[dto.SkillVersionRollbackRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	version, err := h.service.RollbackSkillVersion(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"), r.Body.VersionID)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillVersion(version)), nil
}

func (h *SkillsHandler) Update(r meshxapi.AuthenticatedJSON[dto.SkillUpdateRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	skill, err := h.service.UpdateSkill(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("skill_id"), r.Body.Input())
	if err != nil {
		return meshxapi.Response{}, err
	}
	repositories, err := h.service.GetSkillRepositories(r.Context(), r.WorkspaceID(), r.Subject(), skill.ID)
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToSkillWithRepositories(skill, repositories)), nil
}

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

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

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

func (h *SkillsHandler) ListAgentBindings(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	bindings, err := h.service.ListAgentSkillBindings(r.Context(), r.WorkspaceID(), r.Subject(), skillservice.AgentSkillBindingsOptions{
		TemplateLocale: r.Request.URL.Query().Get("template_locale"),
	})
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentSkillBindingsResponse(bindings)), nil
}

func (h *SkillsHandler) ListAgentBindingsForAgent(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	bindings, err := h.service.ListAgentSkillBindingsForAgent(r.Context(), r.WorkspaceID(), r.Subject(), r.Request.PathValue("agent_id"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentSkillBindingSummaries(bindings)), nil
}

func (h *SkillsHandler) AgentAccessBatch(r meshxapi.AuthenticatedJSON[dto.AgentSkillAccessBatchRequest]) (meshxapi.Response, error) {
	if err := h.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	if len(r.Body.AgentIDs) == 0 {
		return meshxapi.OK(dto.AgentSkillAccessBatchResponse{Agents: map[string][]dto.AgentAccessibleSkill{}}), nil
	}
	if len(r.Body.AgentIDs) > batchAgentIDsLimit {
		return meshxapi.Response{}, meshxapi.NewHTTPError(http.StatusBadRequest, "too many agentIds in batch request")
	}
	batch, err := h.service.BatchListAgentAccessibleSkills(r.Context(), r.WorkspaceID(), r.Subject(), skillservice.AgentSkillAccessBatchOptions{
		AgentIDs:       r.Body.AgentIDs,
		TemplateLocale: r.Body.TemplateLocale,
	})
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(converter.ToAgentSkillAccessBatchResponse(batch)), nil
}

func writeSkillHTTPError(w http.ResponseWriter, err error) {
	var httpErr meshxapi.HTTPError
	if errors.As(err, &httpErr) {
		meshxapi.WriteError(w, err)
		return
	}
	if errors.Is(err, skillservice.ErrUnauthorized) || errors.Is(err, skillservice.ErrInvalidArgument) || errors.Is(err, skillservice.ErrForbidden) || errors.Is(err, skillservice.ErrNotFound) || errors.Is(err, skillservice.ErrConflict) || errors.Is(err, store.ErrRepositoryBranchConflict) {
		status := skillservice.HTTPStatus(err)
		code := strings.TrimSpace(skillservice.Code(err))
		if code == "" {
			code = http.StatusText(status)
			code = strings.ToLower(strings.ReplaceAll(code, " ", "_"))
		}
		body := map[string]any{"error": code, "message": err.Error()}
		if details := skillservice.Details(err); details != nil {
			body["details"] = details
		}
		meshxapi.WriteResponse(w, meshxapi.Response{Status: status, Body: body})
		return
	}
	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) {
		status := repos.HTTPStatus(err)
		code := strings.TrimSpace(repos.Code(err))
		if code == "" {
			code = http.StatusText(status)
			code = strings.ToLower(strings.ReplaceAll(code, " ", "_"))
		}
		body := map[string]any{"error": code, "message": err.Error()}
		if details := repos.Details(err); details != nil {
			body["details"] = details
		}
		meshxapi.WriteResponse(w, meshxapi.Response{Status: status, Body: body})
		return
	}
	meshxapi.WriteError(w, err)
}

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 := positiveInt(query.Get("limit"), 0)
	if limit <= 0 {
		limit = positiveInt(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) {
	if r == nil || r.URL == nil {
		return 1, 50
	}
	page := positiveInt(r.URL.Query().Get("page"), 1)
	perPage := positiveInt(r.URL.Query().Get("per_page"), 50)
	if perPage > maxPerPage {
		perPage = maxPerPage
	}
	return page, perPage
}

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

func skillErrorHandling() 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, skillservice.ErrUnauthorized) || errors.Is(err, skillservice.ErrInvalidArgument) || errors.Is(err, skillservice.ErrForbidden) || errors.Is(err, skillservice.ErrNotFound) || errors.Is(err, skillservice.ErrConflict) || errors.Is(err, store.ErrRepositoryBranchConflict) {
			return meshxapi.ErrorResponse{Status: skillservice.HTTPStatus(err), Message: err.Error(), Code: skillservice.Code(err), Details: skillservice.Details(err)}, 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
		}
		return meshxapi.ErrorResponse{}, false
	})
}
