package app

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"sub2api-cn-relay-manager/internal/batch"
	"sub2api-cn-relay-manager/internal/config"
	"sub2api-cn-relay-manager/internal/host/sub2api"
	"sub2api-cn-relay-manager/internal/pack"
	"sub2api-cn-relay-manager/internal/provision"
	"sub2api-cn-relay-manager/internal/reconcile"
	"sub2api-cn-relay-manager/internal/store/sqlite"

	"sub2api-cn-relay-manager/internal/access"
)

type ActionSet struct {
	CreateBatchImportRun                func(context.Context, CreateBatchImportRunRequest) (BatchImportRunCreateResponse, error)
	ListBatchImportRuns                 func(context.Context, ListBatchImportRunsRequest) (ListBatchImportRunsResponse, error)
	GetBatchImportRun                   func(context.Context, string) (batch.RunSummaryProjection, error)
	ListBatchImportRunItems             func(context.Context, ListBatchImportRunItemsRequest) (ListBatchImportRunItemsResponse, error)
	GetBatchImportRunItem               func(context.Context, GetBatchImportRunItemRequest) (batch.ItemDetailProjection, error)
	ListPortalLogicalGroups             func(context.Context) ([]PortalLogicalGroupInfo, error)
	GetPortalLogicalGroup               func(context.Context, string) (PortalLogicalGroupInfo, error)
	ListPortalLogicalGroupModels        func(context.Context, string) ([]PortalLogicalGroupModel, error)
	CreateLogicalGroup                  func(context.Context, CreateLogicalGroupRequest) (LogicalGroupInfo, error)
	ListLogicalGroups                   func(context.Context) ([]LogicalGroupInfo, error)
	GetLogicalGroup                     func(context.Context, string) (LogicalGroupInfo, error)
	UpdateLogicalGroup                  func(context.Context, UpdateLogicalGroupRequest) (LogicalGroupInfo, error)
	DeleteLogicalGroup                  func(context.Context, string) error
	CreateLogicalGroupModel             func(context.Context, CreateLogicalGroupModelRequest) (LogicalGroupModelInfo, error)
	ListLogicalGroupModels              func(context.Context, string) ([]LogicalGroupModelInfo, error)
	DeleteLogicalGroupModel             func(context.Context, DeleteLogicalGroupModelRequest) error
	CreateLogicalGroupRoute             func(context.Context, CreateLogicalGroupRouteRequest) (LogicalGroupRouteInfo, error)
	ListLogicalGroupRoutes              func(context.Context, string) ([]LogicalGroupRouteInfo, error)
	UpdateLogicalGroupRoute             func(context.Context, UpdateLogicalGroupRouteRequest) (LogicalGroupRouteInfo, error)
	DeleteLogicalGroupRoute             func(context.Context, DeleteLogicalGroupRouteRequest) error
	CreateLogicalGroupRouteModel        func(context.Context, CreateLogicalGroupRouteModelRequest) (LogicalGroupRouteModelInfo, error)
	ListLogicalGroupRouteModels         func(context.Context, ListLogicalGroupRouteModelsRequest) ([]LogicalGroupRouteModelInfo, error)
	AppendRouteDecisionLog              func(context.Context, AppendRouteDecisionLogRequest) (RouteDecisionLogInfo, error)
	ListRouteDecisionLogs               func(context.Context, ListRouteDecisionLogsRequest) ([]RouteDecisionLogInfo, error)
	AppendRouteFailoverEvent            func(context.Context, AppendRouteFailoverEventRequest) (RouteFailoverEventInfo, error)
	ListRouteFailoverEvents             func(context.Context, ListRouteFailoverEventsRequest) ([]RouteFailoverEventInfo, error)
	AppendRouteStickyAudit              func(context.Context, AppendRouteStickyAuditRequest) (RouteStickyAuditInfo, error)
	ListRouteStickyAudit                func(context.Context, ListRouteStickyAuditRequest) ([]RouteStickyAuditInfo, error)
	ListRouteHealth                     func(context.Context, ListRouteHealthRequest) ([]RouteHealthInfo, error)
	ResolveRoute                        func(context.Context, ResolveRouteRequest) (ResolveRouteInfo, error)
	RouteChatCompletions                func(context.Context, RouteChatCompletionsRequest) (RouteChatCompletionsResult, error)
	ProxyRouteChatCompletions           func(context.Context, ProxyRouteChatCompletionsRequest) (ProxyRouteChatCompletionsResult, error)
	SetStickyBinding                    func(context.Context, SetStickyBindingRequest) (StickyBindingInfo, error)
	GetStickyBinding                    func(context.Context, GetStickyBindingRequest) (StickyBindingInfo, error)
	SetRouteFailure                     func(context.Context, SetRouteFailureRequest) (RouteFailureInfo, error)
	GetRouteFailure                     func(context.Context, GetRouteFailureRequest) (RouteFailureInfo, error)
	SetRouteCooldown                    func(context.Context, SetRouteCooldownRequest) (RouteCooldownInfo, error)
	GetRouteCooldown                    func(context.Context, GetRouteCooldownRequest) (RouteCooldownInfo, error)
	ListProviderAccounts                func(context.Context, ListProviderAccountsRequest) ([]ProviderAccountInfo, error)
	GetProviderAccountBindingCandidates func(context.Context, GetProviderAccountBindingCandidatesRequest) (ProviderAccountBindingCandidatesResult, error)
	UpdateProviderAccountBinding        func(context.Context, UpdateProviderAccountBindingRequest) (ProviderAccountInfo, error)
	EnableProviderAccount               func(context.Context, UpdateProviderAccountStatusRequest) (ProviderAccountInfo, error)
	DisableProviderAccount              func(context.Context, UpdateProviderAccountStatusRequest) (ProviderAccountInfo, error)
	RetireProviderAccount               func(context.Context, UpdateProviderAccountStatusRequest) (ProviderAccountInfo, error)
	CreateProviderDraft                 func(context.Context, CreateProviderDraftRequest) (ProviderDraftInfo, error)
	ListProviderDrafts                  func(context.Context, ListProviderDraftsRequest) ([]ProviderDraftInfo, error)
	GetProviderDraft                    func(context.Context, string) (ProviderDraftInfo, error)
	UpdateProviderDraft                 func(context.Context, UpdateProviderDraftRequest) (ProviderDraftInfo, error)
	DeleteProviderDraft                 func(context.Context, string) error
	PublishProviderDraft                func(context.Context, PublishProviderDraftRequest) (PublishProviderDraftResult, error)
	InstallPack                         func(context.Context, InstallPackRequest) (provision.PackInstallResult, error)
	BatchDetail                         func(context.Context, BatchDetailRequest) (provision.BatchDetailResult, error)
	GetProviderStatus                   func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)
	GetProviderResources                func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)
	GetProviderAccessStatus             func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)
	ListProviderImportBatches           func(context.Context, ProviderQueryRequest) ([]ImportBatchInfo, error)
	PreviewProvider                     func(context.Context, PreviewProviderRequest) (provision.PreviewReport, error)
	ImportProvider                      func(context.Context, ImportProviderRequest) (provision.RuntimeImportResult, error)
	RollbackProvider                    func(context.Context, RollbackProviderRequest) (provision.RollbackReport, error)
	RollbackBatch                       func(context.Context, RollbackBatchRequest) (provision.RollbackReport, error)
	ReconcileProvider                   func(context.Context, ReconcileProviderRequest) (reconcile.Result, error)
	CreateHost                          func(context.Context, CreateHostRequest) (HostInfo, error)
	ProbeHost                           func(context.Context, ProbeHostRequest) (HostInfo, error)
	ListHosts                           func(context.Context) ([]HostInfo, error)
	GetHost                             func(context.Context, string) (HostInfo, error)
	DeleteHost                          func(context.Context, string) error
	ListPacks                           func(context.Context) ([]PackInfo, error)
	GetPack                             func(context.Context, string) (PackInfo, error)
	ListPackProviders                   func(context.Context, string) ([]PackProviderInfo, error)
	AssignAccessSubscriptions           func(context.Context, AssignAccessSubscriptionsRequest) (AssignAccessSubscriptionsResult, error)
	AccessPreview                       func(context.Context, AccessPreviewRequest) (AccessPreviewResult, error)
}

const maxJSONBodyBytes int64 = 1 << 20

type HostInfo struct {
	HostID       string                    `json:"host_id"`
	BaseURL      string                    `json:"base_url"`
	HostVersion  string                    `json:"host_version"`
	AuthType     string                    `json:"auth_type,omitempty"`
	Status       string                    `json:"status,omitempty"`
	Capabilities *sub2api.HostCapabilities `json:"capabilities,omitempty"`
}

type CreateHostRequest struct {
	Name    string         `json:"name"`
	BaseURL string         `json:"base_url"`
	Auth    CreateHostAuth `json:"auth"`
}

type ProbeHostRequest struct {
	HostID string         `json:"-"`
	Auth   CreateHostAuth `json:"auth"`
}

type CreateHostAuth struct {
	Type  string `json:"type"`
	Token string `json:"token"`
}

type ImportBatchInfo struct {
	BatchID      int64  `json:"batch_id"`
	BatchStatus  string `json:"batch_status"`
	AccessStatus string `json:"access_status"`
}

type PackInfo struct {
	PackID         string `json:"pack_id"`
	Version        string `json:"version"`
	Vendor         string `json:"vendor,omitempty"`
	TargetHost     string `json:"target_host,omitempty"`
	MinHostVersion string `json:"min_host_version,omitempty"`
	MaxHostVersion string `json:"max_host_version,omitempty"`
}

type PackProviderInfo struct {
	ProviderID      string   `json:"provider_id"`
	DisplayName     string   `json:"display_name"`
	Platform        string   `json:"platform,omitempty"`
	HostOverlays    int      `json:"host_overlays,omitempty"`
	BaseURL         string   `json:"base_url,omitempty"`
	SmokeTestModel  string   `json:"smoke_test_model,omitempty"`
	SupportedModels []string `json:"supported_models,omitempty"`
}

type CreateProviderDraftRequest struct {
	DraftID         string          `json:"draft_id,omitempty"`
	PackID          string          `json:"pack_id"`
	ProviderID      string          `json:"provider_id"`
	DisplayName     string          `json:"display_name"`
	Platform        string          `json:"platform"`
	BaseURL         string          `json:"base_url,omitempty"`
	SmokeTestModel  string          `json:"smoke_test_model,omitempty"`
	SupportedModels []string        `json:"supported_models,omitempty"`
	Manifest        json.RawMessage `json:"manifest,omitempty"`
	SourceHostID    string          `json:"source_host_id,omitempty"`
	Notes           string          `json:"notes,omitempty"`
}

type ListProviderDraftsRequest struct {
	PackID     string
	ProviderID string
	Query      string
}

type UpdateProviderDraftRequest struct {
	DraftID string `json:"-"`
	CreateProviderDraftRequest
}

type PublishProviderDraftRequest struct {
	DraftID       string `json:"-"`
	CommitMessage string `json:"commit_message,omitempty"`
}

type ProviderDraftInfo struct {
	DraftID         string   `json:"draft_id"`
	PackID          string   `json:"pack_id"`
	ProviderID      string   `json:"provider_id"`
	DisplayName     string   `json:"display_name"`
	Platform        string   `json:"platform"`
	BaseURL         string   `json:"base_url,omitempty"`
	SmokeTestModel  string   `json:"smoke_test_model,omitempty"`
	SupportedModels []string `json:"supported_models,omitempty"`
	Manifest        any      `json:"manifest,omitempty"`
	SourceHostID    string   `json:"source_host_id,omitempty"`
	Notes           string   `json:"notes,omitempty"`
	CreatedAt       string   `json:"created_at,omitempty"`
	UpdatedAt       string   `json:"updated_at,omitempty"`
}

type PublishProviderDraftResult struct {
	DraftID           string `json:"draft_id"`
	PackID            string `json:"pack_id"`
	ProviderID        string `json:"provider_id"`
	ProviderPath      string `json:"provider_path"`
	PackVersionBefore string `json:"pack_version_before"`
	PackVersionAfter  string `json:"pack_version_after"`
	PublishMode       string `json:"publish_mode"`
	CommitMessage     string `json:"commit_message"`
	CommitSHA         string `json:"commit_sha"`
	RepoRoot          string `json:"repo_root"`
}

type AssignAccessSubscriptionsRequest struct {
	HostID            string   `json:"host_id,omitempty"`
	PackPath          string   `json:"pack_path"`
	ProviderID        string   `json:"provider_id"`
	HostBaseURL       string   `json:"host_base_url,omitempty"`
	HostAPIKey        string   `json:"host_api_key,omitempty"`
	HostBearerToken   string   `json:"host_bearer_token,omitempty"`
	AccessAPIKey      string   `json:"access_api_key"`
	SubscriptionUsers []string `json:"subscription_users"`
	SubscriptionDays  int      `json:"subscription_days"`
}

type AssignAccessSubscriptionsResult struct {
	ProviderID   string `json:"provider_id"`
	Assigned     int    `json:"assigned"`
	AccessStatus string `json:"access_status"`
}

type AccessPreviewRequest struct {
	ProviderID string `json:"provider_id"`
	PackID     string `json:"pack_id,omitempty"`
	HostID     string `json:"host_id,omitempty"`
	Mode       string `json:"mode"`
}

type AccessPreviewResult struct {
	ProviderID string `json:"provider_id"`
	Mode       string `json:"mode"`
	Available  bool   `json:"available"`
	Message    string `json:"message,omitempty"`
}

type InstallPackRequest struct {
	HostBaseURL     string `json:"host_base_url"`
	HostAPIKey      string `json:"host_api_key"`
	HostBearerToken string `json:"host_bearer_token"`
	PackPath        string `json:"pack_path"`
}

type BatchDetailRequest struct {
	BatchID int64
}

type ProviderQueryRequest struct {
	ProviderID string
	PackID     string
	HostID     string
}

type RollbackProviderRequest struct {
	HostID          string `json:"host_id,omitempty"`
	HostBaseURL     string `json:"host_base_url,omitempty"`
	HostAPIKey      string `json:"host_api_key,omitempty"`
	HostBearerToken string `json:"host_bearer_token,omitempty"`
	PackPath        string `json:"pack_path"`
	ProviderID      string `json:"provider_id"`
}

type RollbackBatchRequest struct {
	BatchID int64          `json:"-"`
	Auth    CreateHostAuth `json:"auth"`
}

type ReconcileProviderRequest struct {
	HostID          string `json:"host_id,omitempty"`
	HostBaseURL     string `json:"host_base_url,omitempty"`
	HostAPIKey      string `json:"host_api_key,omitempty"`
	HostBearerToken string `json:"host_bearer_token,omitempty"`
	PackPath        string `json:"pack_path"`
	ProviderID      string `json:"provider_id"`
	AccessAPIKey    string `json:"access_api_key"`
}

type PreviewProviderRequest struct {
	HostID          string   `json:"host_id,omitempty"`
	HostBaseURL     string   `json:"host_base_url,omitempty"`
	HostAPIKey      string   `json:"host_api_key,omitempty"`
	HostBearerToken string   `json:"host_bearer_token,omitempty"`
	PackPath        string   `json:"pack_path"`
	ProviderID      string   `json:"provider_id"`
	Keys            []string `json:"keys"`
	Mode            string   `json:"mode"`
}

type ImportProviderRequest struct {
	HostID            string   `json:"host_id,omitempty"`
	HostBaseURL       string   `json:"host_base_url,omitempty"`
	HostAPIKey        string   `json:"host_api_key,omitempty"`
	HostBearerToken   string   `json:"host_bearer_token,omitempty"`
	PackPath          string   `json:"pack_path"`
	ProviderID        string   `json:"provider_id"`
	Keys              []string `json:"keys"`
	Mode              string   `json:"mode"`
	AccessMode        string   `json:"access_mode"`
	AccessAPIKey      string   `json:"access_api_key"`
	SubscriptionUsers []string `json:"subscription_users"`
	SubscriptionDays  int      `json:"subscription_days"`
}

type httpError struct {
	StatusCode     int    `json:"-"`
	Code           string `json:"code"`
	Message        string `json:"message"`
	UpstreamStatus int    `json:"upstream_status,omitempty"`
}

func (e *httpError) Error() string {
	return e.Message
}

func NewAPIHandler(adminToken string, actions ActionSet) http.Handler {
	return NewAPIHandlerWithAuth(AdminAuthConfig{Token: adminToken}, actions)
}

func NewAPIHandlerWithAuth(adminAuth AdminAuthConfig, actions ActionSet) http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", healthz)
	mux.HandleFunc("GET /api/admin/session", func(w http.ResponseWriter, r *http.Request) {
		handleAdminSessionState(w, r, adminAuth)
	})
	mux.HandleFunc("POST /api/admin/session/login", func(w http.ResponseWriter, r *http.Request) {
		handleAdminSessionLogin(w, r, adminAuth)
	})
	mux.HandleFunc("POST /api/admin/session/logout", func(w http.ResponseWriter, r *http.Request) {
		handleAdminSessionLogout(w, r)
	})
	mux.HandleFunc("GET /api/portal/logical-groups", func(w http.ResponseWriter, r *http.Request) {
		handleListPortalLogicalGroups(w, r, actions.ListPortalLogicalGroups)
	})
	mux.HandleFunc("GET /api/portal/logical-groups/{groupID}", func(w http.ResponseWriter, r *http.Request) {
		handleGetPortalLogicalGroup(w, r, actions.GetPortalLogicalGroup)
	})
	mux.HandleFunc("GET /api/portal/logical-groups/{groupID}/models", func(w http.ResponseWriter, r *http.Request) {
		handleListPortalLogicalGroupModels(w, r, actions.ListPortalLogicalGroupModels)
	})
	mux.Handle("POST /api/batch-import/runs", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleCreateBatchImportRun(w, r, actions.CreateBatchImportRun)
	})))
	mux.Handle("GET /api/batch-import/runs", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListBatchImportRuns(w, r, actions.ListBatchImportRuns)
	})))
	mux.Handle("GET /api/batch-import/runs/{run_id}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleGetBatchImportRun(w, r, actions.GetBatchImportRun)
	})))
	mux.Handle("GET /api/batch-import/runs/{run_id}/items", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListBatchImportRunItems(w, r, actions.ListBatchImportRunItems)
	})))
	mux.Handle("GET /api/batch-import/runs/{run_id}/items/{item_id}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleGetBatchImportRunItem(w, r, actions.GetBatchImportRunItem)
	})))
	mux.Handle("POST /api/logical-groups", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleCreateLogicalGroup(w, r, actions.CreateLogicalGroup)
	})))
	mux.Handle("GET /api/logical-groups", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListLogicalGroups(w, r, actions.ListLogicalGroups)
	})))
	mux.Handle("GET /api/logical-groups/{groupID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleGetLogicalGroup(w, r, actions.GetLogicalGroup)
	})))
	mux.Handle("PUT /api/logical-groups/{groupID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleUpdateLogicalGroup(w, r, actions.UpdateLogicalGroup)
	})))
	mux.Handle("DELETE /api/logical-groups/{groupID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleDeleteLogicalGroup(w, r, actions.DeleteLogicalGroup)
	})))
	mux.Handle("POST /api/logical-groups/{groupID}/models", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleCreateLogicalGroupModel(w, r, actions.CreateLogicalGroupModel)
	})))
	mux.Handle("GET /api/logical-groups/{groupID}/models", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListLogicalGroupModels(w, r, actions.ListLogicalGroupModels)
	})))
	mux.Handle("DELETE /api/logical-groups/{groupID}/models/{model}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleDeleteLogicalGroupModel(w, r, actions.DeleteLogicalGroupModel)
	})))
	mux.Handle("POST /api/logical-groups/{groupID}/routes", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleCreateLogicalGroupRoute(w, r, actions.CreateLogicalGroupRoute)
	})))
	mux.Handle("GET /api/logical-groups/{groupID}/routes", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListLogicalGroupRoutes(w, r, actions.ListLogicalGroupRoutes)
	})))
	mux.Handle("PUT /api/logical-groups/{groupID}/routes/{routeID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleUpdateLogicalGroupRoute(w, r, actions.UpdateLogicalGroupRoute)
	})))
	mux.Handle("DELETE /api/logical-groups/{groupID}/routes/{routeID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleDeleteLogicalGroupRoute(w, r, actions.DeleteLogicalGroupRoute)
	})))
	mux.Handle("POST /api/logical-groups/{groupID}/routes/{routeID}/models", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleCreateLogicalGroupRouteModel(w, r, actions.CreateLogicalGroupRouteModel)
	})))
	mux.Handle("GET /api/logical-groups/{groupID}/routes/{routeID}/models", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListLogicalGroupRouteModels(w, r, actions.ListLogicalGroupRouteModels)
	})))
	mux.Handle("POST /api/routing/logs/decisions", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleAppendRouteDecisionLog(w, r, actions.AppendRouteDecisionLog)
	})))
	mux.Handle("GET /api/routing/logs/decisions", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListRouteDecisionLogs(w, r, actions.ListRouteDecisionLogs)
	})))
	mux.Handle("POST /api/routing/logs/failovers", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleAppendRouteFailoverEvent(w, r, actions.AppendRouteFailoverEvent)
	})))
	mux.Handle("GET /api/routing/logs/failovers", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListRouteFailoverEvents(w, r, actions.ListRouteFailoverEvents)
	})))
	mux.Handle("POST /api/routing/logs/sticky-audit", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleAppendRouteStickyAudit(w, r, actions.AppendRouteStickyAudit)
	})))
	mux.Handle("GET /api/routing/logs/sticky-audit", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListRouteStickyAudit(w, r, actions.ListRouteStickyAudit)
	})))
	mux.Handle("GET /api/routing/routes/health", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListRouteHealth(w, r, actions.ListRouteHealth)
	})))
	mux.Handle("POST /api/routing/resolve", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleResolveRoute(w, r, actions.ResolveRoute)
	})))
	mux.Handle("POST /api/routing/chat/completions", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleRouteChatCompletions(w, r, actions.RouteChatCompletions)
	})))
	mux.Handle("POST /api/routing/proxy/chat/completions", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleProxyRouteChatCompletions(w, r, actions.ProxyRouteChatCompletions)
	})))
	mux.Handle("POST /api/routing/sticky/bindings", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleSetStickyBinding(w, r, actions.SetStickyBinding)
	})))
	mux.Handle("GET /api/routing/sticky/bindings", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleGetStickyBinding(w, r, actions.GetStickyBinding)
	})))
	mux.Handle("POST /api/routing/sticky/route-failures", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleSetRouteFailure(w, r, actions.SetRouteFailure)
	})))
	mux.Handle("GET /api/routing/sticky/route-failures", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleGetRouteFailure(w, r, actions.GetRouteFailure)
	})))
	mux.Handle("POST /api/routing/sticky/cooldowns", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleSetRouteCooldown(w, r, actions.SetRouteCooldown)
	})))
	mux.Handle("GET /api/routing/sticky/cooldowns", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleGetRouteCooldown(w, r, actions.GetRouteCooldown)
	})))
	mux.Handle("GET /api/provider-accounts", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListProviderAccounts(w, r, actions.ListProviderAccounts)
	})))
	mux.Handle("GET /api/provider-accounts/{accountID}/binding-candidates", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleGetProviderAccountBindingCandidates(w, r, actions.GetProviderAccountBindingCandidates)
	})))
	mux.Handle("POST /api/provider-accounts/{accountID}/binding", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleUpdateProviderAccountBinding(w, r, actions.UpdateProviderAccountBinding)
	})))
	mux.Handle("POST /api/provider-accounts/{accountID}/enable", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleEnableProviderAccount(w, r, actions.EnableProviderAccount)
	})))
	mux.Handle("POST /api/provider-accounts/{accountID}/disable", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleDisableProviderAccount(w, r, actions.DisableProviderAccount)
	})))
	mux.Handle("POST /api/provider-accounts/{accountID}/retire", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleRetireProviderAccount(w, r, actions.RetireProviderAccount)
	})))
	mux.Handle("POST /api/provider-drafts", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleCreateProviderDraft(w, r, actions.CreateProviderDraft)
	})))
	mux.Handle("GET /api/provider-drafts", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListProviderDrafts(w, r, actions.ListProviderDrafts)
	})))
	mux.Handle("GET /api/provider-drafts/{draftID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleGetProviderDraft(w, r, actions.GetProviderDraft)
	})))
	mux.Handle("PUT /api/provider-drafts/{draftID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleUpdateProviderDraft(w, r, actions.UpdateProviderDraft)
	})))
	mux.Handle("DELETE /api/provider-drafts/{draftID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleDeleteProviderDraft(w, r, actions.DeleteProviderDraft)
	})))
	mux.Handle("POST /api/provider-drafts/{draftID}/publish", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handlePublishProviderDraft(w, r, actions.PublishProviderDraft)
	})))
	mux.Handle("GET /api/import-batches/{batchID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleBatchDetail(w, r, actions.BatchDetail)
	})))
	mux.Handle("POST /api/import-batches/{batchID}/rollback", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleRollbackBatch(w, r, actions.RollbackBatch)
	})))
	mux.Handle("GET /api/providers/{providerID}/status", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleProviderStatus(w, r, actions.GetProviderStatus)
	})))
	mux.Handle("GET /api/providers/{providerID}/resources", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleProviderResources(w, r, actions.GetProviderResources)
	})))
	mux.Handle("GET /api/providers/{providerID}/access/status", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleProviderAccessStatus(w, r, actions.GetProviderAccessStatus)
	})))
	mux.Handle("GET /api/providers/{providerID}/import-batches", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListProviderImportBatches(w, r, actions.ListProviderImportBatches)
	})))
	mux.Handle("POST /api/providers/{providerID}/access/assign-subscriptions", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleAssignAccessSubscriptions(w, r, actions.AssignAccessSubscriptions)
	})))
	mux.Handle("POST /api/providers/{providerID}/access/preview", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleAccessPreview(w, r, actions.AccessPreview)
	})))
	mux.Handle("POST /api/packs/install", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleInstallPack(w, r, actions.InstallPack)
	})))
	mux.Handle("GET /api/packs", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListPacks(w, r, actions.ListPacks)
	})))
	mux.Handle("GET /api/packs/{packID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleGetPack(w, r, actions.GetPack)
	})))
	mux.Handle("GET /api/packs/{packID}/providers", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListPackProviders(w, r, actions.ListPackProviders)
	})))
	mux.Handle("POST /api/providers/{providerID}/preview-import", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handlePreviewProvider(w, r, actions.PreviewProvider)
	})))
	mux.Handle("POST /api/providers/{providerID}/import", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleImportProvider(w, r, actions.ImportProvider)
	})))
	mux.Handle("POST /api/providers/{providerID}/rollback", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleRollbackProvider(w, r, actions.RollbackProvider)
	})))
	mux.Handle("POST /api/providers/{providerID}/reconcile", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleReconcileProvider(w, r, actions.ReconcileProvider)
	})))
	mux.Handle("GET /api/hosts", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleListHosts(w, r, actions.ListHosts)
	})))
	mux.Handle("GET /api/hosts/{hostID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleGetHost(w, r, actions.GetHost)
	})))
	mux.Handle("POST /api/hosts", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleCreateHost(w, r, actions.CreateHost)
	})))
	mux.Handle("POST /api/hosts/{hostID}/probe", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleProbeHost(w, r, actions.ProbeHost)
	})))
	mux.Handle("DELETE /api/hosts/{hostID}", requireAdminAccess(adminAuth, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handleDeleteHost(w, r, actions.DeleteHost)
	})))
	return mux
}

func healthz(w http.ResponseWriter, _ *http.Request) {
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte("ok"))
}

func handleCreateProviderDraft(w http.ResponseWriter, r *http.Request, fn func(context.Context, CreateProviderDraftRequest) (ProviderDraftInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "create-provider-draft action is not configured"})
		return
	}
	var req CreateProviderDraftRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	draft, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusCreated, map[string]any{"draft": draft})
}

func handleListProviderDrafts(w http.ResponseWriter, r *http.Request, fn func(context.Context, ListProviderDraftsRequest) ([]ProviderDraftInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-provider-drafts action is not configured"})
		return
	}
	drafts, err := fn(r.Context(), ListProviderDraftsRequest{
		PackID:     strings.TrimSpace(r.URL.Query().Get("pack_id")),
		ProviderID: strings.TrimSpace(r.URL.Query().Get("provider_id")),
		Query:      strings.TrimSpace(r.URL.Query().Get("q")),
	})
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	if drafts == nil {
		drafts = []ProviderDraftInfo{}
	}
	writeJSON(w, http.StatusOK, map[string]any{"provider_drafts": drafts})
}

func handleGetProviderDraft(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) (ProviderDraftInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "get-provider-draft action is not configured"})
		return
	}
	draftID := strings.TrimSpace(r.PathValue("draftID"))
	if draftID == "" {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "draft_id is required"})
		return
	}
	draft, err := fn(r.Context(), draftID)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"draft": draft})
}

func handleUpdateProviderDraft(w http.ResponseWriter, r *http.Request, fn func(context.Context, UpdateProviderDraftRequest) (ProviderDraftInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "update-provider-draft action is not configured"})
		return
	}
	var req UpdateProviderDraftRequest
	if err := decodeJSON(r, &req.CreateProviderDraftRequest); err != nil {
		writeHTTPError(w, err)
		return
	}
	req.DraftID = strings.TrimSpace(r.PathValue("draftID"))
	if req.DraftID == "" {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "draft_id is required"})
		return
	}
	draft, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"draft": draft})
}

func handleDeleteProviderDraft(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) error) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "delete-provider-draft action is not configured"})
		return
	}
	draftID := strings.TrimSpace(r.PathValue("draftID"))
	if draftID == "" {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "draft_id is required"})
		return
	}
	if err := fn(r.Context(), draftID); err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func handlePublishProviderDraft(w http.ResponseWriter, r *http.Request, fn func(context.Context, PublishProviderDraftRequest) (PublishProviderDraftResult, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "publish-provider-draft action is not configured"})
		return
	}
	var req PublishProviderDraftRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	req.DraftID = strings.TrimSpace(r.PathValue("draftID"))
	if req.DraftID == "" {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "draft_id is required"})
		return
	}
	result, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"publish": result})
}

func bearerToken(r *http.Request) string {
	header := strings.TrimSpace(r.Header.Get("Authorization"))
	if !strings.HasPrefix(strings.ToLower(header), "bearer ") {
		return ""
	}
	return strings.TrimSpace(header[len("Bearer "):])
}

func handleInstallPack(w http.ResponseWriter, r *http.Request, fn func(context.Context, InstallPackRequest) (provision.PackInstallResult, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "install-pack action is not configured"})
		return
	}
	var req InstallPackRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	result, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	providers := make([]map[string]string, 0, len(result.Providers))
	for _, provider := range result.Providers {
		providers = append(providers, map[string]string{
			"provider_id":  provider.ProviderID,
			"display_name": provider.DisplayName,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"pack_id":           result.Pack.PackID,
		"version":           result.Pack.Version,
		"host_version":      result.HostVersion,
		"already_installed": result.AlreadyInstalled,
		"providers":         providers,
	})
}

func handleListPacks(w http.ResponseWriter, r *http.Request, fn func(context.Context) ([]PackInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-packs action is not configured"})
		return
	}
	packs, err := fn(r.Context())
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	if packs == nil {
		packs = []PackInfo{}
	}
	writeJSON(w, http.StatusOK, map[string]any{"packs": packs})
}

func handleListProviderImportBatches(w http.ResponseWriter, r *http.Request, fn func(context.Context, ProviderQueryRequest) ([]ImportBatchInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-provider-import-batches action is not configured"})
		return
	}
	batches, err := fn(r.Context(), ProviderQueryRequest{ProviderID: r.PathValue("providerID"), PackID: r.URL.Query().Get("pack_id"), HostID: strings.TrimSpace(r.URL.Query().Get("host_id"))})
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	if batches == nil {
		batches = []ImportBatchInfo{}
	}
	writeJSON(w, http.StatusOK, map[string]any{"batches": batches})
}

func handleAssignAccessSubscriptions(w http.ResponseWriter, r *http.Request, fn func(context.Context, AssignAccessSubscriptionsRequest) (AssignAccessSubscriptionsResult, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "assign-access-subscriptions action is not configured"})
		return
	}
	var req AssignAccessSubscriptionsRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	req.ProviderID = r.PathValue("providerID")
	result, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, result)
}

func handleAccessPreview(w http.ResponseWriter, r *http.Request, fn func(context.Context, AccessPreviewRequest) (AccessPreviewResult, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "access-preview action is not configured"})
		return
	}
	var req AccessPreviewRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	req.ProviderID = r.PathValue("providerID")
	if req.PackID == "" {
		req.PackID = strings.TrimSpace(r.URL.Query().Get("pack_id"))
	}
	if req.HostID == "" {
		req.HostID = strings.TrimSpace(r.URL.Query().Get("host_id"))
	}
	result, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, result)
}

func handleGetPack(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) (PackInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "get-pack action is not configured"})
		return
	}
	packID := strings.TrimSpace(r.PathValue("packID"))
	if packID == "" {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "pack_id is required"})
		return
	}
	pack, err := fn(r.Context(), packID)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, pack)
}

func handleListPackProviders(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) ([]PackProviderInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-pack-providers action is not configured"})
		return
	}
	packID := strings.TrimSpace(r.PathValue("packID"))
	if packID == "" {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "pack_id is required"})
		return
	}
	providers, err := fn(r.Context(), packID)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	if providers == nil {
		providers = []PackProviderInfo{}
	}
	writeJSON(w, http.StatusOK, map[string]any{"pack_id": packID, "providers": providers})
}

func handleBatchDetail(w http.ResponseWriter, r *http.Request, fn func(context.Context, BatchDetailRequest) (provision.BatchDetailResult, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "batch-detail action is not configured"})
		return
	}
	batchID, err := strconv.ParseInt(r.PathValue("batchID"), 10, 64)
	if err != nil || batchID <= 0 {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "batch_id must be a positive integer"})
		return
	}
	result, err := fn(r.Context(), BatchDetailRequest{BatchID: batchID})
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	items := make([]map[string]any, 0, len(result.Items))
	for _, item := range result.Items {
		items = append(items, map[string]any{
			"id":                 item.ID,
			"batch_id":           item.BatchID,
			"key_fingerprint":    item.KeyFingerprint,
			"account_status":     item.AccountStatus,
			"probe_summary_json": item.ProbeSummaryJSON,
		})
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"batch": map[string]any{
			"id":            result.Batch.ID,
			"host_id":       result.Batch.HostID,
			"pack_id":       result.Batch.PackID,
			"provider_id":   result.Batch.ProviderID,
			"mode":          result.Batch.Mode,
			"batch_status":  result.Batch.BatchStatus,
			"access_status": result.Batch.AccessStatus,
		},
		"items":             items,
		"managed_resources": result.ManagedResources,
		"access_closures":   result.AccessClosures,
		"reconcile_runs":    result.ReconcileRuns,
		"items_count":       len(result.Items),
		"managed_count":     len(result.ManagedResources),
		"access_count":      len(result.AccessClosures),
		"reconcile_count":   len(result.ReconcileRuns),
	})
}

func handleProviderStatus(w http.ResponseWriter, r *http.Request, fn func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "provider-status action is not configured"})
		return
	}
	result, err := fn(r.Context(), ProviderQueryRequest{ProviderID: r.PathValue("providerID"), PackID: strings.TrimSpace(r.URL.Query().Get("pack_id")), HostID: strings.TrimSpace(r.URL.Query().Get("host_id"))})
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"host":                     map[string]any{"host_id": result.Host.HostID, "base_url": result.Host.BaseURL, "host_version": result.Host.HostVersion},
		"pack":                     map[string]any{"pack_id": result.Pack.PackID, "version": result.Pack.Version},
		"provider":                 map[string]any{"provider_id": result.Provider.ProviderID, "display_name": result.Provider.DisplayName, "platform": result.Provider.Platform},
		"batch":                    map[string]any{"id": result.Batch.ID, "batch_status": result.Batch.BatchStatus, "access_status": result.Batch.AccessStatus, "mode": result.Batch.Mode},
		"provider_status":          result.ProviderStatus,
		"latest_access_status":     result.LatestAccessStatus,
		"latest_reconcile_status":  result.LatestReconcileStatus,
		"latest_reconcile_summary": result.LatestReconcileSummary,
		"managed_resources_count":  len(result.ManagedResources),
		"access_closures_count":    len(result.AccessClosures),
		"reconcile_runs_count":     len(result.ReconcileRuns),
	})
}

func handleProviderAccessStatus(w http.ResponseWriter, r *http.Request, fn func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "provider-access-status action is not configured"})
		return
	}
	result, err := fn(r.Context(), ProviderQueryRequest{ProviderID: r.PathValue("providerID"), PackID: strings.TrimSpace(r.URL.Query().Get("pack_id")), HostID: strings.TrimSpace(r.URL.Query().Get("host_id"))})
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	latestClosure := map[string]any{}
	if n := len(result.AccessClosures); n > 0 {
		closure := result.AccessClosures[n-1]
		latestClosure = map[string]any{"id": closure.ID, "closure_type": closure.ClosureType, "status": closure.Status, "details_json": closure.DetailsJSON}
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"provider_id":          result.Provider.ProviderID,
		"pack_id":              result.Pack.PackID,
		"batch_id":             result.Batch.ID,
		"batch_access_status":  result.Batch.AccessStatus,
		"latest_access_status": result.LatestAccessStatus,
		"closures_count":       len(result.AccessClosures),
		"latest_closure":       latestClosure,
	})
}

func handleProviderResources(w http.ResponseWriter, r *http.Request, fn func(context.Context, ProviderQueryRequest) (provision.ProviderSnapshot, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "provider-resources action is not configured"})
		return
	}
	result, err := fn(r.Context(), ProviderQueryRequest{ProviderID: r.PathValue("providerID"), PackID: strings.TrimSpace(r.URL.Query().Get("pack_id")), HostID: strings.TrimSpace(r.URL.Query().Get("host_id"))})
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	resources := make([]map[string]any, 0, len(result.ManagedResources))
	for _, resource := range result.ManagedResources {
		resources = append(resources, map[string]any{"id": resource.ID, "resource_type": resource.ResourceType, "host_resource_id": resource.HostResourceID, "resource_name": resource.ResourceName})
	}
	accessClosures := make([]map[string]any, 0, len(result.AccessClosures))
	for _, closure := range result.AccessClosures {
		accessClosures = append(accessClosures, map[string]any{"id": closure.ID, "closure_type": closure.ClosureType, "status": closure.Status, "details_json": closure.DetailsJSON})
	}
	reconcileRuns := make([]map[string]any, 0, len(result.ReconcileRuns))
	for _, run := range result.ReconcileRuns {
		reconcileRuns = append(reconcileRuns, map[string]any{"id": run.ID, "status": run.Status, "summary_json": run.SummaryJSON})
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"provider_id":     result.Provider.ProviderID,
		"pack_id":         result.Pack.PackID,
		"batch_id":        result.Batch.ID,
		"resources":       resources,
		"access_closures": accessClosures,
		"reconcile_runs":  reconcileRuns,
	})
}

func handlePreviewProvider(w http.ResponseWriter, r *http.Request, fn func(context.Context, PreviewProviderRequest) (provision.PreviewReport, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "preview-provider action is not configured"})
		return
	}
	var req PreviewProviderRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	req.ProviderID = r.PathValue("providerID")
	result, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"accepted_keys_count": len(result.AcceptedKeys),
		"host_overlays":       result.HostOverlays,
		"names":               result.Names,
		"decisions":           result.Decisions,
	})
}

func handleImportProvider(w http.ResponseWriter, r *http.Request, fn func(context.Context, ImportProviderRequest) (provision.RuntimeImportResult, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "import-provider action is not configured"})
		return
	}
	var req ImportProviderRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	req.ProviderID = r.PathValue("providerID")
	result, err := fn(r.Context(), req)
	if err != nil {
		payload := map[string]any{
			"batch_id":            result.BatchID,
			"batch_status":        result.Report.BatchStatus,
			"provider_status":     result.Report.ProviderStatus,
			"access_status":       result.Report.AccessStatus,
			"accepted_keys_count": len(result.Report.AcceptedKeys),
			"accounts_count":      len(result.Report.Accounts),
			"host_overlays":       result.Report.HostOverlays,
			"gateway":             result.Report.Gateway,
			"error":               classifyError(err),
		}
		statusCode := http.StatusConflict
		if result.BatchID == 0 {
			statusCode = classifyError(err).StatusCode
		}
		writeJSON(w, statusCode, payload)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"batch_id":            result.BatchID,
		"batch_status":        result.Report.BatchStatus,
		"provider_status":     result.Report.ProviderStatus,
		"access_status":       result.Report.AccessStatus,
		"accepted_keys_count": len(result.Report.AcceptedKeys),
		"accounts_count":      len(result.Report.Accounts),
		"host_overlays":       result.Report.HostOverlays,
		"group":               result.Report.Group,
		"channel":             result.Report.Channel,
		"plan":                result.Report.Plan,
		"gateway":             result.Report.Gateway,
	})
}

func handleRollbackProvider(w http.ResponseWriter, r *http.Request, fn func(context.Context, RollbackProviderRequest) (provision.RollbackReport, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "rollback-provider action is not configured"})
		return
	}
	var req RollbackProviderRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	req.ProviderID = r.PathValue("providerID")
	result, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"provider_id":      req.ProviderID,
		"deleted_accounts": result.AccountsDeleted,
		"deleted_plans":    result.PlansDeleted,
		"deleted_channels": result.ChannelsDeleted,
		"deleted_groups":   result.GroupsDeleted,
	})
}

func handleRollbackBatch(w http.ResponseWriter, r *http.Request, fn func(context.Context, RollbackBatchRequest) (provision.RollbackReport, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "rollback-batch action is not configured"})
		return
	}
	var req RollbackBatchRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	batchID, err := strconv.ParseInt(strings.TrimSpace(r.PathValue("batchID")), 10, 64)
	if err != nil || batchID <= 0 {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "batch_id must be a positive integer"})
		return
	}
	req.BatchID = batchID
	result, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"batch_id":         req.BatchID,
		"deleted_accounts": result.AccountsDeleted,
		"deleted_plans":    result.PlansDeleted,
		"deleted_channels": result.ChannelsDeleted,
		"deleted_groups":   result.GroupsDeleted,
	})
}

func handleReconcileProvider(w http.ResponseWriter, r *http.Request, fn func(context.Context, ReconcileProviderRequest) (reconcile.Result, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "reconcile-provider action is not configured"})
		return
	}
	var req ReconcileProviderRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	req.ProviderID = r.PathValue("providerID")
	result, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"provider_id":       req.ProviderID,
		"batch_id":          result.BatchID,
		"status":            result.Status,
		"missing_count":     result.MissingCount,
		"extra_count":       result.ExtraCount,
		"stale_noise_count": result.StaleNoiseCount,
		"summary":           result.Summary,
	})
}

func handleListHosts(w http.ResponseWriter, r *http.Request, fn func(context.Context) ([]HostInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-hosts action is not configured"})
		return
	}
	hosts, err := fn(r.Context())
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	if hosts == nil {
		hosts = []HostInfo{}
	}
	writeJSON(w, http.StatusOK, map[string]any{"hosts": hosts})
}

func handleGetHost(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) (HostInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "get-host action is not configured"})
		return
	}
	hostID := strings.TrimSpace(r.PathValue("hostID"))
	if hostID == "" {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "host_id is required"})
		return
	}
	host, err := fn(r.Context(), hostID)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, host)
}

func handleProbeHost(w http.ResponseWriter, r *http.Request, fn func(context.Context, ProbeHostRequest) (HostInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "probe-host action is not configured"})
		return
	}
	var req ProbeHostRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	req.HostID = strings.TrimSpace(r.PathValue("hostID"))
	if req.HostID == "" {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "host_id is required"})
		return
	}
	host, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, host)
}

func handleCreateHost(w http.ResponseWriter, r *http.Request, fn func(context.Context, CreateHostRequest) (HostInfo, error)) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "create-host action is not configured"})
		return
	}
	var req CreateHostRequest
	if err := decodeJSON(r, &req); err != nil {
		writeHTTPError(w, err)
		return
	}
	result, err := fn(r.Context(), req)
	if err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	writeJSON(w, http.StatusOK, result)
}

func handleDeleteHost(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) error) {
	if fn == nil {
		writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "delete-host action is not configured"})
		return
	}
	hostID := strings.TrimSpace(r.PathValue("hostID"))
	if hostID == "" {
		writeHTTPError(w, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "host_id is required"})
		return
	}
	if err := fn(r.Context(), hostID); err != nil {
		writeHTTPError(w, classifyError(err))
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func decodeJSON(r *http.Request, dest any) *httpError {
	if r == nil {
		return &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "request is required"}
	}
	r.Body = http.MaxBytesReader(nil, r.Body, maxJSONBodyBytes)
	decoder := json.NewDecoder(r.Body)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(dest); err != nil {
		var maxBytesErr *http.MaxBytesError
		if errors.As(err, &maxBytesErr) {
			return &httpError{StatusCode: http.StatusRequestEntityTooLarge, Code: "request_too_large", Message: fmt.Sprintf("request body exceeds %d bytes", maxJSONBodyBytes)}
		}
		return &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: fmt.Sprintf("decode request body: %v", err)}
	}
	if err := decoder.Decode(&struct{}{}); err != nil && !errors.Is(err, io.EOF) {
		return &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "request body must contain a single JSON object"}
	}
	return nil
}

func writeHTTPError(w http.ResponseWriter, err *httpError) {
	if err == nil {
		err = &httpError{StatusCode: http.StatusInternalServerError, Code: "internal_error", Message: "internal server error"}
	}
	writeJSON(w, err.StatusCode, map[string]any{"error": err})
}

func writeJSON(w http.ResponseWriter, statusCode int, body any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statusCode)
	_ = json.NewEncoder(w).Encode(body)
}

func classifyError(err error) *httpError {
	if err == nil {
		return nil
	}
	var requestErr *httpError
	if errors.As(err, &requestErr) {
		return requestErr
	}
	var upstreamErr *sub2api.HTTPError
	if errors.As(err, &upstreamErr) {
		return &httpError{StatusCode: http.StatusBadGateway, Code: "host_request_failed", Message: err.Error(), UpstreamStatus: upstreamErr.StatusCode}
	}
	var hostDeleteBlocker *sqlite.HostDeleteBlocker
	if errors.As(err, &hostDeleteBlocker) {
		return &httpError{StatusCode: http.StatusConflict, Code: "host_in_use", Message: err.Error()}
	}
	message := err.Error()
	switch {
	case strings.Contains(message, "already installed") || strings.Contains(message, "checksum drift"):
		return &httpError{StatusCode: http.StatusConflict, Code: "pack_conflict", Message: message}
	case strings.Contains(message, "repo root"):
		return &httpError{StatusCode: http.StatusServiceUnavailable, Code: "publish_unavailable", Message: message}
	case strings.Contains(message, "run import again before reconcile"):
		return &httpError{StatusCode: http.StatusConflict, Code: "batch_not_reconcilable", Message: message}
	case strings.Contains(message, "not found in pack"):
		return &httpError{StatusCode: http.StatusBadRequest, Code: "provider_not_found", Message: message}
	case strings.Contains(message, "not found"):
		return &httpError{StatusCode: http.StatusNotFound, Code: "not_found", Message: message}
	case strings.Contains(message, "pack path") || strings.Contains(message, "pack dir") || strings.Contains(message, "required") || strings.Contains(message, "decode"):
		return &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: message}
	default:
		return &httpError{StatusCode: http.StatusInternalServerError, Code: "internal_error", Message: message}
	}
}

func NewActionSet(sqliteDSN string) ActionSet {
	return NewActionSetWithStickyRuntime(sqliteDSN, defaultStickyStoreRuntime())
}

func NewActionSetWithStickyRuntime(sqliteDSN string, stickyRuntime stickyStoreRuntime) ActionSet {
	routeLogWriter := newLazyRouteLogWriter(sqliteDSN)
	resolveRoute := buildResolveRouteAction(sqliteDSN, stickyRuntime, routeLogWriter)
	proxyRouteChatCompletions := buildProxyRouteChatCompletionsAction(sqliteDSN, resolveRoute, routeLogWriter)
	routeChatCompletions := buildRouteChatCompletionsAction(proxyRouteChatCompletions)
	return ActionSet{
		CreateBatchImportRun:                buildCreateBatchImportRunAction(sqliteDSN),
		ListBatchImportRuns:                 buildListBatchImportRunsAction(sqliteDSN),
		GetBatchImportRun:                   buildGetBatchImportRunAction(sqliteDSN),
		ListBatchImportRunItems:             buildListBatchImportRunItemsAction(sqliteDSN),
		GetBatchImportRunItem:               buildGetBatchImportRunItemAction(sqliteDSN),
		ListPortalLogicalGroups:             buildListPortalLogicalGroupsAction(sqliteDSN),
		GetPortalLogicalGroup:               buildGetPortalLogicalGroupAction(sqliteDSN),
		ListPortalLogicalGroupModels:        buildListPortalLogicalGroupModelsAction(sqliteDSN),
		CreateLogicalGroup:                  buildCreateLogicalGroupAction(sqliteDSN),
		ListLogicalGroups:                   buildListLogicalGroupsAction(sqliteDSN),
		GetLogicalGroup:                     buildGetLogicalGroupAction(sqliteDSN),
		UpdateLogicalGroup:                  buildUpdateLogicalGroupAction(sqliteDSN),
		DeleteLogicalGroup:                  buildDeleteLogicalGroupAction(sqliteDSN),
		CreateLogicalGroupModel:             buildCreateLogicalGroupModelAction(sqliteDSN),
		ListLogicalGroupModels:              buildListLogicalGroupModelsAction(sqliteDSN),
		DeleteLogicalGroupModel:             buildDeleteLogicalGroupModelAction(sqliteDSN),
		CreateLogicalGroupRoute:             buildCreateLogicalGroupRouteAction(sqliteDSN),
		ListLogicalGroupRoutes:              buildListLogicalGroupRoutesAction(sqliteDSN),
		UpdateLogicalGroupRoute:             buildUpdateLogicalGroupRouteAction(sqliteDSN),
		DeleteLogicalGroupRoute:             buildDeleteLogicalGroupRouteAction(sqliteDSN),
		CreateLogicalGroupRouteModel:        buildCreateLogicalGroupRouteModelAction(sqliteDSN),
		ListLogicalGroupRouteModels:         buildListLogicalGroupRouteModelsAction(sqliteDSN),
		AppendRouteDecisionLog:              buildAppendRouteDecisionLogAction(routeLogWriter, sqliteDSN),
		ListRouteDecisionLogs:               buildListRouteDecisionLogsAction(sqliteDSN),
		AppendRouteFailoverEvent:            buildAppendRouteFailoverEventAction(routeLogWriter, sqliteDSN),
		ListRouteFailoverEvents:             buildListRouteFailoverEventsAction(sqliteDSN),
		AppendRouteStickyAudit:              buildAppendRouteStickyAuditAction(routeLogWriter, sqliteDSN),
		ListRouteStickyAudit:                buildListRouteStickyAuditAction(sqliteDSN),
		ListRouteHealth:                     buildListRouteHealthAction(sqliteDSN, stickyRuntime),
		ResolveRoute:                        resolveRoute,
		RouteChatCompletions:                routeChatCompletions,
		ProxyRouteChatCompletions:           proxyRouteChatCompletions,
		SetStickyBinding:                    buildSetStickyBindingAction(stickyRuntime),
		GetStickyBinding:                    buildGetStickyBindingAction(stickyRuntime),
		SetRouteFailure:                     buildSetRouteFailureAction(stickyRuntime),
		GetRouteFailure:                     buildGetRouteFailureAction(stickyRuntime),
		SetRouteCooldown:                    buildSetRouteCooldownAction(stickyRuntime),
		GetRouteCooldown:                    buildGetRouteCooldownAction(stickyRuntime),
		ListProviderAccounts:                buildListProviderAccountsAction(sqliteDSN),
		GetProviderAccountBindingCandidates: buildGetProviderAccountBindingCandidatesAction(sqliteDSN),
		UpdateProviderAccountBinding:        buildUpdateProviderAccountBindingAction(sqliteDSN),
		EnableProviderAccount:               buildUpdateProviderAccountStatusAction(sqliteDSN, sqlite.ProviderAccountStatusActive),
		DisableProviderAccount:              buildUpdateProviderAccountStatusAction(sqliteDSN, sqlite.ProviderAccountStatusDisabled),
		RetireProviderAccount:               buildUpdateProviderAccountStatusAction(sqliteDSN, sqlite.ProviderAccountStatusDeprecated),
		CreateProviderDraft: func(ctx context.Context, req CreateProviderDraftRequest) (ProviderDraftInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return ProviderDraftInfo{}, err
			}
			defer store.Close()

			draftID := strings.TrimSpace(req.DraftID)
			if draftID == "" {
				draftID = fmt.Sprintf("draft_%d", time.Now().UnixNano())
			}
			manifestJSON, manifestValue, supportedModels, err := normalizeProviderDraftPayload(req)
			if err != nil {
				return ProviderDraftInfo{}, err
			}
			if err := validateProviderDraftModelConflicts(ctx, store, strings.TrimSpace(req.PackID), strings.TrimSpace(req.ProviderID), "", supportedModels, strings.TrimSpace(req.SmokeTestModel)); err != nil {
				return ProviderDraftInfo{}, err
			}

			draftRow := sqlite.ProviderDraft{
				DraftID:             draftID,
				PackID:              strings.TrimSpace(req.PackID),
				ProviderID:          strings.TrimSpace(req.ProviderID),
				DisplayName:         strings.TrimSpace(req.DisplayName),
				Platform:            strings.TrimSpace(req.Platform),
				BaseURL:             strings.TrimSpace(req.BaseURL),
				SmokeTestModel:      strings.TrimSpace(req.SmokeTestModel),
				SupportedModelsJSON: encodeStringList(supportedModels),
				ManifestJSON:        manifestJSON,
				SourceHostID:        strings.TrimSpace(req.SourceHostID),
				Notes:               strings.TrimSpace(req.Notes),
			}
			if _, err := store.ProviderDrafts().Create(ctx, draftRow); err != nil {
				return ProviderDraftInfo{}, err
			}
			persisted, err := store.ProviderDrafts().GetByDraftID(ctx, draftID)
			if err != nil {
				return ProviderDraftInfo{}, err
			}
			return providerDraftRecordToInfo(persisted, manifestValue, supportedModels)
		},
		ListProviderDrafts: func(ctx context.Context, req ListProviderDraftsRequest) ([]ProviderDraftInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return nil, err
			}
			defer store.Close()

			rows, err := store.ProviderDrafts().List(ctx, sqlite.ListProviderDraftsFilter{
				PackID:     req.PackID,
				ProviderID: req.ProviderID,
				Query:      req.Query,
			})
			if err != nil {
				return nil, err
			}
			result := make([]ProviderDraftInfo, 0, len(rows))
			for _, row := range rows {
				info, err := providerDraftRecordToInfoFromStored(row)
				if err != nil {
					return nil, err
				}
				result = append(result, info)
			}
			return result, nil
		},
		GetProviderDraft: func(ctx context.Context, draftID string) (ProviderDraftInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return ProviderDraftInfo{}, err
			}
			defer store.Close()

			row, err := store.ProviderDrafts().GetByDraftID(ctx, draftID)
			if err != nil {
				return ProviderDraftInfo{}, err
			}
			return providerDraftRecordToInfoFromStored(row)
		},
		UpdateProviderDraft: func(ctx context.Context, req UpdateProviderDraftRequest) (ProviderDraftInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return ProviderDraftInfo{}, err
			}
			defer store.Close()

			manifestJSON, _, supportedModels, err := normalizeProviderDraftPayload(req.CreateProviderDraftRequest)
			if err != nil {
				return ProviderDraftInfo{}, err
			}
			if err := validateProviderDraftModelConflicts(ctx, store, strings.TrimSpace(req.PackID), strings.TrimSpace(req.ProviderID), strings.TrimSpace(req.DraftID), supportedModels, strings.TrimSpace(req.SmokeTestModel)); err != nil {
				return ProviderDraftInfo{}, err
			}

			if err := store.ProviderDrafts().UpdateByDraftID(ctx, sqlite.ProviderDraft{
				DraftID:             strings.TrimSpace(req.DraftID),
				PackID:              strings.TrimSpace(req.PackID),
				ProviderID:          strings.TrimSpace(req.ProviderID),
				DisplayName:         strings.TrimSpace(req.DisplayName),
				Platform:            strings.TrimSpace(req.Platform),
				BaseURL:             strings.TrimSpace(req.BaseURL),
				SmokeTestModel:      strings.TrimSpace(req.SmokeTestModel),
				SupportedModelsJSON: encodeStringList(supportedModels),
				ManifestJSON:        manifestJSON,
				SourceHostID:        strings.TrimSpace(req.SourceHostID),
				Notes:               strings.TrimSpace(req.Notes),
			}); err != nil {
				return ProviderDraftInfo{}, err
			}
			row, err := store.ProviderDrafts().GetByDraftID(ctx, req.DraftID)
			if err != nil {
				return ProviderDraftInfo{}, err
			}
			return providerDraftRecordToInfoFromStored(row)
		},
		DeleteProviderDraft: func(ctx context.Context, draftID string) error {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return err
			}
			defer store.Close()
			return store.ProviderDrafts().DeleteByDraftID(ctx, draftID)
		},
		PublishProviderDraft: func(ctx context.Context, req PublishProviderDraftRequest) (PublishProviderDraftResult, error) {
			startupCfg, err := config.LoadStartupFromEnv()
			if err != nil {
				return PublishProviderDraftResult{}, err
			}
			if strings.TrimSpace(startupCfg.Repository.RepoRoot) == "" {
				return PublishProviderDraftResult{}, fmt.Errorf("pack repo root is not configured")
			}

			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return PublishProviderDraftResult{}, err
			}
			defer store.Close()

			row, err := store.ProviderDrafts().GetByDraftID(ctx, req.DraftID)
			if err != nil {
				return PublishProviderDraftResult{}, err
			}
			manifest, err := buildPublishedProviderManifest(row)
			if err != nil {
				return PublishProviderDraftResult{}, err
			}
			if err := validateProviderDraftModelConflicts(ctx, store, strings.TrimSpace(row.PackID), strings.TrimSpace(manifest.ProviderID), strings.TrimSpace(row.DraftID), manifest.DefaultModels, strings.TrimSpace(manifest.SmokeTestModel)); err != nil {
				return PublishProviderDraftResult{}, err
			}
			publishResult, err := pack.PublishProviderManifest(ctx, pack.PublishProviderManifestRequest{
				RepoRoot:      startupCfg.Repository.RepoRoot,
				PackID:        row.PackID,
				Manifest:      manifest,
				CommitMessage: strings.TrimSpace(req.CommitMessage),
			})
			if err != nil {
				return PublishProviderDraftResult{}, err
			}
			return PublishProviderDraftResult{
				DraftID:           row.DraftID,
				PackID:            publishResult.PackID,
				ProviderID:        publishResult.ProviderID,
				ProviderPath:      publishResult.ProviderPath,
				PackVersionBefore: publishResult.PackVersionBefore,
				PackVersionAfter:  publishResult.PackVersionAfter,
				PublishMode:       publishResult.PublishMode,
				CommitMessage:     publishResult.CommitMessage,
				CommitSHA:         publishResult.CommitSHA,
				RepoRoot:          publishResult.RepoRoot,
			}, nil
		},
		InstallPack: func(ctx context.Context, req InstallPackRequest) (provision.PackInstallResult, error) {
			loadedPack, err := pack.LoadPath(req.PackPath)
			if err != nil {
				return provision.PackInstallResult{}, err
			}
			client, err := sub2api.NewClient(req.HostBaseURL, sub2api.WithAPIKey(req.HostAPIKey), sub2api.WithBearerToken(req.HostBearerToken))
			if err != nil {
				return provision.PackInstallResult{}, err
			}
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return provision.PackInstallResult{}, err
			}
			defer store.Close()
			service := provision.NewPackInstallService(store, client)
			return service.Install(ctx, provision.PackInstallRequest{Pack: loadedPack})
		},
		BatchDetail: func(ctx context.Context, req BatchDetailRequest) (provision.BatchDetailResult, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return provision.BatchDetailResult{}, err
			}
			defer store.Close()
			return provision.NewBatchDetailService(store).Get(ctx, req.BatchID)
		},
		GetProviderStatus: func(ctx context.Context, req ProviderQueryRequest) (provision.ProviderSnapshot, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return provision.ProviderSnapshot{}, err
			}
			defer store.Close()
			return provision.NewProviderStatusService(store).GetStatus(ctx, provision.ProviderQuery{ProviderID: req.ProviderID, PackID: req.PackID, HostID: req.HostID})
		},
		GetProviderResources: func(ctx context.Context, req ProviderQueryRequest) (provision.ProviderSnapshot, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return provision.ProviderSnapshot{}, err
			}
			defer store.Close()
			return provision.NewProviderStatusService(store).GetResources(ctx, provision.ProviderQuery{ProviderID: req.ProviderID, PackID: req.PackID, HostID: req.HostID})
		},
		GetProviderAccessStatus: func(ctx context.Context, req ProviderQueryRequest) (provision.ProviderSnapshot, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return provision.ProviderSnapshot{}, err
			}
			defer store.Close()
			return provision.NewProviderStatusService(store).GetStatus(ctx, provision.ProviderQuery{ProviderID: req.ProviderID, PackID: req.PackID, HostID: req.HostID})
		},
		ListProviderImportBatches: func(ctx context.Context, req ProviderQueryRequest) ([]ImportBatchInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return nil, err
			}
			defer store.Close()
			providers, err := resolveProvidersForQuery(ctx, store, req)
			if err != nil {
				return nil, err
			}
			batches := make([]ImportBatchInfo, 0)
			for _, providerRow := range providers {
				var rows []sqlite.ImportBatch
				if strings.TrimSpace(req.HostID) != "" {
					hostRow, err := store.Hosts().GetByHostID(ctx, req.HostID)
					if err != nil {
						return nil, err
					}
					rows, err = store.ImportBatches().ListByProviderIDAndHostID(ctx, providerRow.ID, hostRow.ID)
					if err != nil {
						return nil, err
					}
				} else {
					rows, err = store.ImportBatches().ListByProviderID(ctx, providerRow.ID)
					if err != nil {
						return nil, err
					}
					if len(rows) > 1 {
						firstHostID := rows[0].HostID
						for _, row := range rows[1:] {
							if row.HostID != firstHostID {
								return nil, fmt.Errorf("provider exists on multiple hosts; host_id is required")
							}
						}
					}
				}
				for _, batch := range rows {
					batches = append(batches, ImportBatchInfo{BatchID: batch.ID, BatchStatus: batch.BatchStatus, AccessStatus: batch.AccessStatus})
				}
			}
			return batches, nil
		},
		PreviewProvider: func(ctx context.Context, req PreviewProviderRequest) (provision.PreviewReport, error) {
			loadedPack, err := pack.LoadPath(req.PackPath)
			if err != nil {
				return provision.PreviewReport{}, err
			}
			providerManifest, err := findProvider(loadedPack, req.ProviderID)
			if err != nil {
				return provision.PreviewReport{}, err
			}
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return provision.PreviewReport{}, err
			}
			defer store.Close()
			_, client, err := resolveManagedHost(ctx, store, req.HostID, req.HostBaseURL, createHostAuthFromLegacyFields(req.HostAPIKey, req.HostBearerToken))
			if err != nil {
				return provision.PreviewReport{}, err
			}
			hostVersion, err := client.GetHostVersion(ctx)
			if err != nil {
				return provision.PreviewReport{}, err
			}
			service := provision.NewPreviewService(client)
			return service.PreviewImport(ctx, provision.PreviewRequest{
				TargetHost:  loadedPack.Manifest.TargetHost,
				HostVersion: hostVersion,
				Provider:    providerManifest,
				Mode:        req.Mode,
				Keys:        req.Keys,
			})
		},
		ImportProvider: func(ctx context.Context, req ImportProviderRequest) (provision.RuntimeImportResult, error) {
			loadedPack, err := pack.LoadPath(req.PackPath)
			if err != nil {
				return provision.RuntimeImportResult{}, err
			}
			providerManifest, err := findProvider(loadedPack, req.ProviderID)
			if err != nil {
				return provision.RuntimeImportResult{}, err
			}
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return provision.RuntimeImportResult{}, err
			}
			defer store.Close()
			hostRow, client, err := resolveManagedHost(ctx, store, req.HostID, req.HostBaseURL, createHostAuthFromLegacyFields(req.HostAPIKey, req.HostBearerToken))
			if err != nil {
				return provision.RuntimeImportResult{}, err
			}
			subscriptions := make([]provision.SubscriptionTarget, 0, len(req.SubscriptionUsers))
			for _, userID := range req.SubscriptionUsers {
				subscriptions = append(subscriptions, provision.SubscriptionTarget{UserID: userID, DurationDays: req.SubscriptionDays})
			}
			service := provision.NewRuntimeImportService(store, client)
			return service.Import(ctx, provision.RuntimeImportRequest{
				HostID:      hostRow.HostID,
				HostBaseURL: hostRow.BaseURL,
				Pack:        loadedPack,
				Provider:    providerManifest,
				Mode:        req.Mode,
				Keys:        req.Keys,
				Access: provision.AccessRequest{
					Mode:          req.AccessMode,
					ProbeAPIKey:   req.AccessAPIKey,
					Subscriptions: subscriptions,
				},
			})
		},
		RollbackProvider: func(ctx context.Context, req RollbackProviderRequest) (provision.RollbackReport, error) {
			loadedPack, err := pack.LoadPath(req.PackPath)
			if err != nil {
				return provision.RollbackReport{}, err
			}
			providerManifest, err := findProvider(loadedPack, req.ProviderID)
			if err != nil {
				return provision.RollbackReport{}, err
			}
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return provision.RollbackReport{}, err
			}
			defer store.Close()
			hostRow, client, err := resolveManagedHost(ctx, store, req.HostID, req.HostBaseURL, createHostAuthFromLegacyFields(req.HostAPIKey, req.HostBearerToken))
			if err != nil {
				return provision.RollbackReport{}, err
			}

			packRow, err := store.Packs().GetByPackID(ctx, loadedPack.Manifest.PackID)
			if err != nil {
				return provision.RollbackReport{}, err
			}
			providerRow, err := store.Providers().GetByPackIDAndProviderID(ctx, packRow.ID, providerManifest.ProviderID)
			if err != nil {
				return provision.RollbackReport{}, err
			}
			batch, err := store.ImportBatches().GetLatestByProviderIDAndHostID(ctx, providerRow.ID, hostRow.ID)
			if err != nil {
				return provision.RollbackReport{}, fmt.Errorf("find latest batch for provider %q on host %q: %w", providerManifest.ProviderID, hostRow.HostID, err)
			}
			managedResources, err := store.ManagedResources().GetByBatchID(ctx, batch.ID)
			if err != nil {
				return provision.RollbackReport{}, err
			}
			if len(managedResources) == 0 {
				return provision.RollbackReport{}, fmt.Errorf("rollback requires stored managed resources for provider %q on host %q", providerManifest.ProviderID, hostRow.HostID)
			}
			service := provision.NewRollbackService(client)
			report, rollbackErr := service.RollbackStoredResources(ctx, managedResources)
			if rollbackErr != nil {
				_ = store.ImportBatches().UpdateStatus(ctx, batch.ID, provision.BatchStatusFailed, batch.AccessStatus)
				return report, rollbackErr
			}
			if err := store.ImportBatches().UpdateStatus(ctx, batch.ID, provision.BatchStatusRolledBack, batch.AccessStatus); err != nil {
				return report, fmt.Errorf("rollback resources succeeded but update batch status: %w", err)
			}
			return report, nil
		},
		RollbackBatch: func(ctx context.Context, req RollbackBatchRequest) (provision.RollbackReport, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return provision.RollbackReport{}, err
			}
			defer store.Close()
			batch, err := store.ImportBatches().GetByID(ctx, req.BatchID)
			if err != nil {
				return provision.RollbackReport{}, err
			}
			hostRow, err := store.Hosts().GetByID(ctx, batch.HostID)
			if err != nil {
				return provision.RollbackReport{}, err
			}
			client, err := newSub2APIClient(hostRow.BaseURL, authFromStoredHost(hostRow))
			if err != nil {
				return provision.RollbackReport{}, err
			}
			managedResources, err := store.ManagedResources().GetByBatchID(ctx, batch.ID)
			if err != nil {
				return provision.RollbackReport{}, err
			}
			service := provision.NewRollbackService(client)
			report, rollbackErr := service.RollbackStoredResources(ctx, managedResources)
			if rollbackErr != nil {
				_ = store.ImportBatches().UpdateStatus(ctx, batch.ID, provision.BatchStatusFailed, batch.AccessStatus)
				return report, rollbackErr
			}
			if err := store.ImportBatches().UpdateStatus(ctx, batch.ID, provision.BatchStatusRolledBack, batch.AccessStatus); err != nil {
				return report, fmt.Errorf("rollback resources succeeded but update batch status: %w", err)
			}
			return report, nil
		},
		ReconcileProvider: func(ctx context.Context, req ReconcileProviderRequest) (reconcile.Result, error) {
			loadedPack, err := pack.LoadPath(req.PackPath)
			if err != nil {
				return reconcile.Result{}, err
			}
			providerManifest, err := findProvider(loadedPack, req.ProviderID)
			if err != nil {
				return reconcile.Result{}, err
			}
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return reconcile.Result{}, err
			}
			defer store.Close()
			hostRow, client, err := resolveManagedHost(ctx, store, req.HostID, req.HostBaseURL, createHostAuthFromLegacyFields(req.HostAPIKey, req.HostBearerToken))
			if err != nil {
				return reconcile.Result{}, err
			}
			service := reconcile.NewService(store, client)
			return service.Reconcile(ctx, reconcile.Request{HostID: hostRow.HostID, HostBaseURL: hostRow.BaseURL, AccessProbeAPIKey: req.AccessAPIKey, Pack: loadedPack, Provider: providerManifest})
		},
		CreateHost: func(ctx context.Context, req CreateHostRequest) (HostInfo, error) {
			if strings.TrimSpace(req.BaseURL) == "" {
				return HostInfo{}, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "base_url is required"}
			}
			client, err := newSub2APIClient(req.BaseURL, req.Auth)
			if err != nil {
				return HostInfo{}, err
			}

			hostVersion, capabilities, err := probeHostSnapshot(ctx, client)
			if err != nil {
				return HostInfo{}, err
			}
			capabilityJSON, err := json.Marshal(capabilities)
			if err != nil {
				return HostInfo{}, fmt.Errorf("marshal capabilities: %w", err)
			}

			name := strings.TrimSpace(req.Name)
			if name == "" {
				name = req.BaseURL
			}

			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return HostInfo{}, err
			}
			defer store.Close()
			if existing, err := store.Hosts().GetByBaseURL(ctx, req.BaseURL); err == nil && existing.HostID != name {
				return HostInfo{}, &httpError{StatusCode: http.StatusConflict, Code: "host_conflict", Message: fmt.Sprintf("base_url %s already registered as host_id %s", req.BaseURL, existing.HostID)}
			}
			hostRecord := sqlite.Host{HostID: name, BaseURL: req.BaseURL, HostVersion: hostVersion, CapabilityProbeJSON: string(capabilityJSON), AuthType: req.Auth.Type, AuthToken: req.Auth.Token}
			if _, err := store.Hosts().GetByHostID(ctx, name); err == nil {
				if err := store.Hosts().UpdateConnectionByHostID(ctx, name, req.BaseURL, hostVersion, string(capabilityJSON), req.Auth.Type, req.Auth.Token); err != nil {
					return HostInfo{}, fmt.Errorf("update host: %w", err)
				}
			} else {
				if _, err := store.Hosts().Create(ctx, hostRecord); err != nil {
					return HostInfo{}, fmt.Errorf("save host: %w", err)
				}
			}
			stored, err := store.Hosts().GetByHostID(ctx, name)
			if err != nil {
				return HostInfo{}, err
			}
			return hostRecordToInfo(stored), nil
		},
		ProbeHost: func(ctx context.Context, req ProbeHostRequest) (HostInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return HostInfo{}, err
			}
			defer store.Close()
			hostRow, err := store.Hosts().GetByHostID(ctx, req.HostID)
			if err != nil {
				return HostInfo{}, err
			}
			client, err := newSub2APIClient(hostRow.BaseURL, authFromStoredHost(hostRow))
			if err != nil {
				return HostInfo{}, err
			}
			hostVersion, capabilities, err := probeHostSnapshot(ctx, client)
			if err != nil {
				return HostInfo{}, err
			}
			capabilityJSON, err := json.Marshal(capabilities)
			if err != nil {
				return HostInfo{}, fmt.Errorf("marshal capabilities: %w", err)
			}
			if err := store.Hosts().UpdateProbeByHostID(ctx, req.HostID, hostVersion, string(capabilityJSON)); err != nil {
				return HostInfo{}, err
			}
			return HostInfo{HostID: hostRow.HostID, BaseURL: hostRow.BaseURL, HostVersion: hostVersion, Status: hostSupportStatus(capabilities), Capabilities: &capabilities}, nil
		},
		ListHosts: func(ctx context.Context) ([]HostInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return nil, err
			}
			defer store.Close()
			hosts, err := store.Hosts().ListAll(ctx)
			if err != nil {
				return nil, err
			}
			result := make([]HostInfo, 0, len(hosts))
			for _, host := range hosts {
				result = append(result, hostRecordToInfo(host))
			}
			return result, nil
		},
		GetHost: func(ctx context.Context, hostID string) (HostInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return HostInfo{}, err
			}
			defer store.Close()
			host, err := store.Hosts().GetByHostID(ctx, hostID)
			if err != nil {
				return HostInfo{}, err
			}
			return hostRecordToInfo(host), nil
		},
		DeleteHost: func(ctx context.Context, hostID string) error {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return err
			}
			defer store.Close()
			if err := store.Hosts().DeleteByHostID(ctx, hostID); err != nil {
				return classifyError(err)
			}
			return nil
		},
		ListPacks: func(ctx context.Context) ([]PackInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return nil, err
			}
			defer store.Close()
			packs, err := store.Packs().ListAll(ctx)
			if err != nil {
				return nil, err
			}
			result := make([]PackInfo, 0, len(packs))
			for _, p := range packs {
				result = append(result, packRecordToInfo(p))
			}
			return result, nil
		},
		GetPack: func(ctx context.Context, packID string) (PackInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return PackInfo{}, err
			}
			defer store.Close()
			pack, err := store.Packs().GetByPackID(ctx, packID)
			if err != nil {
				return PackInfo{}, err
			}
			return packRecordToInfo(pack), nil
		},
		ListPackProviders: func(ctx context.Context, packID string) ([]PackProviderInfo, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return nil, err
			}
			defer store.Close()
			packRow, err := store.Packs().GetByPackID(ctx, packID)
			if err != nil {
				return nil, err
			}
			providers, err := store.Providers().ListByPackID(ctx, packRow.ID)
			if err != nil {
				return nil, err
			}
			result := make([]PackProviderInfo, 0, len(providers))
			for _, p := range providers {
				hostOverlays := 0
				baseURL := ""
				smokeTestModel := ""
				supportedModels := []string{}
				if strings.TrimSpace(p.ManifestJSON) != "" {
					var providerManifest pack.ProviderManifest
					if err := json.Unmarshal([]byte(p.ManifestJSON), &providerManifest); err != nil {
						return nil, fmt.Errorf("decode stored provider manifest: %w", err)
					}
					hostOverlays = len(providerManifest.HostOverlays)
					baseURL = strings.TrimSpace(providerManifest.BaseURL)
					smokeTestModel = strings.TrimSpace(providerManifest.SmokeTestModel)
					supportedModels = append([]string(nil), providerManifest.DefaultModels...)
				}
				result = append(result, PackProviderInfo{
					ProviderID:      p.ProviderID,
					DisplayName:     p.DisplayName,
					Platform:        p.Platform,
					HostOverlays:    hostOverlays,
					BaseURL:         baseURL,
					SmokeTestModel:  smokeTestModel,
					SupportedModels: supportedModels,
				})
			}
			return result, nil
		},
		AssignAccessSubscriptions: func(ctx context.Context, req AssignAccessSubscriptionsRequest) (AssignAccessSubscriptionsResult, error) {
			loadedPack, err := pack.LoadPath(req.PackPath)
			if err != nil {
				return AssignAccessSubscriptionsResult{}, err
			}
			providerManifest, err := findProvider(loadedPack, req.ProviderID)
			if err != nil {
				return AssignAccessSubscriptionsResult{}, err
			}
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return AssignAccessSubscriptionsResult{}, err
			}
			defer store.Close()
			hostRow, client, err := resolveManagedHost(ctx, store, req.HostID, req.HostBaseURL, createHostAuthFromLegacyFields(req.HostAPIKey, req.HostBearerToken))
			if err != nil {
				return AssignAccessSubscriptionsResult{}, err
			}

			packRow, err := store.Packs().GetByPackID(ctx, loadedPack.Manifest.PackID)
			if err != nil {
				return AssignAccessSubscriptionsResult{}, err
			}
			providerRow, err := store.Providers().GetByPackIDAndProviderID(ctx, packRow.ID, providerManifest.ProviderID)
			if err != nil {
				return AssignAccessSubscriptionsResult{}, fmt.Errorf("provider %q not found in pack %q", req.ProviderID, loadedPack.Manifest.PackID)
			}
			batch, err := store.ImportBatches().GetLatestByProviderIDAndHostID(ctx, providerRow.ID, hostRow.ID)
			if err != nil {
				return AssignAccessSubscriptionsResult{}, fmt.Errorf("find batch for provider on host: %w", err)
			}

			resources, err := store.ManagedResources().GetByBatchID(ctx, batch.ID)
			if err != nil {
				return AssignAccessSubscriptionsResult{}, err
			}
			groupID := ""
			for _, r := range resources {
				if r.ResourceType == "group" {
					groupID = r.HostResourceID
					break
				}
			}
			if groupID == "" {
				return AssignAccessSubscriptionsResult{}, fmt.Errorf("no group found for provider batch")
			}

			subscriptions := make([]access.SubscriptionTarget, 0, len(req.SubscriptionUsers))
			for _, userID := range req.SubscriptionUsers {
				subscriptions = append(subscriptions, access.SubscriptionTarget{UserID: userID, DurationDays: req.SubscriptionDays})
			}

			accessSvc := access.NewService(client)
			gwResult, err := accessSvc.Close(ctx, access.ClosureRequest{Mode: access.ModeSubscription, ProbeAPIKey: req.AccessAPIKey, Subscriptions: subscriptions, GroupID: groupID, ExpectedModel: providerManifest.SmokeTestModel, Prompt: "ping", MaxTokens: 8})
			if err != nil {
				return AssignAccessSubscriptionsResult{}, err
			}

			accessStatus := deriveAccessStatus(gwResult)
			accessPayload, _ := json.Marshal(provision.BuildAccessClosureDetails(provision.AccessRequest{
				Mode:          provision.AccessModeSubscription,
				ProbeAPIKey:   req.AccessAPIKey,
				Subscriptions: subscriptionTargets(req.SubscriptionUsers, req.SubscriptionDays),
			}, gwResult))
			if _, err := store.AccessClosures().Create(ctx, sqlite.AccessClosureRecord{BatchID: batch.ID, ClosureType: access.ModeSubscription, Status: accessStatus, DetailsJSON: string(accessPayload)}); err != nil {
				return AssignAccessSubscriptionsResult{}, fmt.Errorf("record access closure: %w", err)
			}
			if err := store.ImportBatches().UpdateStatus(ctx, batch.ID, batch.BatchStatus, accessStatus); err != nil {
				return AssignAccessSubscriptionsResult{}, fmt.Errorf("update batch access status: %w", err)
			}

			return AssignAccessSubscriptionsResult{ProviderID: req.ProviderID, Assigned: len(req.SubscriptionUsers), AccessStatus: accessStatus}, nil
		},
		AccessPreview: func(ctx context.Context, req AccessPreviewRequest) (AccessPreviewResult, error) {
			store, err := sqlite.Open(ctx, sqliteDSN)
			if err != nil {
				return AccessPreviewResult{}, err
			}
			defer store.Close()

			providers, err := resolveProvidersForQuery(ctx, store, ProviderQueryRequest{ProviderID: req.ProviderID, PackID: req.PackID, HostID: req.HostID})
			if err != nil {
				return AccessPreviewResult{}, err
			}
			if len(providers) == 0 {
				return AccessPreviewResult{}, fmt.Errorf("provider %q not found", req.ProviderID)
			}
			if len(providers) > 1 {
				return AccessPreviewResult{}, fmt.Errorf("provider %q exists in multiple packs; pack_id is required", req.ProviderID)
			}
			providerRow := providers[0]
			latestStatus, err := resolveLatestAccessStatus(ctx, store, providerRow, req.HostID)
			if err != nil {
				return AccessPreviewResult{}, fmt.Errorf("find batch for provider: %w", err)
			}
			available := accessStatusSupportsMode(latestStatus, req.Mode)
			message := fmt.Sprintf("latest access status: %s", latestStatus)
			if !available {
				message = fmt.Sprintf("access status %s does not satisfy mode %s", latestStatus, req.Mode)
			}

			return AccessPreviewResult{ProviderID: req.ProviderID, Mode: req.Mode, Available: available, Message: message}, nil
		},
	}
}

func findProvider(loaded pack.LoadedPack, providerID string) (pack.ProviderManifest, error) {
	for _, provider := range loaded.Providers {
		if provider.ProviderID == strings.TrimSpace(providerID) {
			return provider, nil
		}
	}
	return pack.ProviderManifest{}, fmt.Errorf("provider %q not found in pack %q", providerID, loaded.Manifest.PackID)
}

func resolveProvidersForQuery(ctx context.Context, store *sqlite.DB, req ProviderQueryRequest) ([]sqlite.Provider, error) {
	if store == nil {
		return nil, fmt.Errorf("store is required")
	}
	providerID := strings.TrimSpace(req.ProviderID)
	if providerID == "" {
		return nil, fmt.Errorf("provider_id is required")
	}
	if packID := strings.TrimSpace(req.PackID); packID != "" {
		packRow, err := store.Packs().GetByPackID(ctx, packID)
		if err != nil {
			return nil, err
		}
		providerRow, err := store.Providers().GetByPackIDAndProviderID(ctx, packRow.ID, providerID)
		if err != nil {
			return nil, err
		}
		return []sqlite.Provider{providerRow}, nil
	}
	return store.Providers().ListByProviderID(ctx, providerID)
}

func resolveLatestAccessStatus(ctx context.Context, store *sqlite.DB, providerRow sqlite.Provider, hostID string) (string, error) {
	if store == nil {
		return "", fmt.Errorf("store is required")
	}
	if strings.TrimSpace(hostID) != "" {
		hostRow, err := store.Hosts().GetByHostID(ctx, hostID)
		if err != nil {
			return "", err
		}
		batches, err := store.ImportBatches().ListByProviderIDAndHostID(ctx, providerRow.ID, hostRow.ID)
		if err != nil {
			return "", err
		}
		modeStatuses, err := provision.LatestModeAccessStatuses(ctx, store, batches)
		if err != nil {
			return "", err
		}
		return provision.AggregateAccessStatus(modeStatuses), nil
	}
	batches, err := store.ImportBatches().ListByProviderID(ctx, providerRow.ID)
	if err != nil {
		return "", err
	}
	if len(batches) == 0 {
		return "", fmt.Errorf("latest import batch not found for provider")
	}
	hostIDValue := batches[0].HostID
	for _, batch := range batches[1:] {
		if batch.HostID != hostIDValue {
			return "", fmt.Errorf("provider exists on multiple hosts; host_id is required")
		}
	}
	modeStatuses, err := provision.LatestModeAccessStatuses(ctx, store, batches)
	if err != nil {
		return "", err
	}
	return provision.AggregateAccessStatus(modeStatuses), nil
}

func resolveManagedHost(ctx context.Context, store *sqlite.DB, hostID, baseURL string, auth CreateHostAuth) (sqlite.Host, *sub2api.Client, error) {
	if store == nil {
		return sqlite.Host{}, nil, fmt.Errorf("store is required")
	}
	hostID = strings.TrimSpace(hostID)
	baseURL = strings.TrimSpace(baseURL)
	if hostID != "" {
		hostRow, err := store.Hosts().GetByHostID(ctx, hostID)
		if err != nil {
			return sqlite.Host{}, nil, err
		}
		if baseURL != "" && baseURL != strings.TrimSpace(hostRow.BaseURL) {
			return sqlite.Host{}, nil, fmt.Errorf("host %q base_url mismatch: registered=%s runtime=%s", hostID, hostRow.BaseURL, baseURL)
		}
		client, err := newSub2APIClient(hostRow.BaseURL, authFromStoredHost(hostRow))
		if err != nil {
			return sqlite.Host{}, nil, err
		}
		return hostRow, client, nil
	}
	if baseURL == "" {
		return sqlite.Host{}, nil, fmt.Errorf("host_id is required")
	}
	hostRow, err := store.Hosts().GetByBaseURL(ctx, baseURL)
	if err != nil {
		return sqlite.Host{}, nil, fmt.Errorf("host_id is required for unregistered host_base_url %q: %w", baseURL, err)
	}
	client, err := newSub2APIClient(hostRow.BaseURL, authFromStoredHost(hostRow))
	if err != nil {
		return sqlite.Host{}, nil, err
	}
	return hostRow, client, nil
}

func authFromStoredHost(host sqlite.Host) CreateHostAuth {
	authType := strings.TrimSpace(host.AuthType)
	if authType == "" {
		authType = "apikey"
	}
	return CreateHostAuth{Type: authType, Token: strings.TrimSpace(host.AuthToken)}
}

func createHostAuthFromLegacyFields(apiKey, bearerToken string) CreateHostAuth {
	if token := strings.TrimSpace(bearerToken); token != "" {
		return CreateHostAuth{Type: "bearer", Token: token}
	}
	return CreateHostAuth{Type: "apikey", Token: strings.TrimSpace(apiKey)}
}

func newSub2APIClient(baseURL string, auth CreateHostAuth) (*sub2api.Client, error) {
	authToken := strings.TrimSpace(auth.Token)
	if authToken == "" {
		return nil, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: "auth.token is required"}
	}
	switch strings.ToLower(strings.TrimSpace(auth.Type)) {
	case "bearer":
		return sub2api.NewClient(baseURL, sub2api.WithBearerToken(authToken))
	case "apikey", "api_key", "":
		return sub2api.NewClient(baseURL, sub2api.WithAPIKey(authToken))
	default:
		return nil, &httpError{StatusCode: http.StatusBadRequest, Code: "bad_request", Message: fmt.Sprintf("unsupported auth type %q (supported: bearer, apikey)", auth.Type)}
	}
}

func probeHostSnapshot(ctx context.Context, client *sub2api.Client) (string, sub2api.HostCapabilities, error) {
	hostVersion, err := client.GetHostVersion(ctx)
	if err != nil {
		return "", sub2api.HostCapabilities{}, fmt.Errorf("get host version: %w", err)
	}
	capabilities, err := client.ProbeCapabilities(ctx)
	if err != nil {
		return "", sub2api.HostCapabilities{}, fmt.Errorf("probe host capabilities: %w", err)
	}
	return hostVersion, capabilities, nil
}

func hostSupportStatus(capabilities sub2api.HostCapabilities) string {
	if !capabilities.Groups || !capabilities.Channels || !capabilities.Plans || !capabilities.Accounts || !capabilities.AccountTest || !capabilities.AccountModels || !capabilities.Subscriptions {
		return "unsupported"
	}
	return "supported"
}

func accessStatusSupportsMode(status, mode string) bool {
	status = strings.TrimSpace(status)
	mode = strings.TrimSpace(mode)
	switch mode {
	case "", "any":
		return status != provision.AccessStatusBroken && status != ""
	case provision.AccessModeSubscription:
		return status == provision.AccessStatusSubscriptionReady || status == provision.AccessStatusFullyReady
	case provision.AccessModeSelfService:
		return status == provision.AccessStatusSelfServiceReady || status == provision.AccessStatusFullyReady
	default:
		return status == provision.AccessStatusFullyReady
	}
}

func hostRecordToInfo(host sqlite.Host) HostInfo {
	info := HostInfo{
		HostID:      host.HostID,
		BaseURL:     host.BaseURL,
		HostVersion: host.HostVersion,
		AuthType:    strings.TrimSpace(host.AuthType),
	}
	if strings.TrimSpace(host.CapabilityProbeJSON) != "" && host.CapabilityProbeJSON != "{}" {
		var caps sub2api.HostCapabilities
		if err := json.Unmarshal([]byte(host.CapabilityProbeJSON), &caps); err == nil {
			info.Capabilities = &caps
			info.Status = hostSupportStatus(caps)
		}
	}
	return info
}

func packRecordToInfo(pack sqlite.Pack) PackInfo {
	return PackInfo{
		PackID:         pack.PackID,
		Version:        pack.Version,
		Vendor:         pack.Vendor,
		TargetHost:     pack.TargetHost,
		MinHostVersion: pack.MinHostVersion,
		MaxHostVersion: pack.MaxHostVersion,
	}
}

type providerModelOwner struct {
	ProviderID  string
	DisplayName string
	Models      []string
	Source      string
	DraftID     string
}

func validateProviderDraftModelConflicts(ctx context.Context, store *sqlite.DB, packID, providerID, draftID string, supportedModels []string, smokeTestModel string) error {
	packID = strings.TrimSpace(packID)
	providerID = strings.TrimSpace(providerID)
	draftID = strings.TrimSpace(draftID)
	targetModels := normalizeProviderModels(supportedModels, smokeTestModel)
	if packID == "" || providerID == "" || len(targetModels) == 0 {
		return nil
	}

	owners, err := collectProviderModelOwners(ctx, store, packID)
	if err != nil {
		return err
	}

	conflicts := make([]string, 0)
	seen := make(map[string]struct{})
	for _, owner := range owners {
		if owner.ProviderID == "" || owner.ProviderID == providerID {
			continue
		}
		if owner.DraftID != "" && owner.DraftID == draftID {
			continue
		}
		matched := intersectNormalizedModels(targetModels, owner.Models)
		if len(matched) == 0 {
			continue
		}
		label := fmt.Sprintf("%s -> %s[%s]", strings.Join(matched, "/"), owner.ProviderID, owner.Source)
		if _, ok := seen[label]; ok {
			continue
		}
		seen[label] = struct{}{}
		conflicts = append(conflicts, label)
	}
	if len(conflicts) == 0 {
		return nil
	}
	return &httpError{
		StatusCode: http.StatusConflict,
		Code:       "provider_model_conflict",
		Message:    fmt.Sprintf("provider model conflict in pack %q: %s", packID, strings.Join(conflicts, "; ")),
	}
}

func collectProviderModelOwners(ctx context.Context, store *sqlite.DB, packID string) ([]providerModelOwner, error) {
	owners := make([]providerModelOwner, 0)

	repoOwners, err := loadRepoPackModelOwners(packID)
	if err != nil {
		return nil, err
	}
	if len(repoOwners) > 0 {
		owners = append(owners, repoOwners...)
	} else {
		storedOwners, err := loadStoredPackModelOwners(ctx, store, packID)
		if err != nil {
			return nil, err
		}
		owners = append(owners, storedOwners...)
	}

	draftRows, err := store.ProviderDrafts().List(ctx, sqlite.ListProviderDraftsFilter{PackID: packID})
	if err != nil {
		return nil, err
	}
	for _, row := range draftRows {
		owners = append(owners, providerModelOwner{
			ProviderID:  strings.TrimSpace(row.ProviderID),
			DisplayName: strings.TrimSpace(row.DisplayName),
			Models:      normalizeProviderModels(decodeStringList(row.SupportedModelsJSON), row.SmokeTestModel),
			Source:      "draft",
			DraftID:     strings.TrimSpace(row.DraftID),
		})
	}

	return owners, nil
}

func loadRepoPackModelOwners(packID string) ([]providerModelOwner, error) {
	startupCfg, err := config.LoadStartupFromEnv()
	if err != nil {
		return nil, err
	}
	repoRoot := strings.TrimSpace(startupCfg.Repository.RepoRoot)
	if repoRoot == "" {
		return nil, nil
	}
	packDir := filepath.Join(repoRoot, "packs", strings.TrimSpace(packID))
	if _, err := os.Stat(packDir); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, nil
		}
		return nil, fmt.Errorf("stat repo pack dir %q: %w", packDir, err)
	}
	loadedPack, err := pack.LoadPath(packDir)
	if err != nil {
		return nil, fmt.Errorf("load repo pack %q for conflict check: %w", packID, err)
	}

	owners := make([]providerModelOwner, 0, len(loadedPack.Providers))
	for _, provider := range loadedPack.Providers {
		owners = append(owners, providerModelOwner{
			ProviderID:  strings.TrimSpace(provider.ProviderID),
			DisplayName: strings.TrimSpace(provider.DisplayName),
			Models:      normalizeProviderModels(provider.DefaultModels, provider.SmokeTestModel),
			Source:      "repo",
		})
	}
	return owners, nil
}

func loadStoredPackModelOwners(ctx context.Context, store *sqlite.DB, packID string) ([]providerModelOwner, error) {
	packRow, err := store.Packs().GetByPackID(ctx, packID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil
		}
		return nil, err
	}
	providerRows, err := store.Providers().ListByPackID(ctx, packRow.ID)
	if err != nil {
		return nil, err
	}
	owners := make([]providerModelOwner, 0, len(providerRows))
	for _, row := range providerRows {
		owners = append(owners, providerModelOwner{
			ProviderID:  strings.TrimSpace(row.ProviderID),
			DisplayName: strings.TrimSpace(row.DisplayName),
			Models:      normalizeProviderModels(decodeStringList(row.DefaultModelsJSON), row.SmokeTestModel),
			Source:      "store",
		})
	}
	return owners, nil
}

func normalizeProviderDraftPayload(req CreateProviderDraftRequest) (string, any, []string, error) {
	supportedModels := normalizeStringList(req.SupportedModels)
	if len(req.Manifest) > 0 {
		var manifestValue any
		if err := json.Unmarshal(req.Manifest, &manifestValue); err != nil {
			return "", nil, nil, fmt.Errorf("decode manifest: %w", err)
		}
		manifestJSON := strings.TrimSpace(string(req.Manifest))
		if manifestJSON == "" {
			manifestJSON = "{}"
		}
		return manifestJSON, manifestValue, supportedModels, nil
	}

	manifestValue := map[string]any{
		"provider_id":      strings.TrimSpace(req.ProviderID),
		"display_name":     strings.TrimSpace(req.DisplayName),
		"platform":         strings.TrimSpace(req.Platform),
		"base_url":         strings.TrimSpace(req.BaseURL),
		"smoke_test_model": strings.TrimSpace(req.SmokeTestModel),
		"supported_models": supportedModels,
	}
	manifestJSONBytes, err := json.Marshal(manifestValue)
	if err != nil {
		return "", nil, nil, fmt.Errorf("marshal manifest: %w", err)
	}
	return string(manifestJSONBytes), manifestValue, supportedModels, nil
}

func providerDraftRecordToInfo(row sqlite.ProviderDraft, manifestValue any, supportedModels []string) (ProviderDraftInfo, error) {
	if manifestValue == nil {
		manifestValue = map[string]any{}
	}
	return ProviderDraftInfo{
		DraftID:         row.DraftID,
		PackID:          row.PackID,
		ProviderID:      row.ProviderID,
		DisplayName:     row.DisplayName,
		Platform:        row.Platform,
		BaseURL:         row.BaseURL,
		SmokeTestModel:  row.SmokeTestModel,
		SupportedModels: append([]string(nil), supportedModels...),
		Manifest:        manifestValue,
		SourceHostID:    row.SourceHostID,
		Notes:           row.Notes,
		CreatedAt:       row.CreatedAt,
		UpdatedAt:       row.UpdatedAt,
	}, nil
}

func providerDraftRecordToInfoFromStored(row sqlite.ProviderDraft) (ProviderDraftInfo, error) {
	var manifestValue any
	if strings.TrimSpace(row.ManifestJSON) != "" {
		if err := json.Unmarshal([]byte(row.ManifestJSON), &manifestValue); err != nil {
			return ProviderDraftInfo{}, fmt.Errorf("decode stored provider draft manifest: %w", err)
		}
	}
	supportedModels := []string{}
	if strings.TrimSpace(row.SupportedModelsJSON) != "" {
		if err := json.Unmarshal([]byte(row.SupportedModelsJSON), &supportedModels); err != nil {
			return ProviderDraftInfo{}, fmt.Errorf("decode stored provider draft supported_models: %w", err)
		}
	}
	return providerDraftRecordToInfo(row, manifestValue, supportedModels)
}

func buildPublishedProviderManifest(row sqlite.ProviderDraft) (pack.ProviderManifest, error) {
	manifest := pack.ProviderManifest{
		ProviderID:     strings.TrimSpace(row.ProviderID),
		DisplayName:    strings.TrimSpace(row.DisplayName),
		BaseURL:        strings.TrimSpace(row.BaseURL),
		Platform:       strings.TrimSpace(row.Platform),
		AccountType:    "apikey",
		SmokeTestModel: strings.TrimSpace(row.SmokeTestModel),
		DefaultModels:  normalizeStringList(decodeStringList(row.SupportedModelsJSON)),
		GroupTemplate: pack.GroupTemplate{
			Name:           strings.TrimSpace(row.DisplayName) + " 默认分组",
			RateMultiplier: 1.0,
		},
		ChannelTemplate: pack.ChannelTemplate{
			Name:         strings.TrimSpace(row.DisplayName) + " 默认渠道",
			ModelMapping: map[string]string{},
		},
		PlanTemplate: pack.PlanTemplate{
			Name:         strings.TrimSpace(row.DisplayName) + " 默认套餐",
			Price:        19.9,
			ValidityDays: 30,
			ValidityUnit: "day",
		},
		Import: pack.ImportOptions{
			SupportsMultiKey: true,
			SupportsStrict:   true,
			SupportsPartial:  true,
		},
	}
	if strings.TrimSpace(manifest.SmokeTestModel) == "" && len(manifest.DefaultModels) > 0 {
		manifest.SmokeTestModel = manifest.DefaultModels[0]
	}
	if len(manifest.DefaultModels) == 0 && strings.TrimSpace(manifest.SmokeTestModel) != "" {
		manifest.DefaultModels = []string{manifest.SmokeTestModel}
	}
	for _, model := range manifest.DefaultModels {
		manifest.ChannelTemplate.ModelMapping[model] = model
	}

	if strings.TrimSpace(row.ManifestJSON) != "" {
		if err := json.Unmarshal([]byte(row.ManifestJSON), &manifest); err != nil {
			return pack.ProviderManifest{}, fmt.Errorf("decode publishable provider manifest: %w", err)
		}
	}

	manifest.ProviderID = strings.TrimSpace(manifest.ProviderID)
	manifest.DisplayName = strings.TrimSpace(manifest.DisplayName)
	manifest.BaseURL = strings.TrimSpace(manifest.BaseURL)
	manifest.Platform = strings.TrimSpace(manifest.Platform)
	manifest.AccountType = strings.TrimSpace(manifest.AccountType)
	manifest.SmokeTestModel = strings.TrimSpace(manifest.SmokeTestModel)
	if manifest.AccountType == "" {
		manifest.AccountType = "apikey"
	}
	manifest.DefaultModels = normalizeStringList(manifest.DefaultModels)
	if len(manifest.DefaultModels) == 0 && manifest.SmokeTestModel != "" {
		manifest.DefaultModels = []string{manifest.SmokeTestModel}
	}
	if manifest.SmokeTestModel == "" && len(manifest.DefaultModels) > 0 {
		manifest.SmokeTestModel = manifest.DefaultModels[0]
	}
	if manifest.ChannelTemplate.ModelMapping == nil {
		manifest.ChannelTemplate.ModelMapping = map[string]string{}
	}
	for _, model := range manifest.DefaultModels {
		model = strings.TrimSpace(model)
		if model == "" {
			continue
		}
		if _, ok := manifest.ChannelTemplate.ModelMapping[model]; !ok {
			manifest.ChannelTemplate.ModelMapping[model] = model
		}
	}
	if manifest.SmokeTestModel != "" {
		if _, ok := manifest.ChannelTemplate.ModelMapping[manifest.SmokeTestModel]; !ok {
			manifest.ChannelTemplate.ModelMapping[manifest.SmokeTestModel] = manifest.SmokeTestModel
		}
	}
	return manifest, nil
}

func encodeStringList(values []string) string {
	encoded, err := json.Marshal(normalizeStringList(values))
	if err != nil {
		return "[]"
	}
	return string(encoded)
}

func normalizeProviderModels(values []string, smokeTestModel string) []string {
	normalized := normalizeStringList(values)
	if len(normalized) == 0 {
		if smoke := strings.TrimSpace(smokeTestModel); smoke != "" {
			normalized = []string{smoke}
		}
	}
	if len(normalized) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(normalized))
	result := make([]string, 0, len(normalized))
	for _, value := range normalized {
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	return result
}

func intersectNormalizedModels(left, right []string) []string {
	if len(left) == 0 || len(right) == 0 {
		return nil
	}
	rightSet := make(map[string]struct{}, len(right))
	for _, value := range right {
		rightSet[value] = struct{}{}
	}
	matched := make([]string, 0)
	for _, value := range left {
		if _, ok := rightSet[value]; ok {
			matched = append(matched, value)
		}
	}
	return matched
}

func normalizeStringList(values []string) []string {
	normalized := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		normalized = append(normalized, value)
	}
	return normalized
}

func decodeStringList(raw string) []string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return []string{}
	}
	values := []string{}
	if err := json.Unmarshal([]byte(raw), &values); err != nil {
		return []string{}
	}
	return values
}

func deriveAccessStatus(gw sub2api.GatewayAccessResult) string {
	if provision.GatewayAccessReady(gw) {
		return provision.AccessStatusSubscriptionReady
	}
	return provision.AccessStatusBroken
}

func subscriptionTargets(userIDs []string, durationDays int) []provision.SubscriptionTarget {
	targets := make([]provision.SubscriptionTarget, 0, len(userIDs))
	for _, userID := range userIDs {
		targets = append(targets, provision.SubscriptionTarget{
			UserID:       strings.TrimSpace(userID),
			DurationDays: durationDays,
		})
	}
	return targets
}
