package servicectl

import (
	"context"
	"errors"
	"net/http"
	"sort"
	"strings"
	"time"

	"gitcode-mcp/internal/adminhttp"
	"gitcode-mcp/internal/cache"
	"gitcode-mcp/internal/config"
	"gitcode-mcp/internal/feedback"
	"gitcode-mcp/internal/rag"
	"gitcode-mcp/internal/repositorydocs"
	"gitcode-mcp/internal/service"
)

type AdminBindingPlan struct {
	SchemaVersion string                    `json:"schema_version"`
	PlanID        string                    `json:"plan_id"`
	Status        string                    `json:"status"`
	CacheRef      string                    `json:"cache_ref"`
	RepoID        string                    `json:"repo_id"`
	Action        string                    `json:"action"`
	BeforeHash    string                    `json:"before_hash,omitempty"`
	AfterHash     string                    `json:"after_hash"`
	Binding       service.RepositoryBinding `json:"binding"`
	Effects       []MaintenancePlanAction   `json:"effects"`
	Blockers      []string                  `json:"blockers,omitempty"`
}

type AdminControlManager struct {
	manager     Manager
	maintenance *MaintenanceManager
	jobs        *JobManager
	receipts    *AdminControlReceiptManager
}

type AdminFeedbackSetupResult struct {
	config.FeedbackSetupResult
	Feedback adminhttp.FeedbackObservation `json:"feedback"`
}

func NewAdminControlManager(manager Manager, maintenance *MaintenanceManager, jobs *JobManager, receipts *AdminControlReceiptManager) *AdminControlManager {
	return &AdminControlManager{manager: manager, maintenance: maintenance, jobs: jobs, receipts: receipts}
}

func (m *AdminControlManager) PlanMaintenance(ctx context.Context, req adminhttp.MaintenanceControlRequest) (any, error) {
	setup, mapped, err := m.maintenanceSetup(ctx, req)
	if err != nil {
		return nil, err
	}
	plan, err := setup.Plan(ctx, mapped)
	if err != nil {
		return nil, adminControlError(err)
	}
	return plan, nil
}

func (m *AdminControlManager) ApplyMaintenance(ctx context.Context, req adminhttp.MaintenanceControlRequest) (any, error) {
	if strings.TrimSpace(req.PlanID) == "" {
		return nil, controlError(http.StatusBadRequest, "invalid_request", "plan_id is required.", "Render and review a current plan before apply.")
	}
	setup, mapped, err := m.maintenanceSetup(ctx, req)
	if err != nil {
		return nil, err
	}
	mapped.PlanID = strings.TrimSpace(req.PlanID)
	mapped.IdempotencyKey = strings.TrimSpace(req.IdempotencyKey)
	mapped.Confirmed = true
	// Browser authority never extends to service installation, provider startup,
	// model downloads, migration, or other machine-level effects.
	mapped.AllowMachineChange = false
	result, err := setup.Apply(ctx, mapped)
	if err != nil {
		return nil, adminControlError(err)
	}
	return result, nil
}

func (m *AdminControlManager) PlanFeedbackSetup(ctx context.Context, req adminhttp.FeedbackSetupRequest) (any, error) {
	repoID := strings.TrimSpace(req.RepoID)
	if !feedback.ValidRepositoryID(repoID) {
		return nil, adminhttp.ControlError{Status: http.StatusBadRequest, Code: "invalid_request", Field: "repo_id", Message: "repo_id must be an exact owner/repository identity.", Remediation: "Select a repository already bound in the effective cache."}
	}
	if _, err := m.feedbackSetupEffective(ctx, repoID); err != nil {
		return nil, err
	}
	plan, err := config.PlanFeedbackSetup(m.manager.Source, repoID)
	if err != nil {
		return nil, controlError(http.StatusConflict, "feedback_setup_unavailable", "Trusted feedback setup is unavailable for the current global configuration.", "Use a global YAML configuration and run gitcode-mcp feedback setup from a trusted terminal.")
	}
	return plan, nil
}

func (m *AdminControlManager) ApplyFeedbackSetup(ctx context.Context, req adminhttp.FeedbackSetupRequest) (any, error) {
	repoID := strings.TrimSpace(req.RepoID)
	if strings.TrimSpace(req.PlanID) == "" {
		return nil, adminhttp.ControlError{Status: http.StatusBadRequest, Code: "invalid_request", Field: "plan_id", Message: "plan_id is required.", Remediation: "Render and review a current feedback setup plan."}
	}
	if _, err := m.feedbackSetupEffective(ctx, repoID); err != nil {
		return nil, err
	}
	plan, err := config.PlanFeedbackSetup(m.manager.Source, repoID)
	if err != nil {
		return nil, controlError(http.StatusConflict, "feedback_setup_unavailable", "Trusted feedback setup is unavailable for the current global configuration.", "Use a global YAML configuration and run gitcode-mcp feedback setup from a trusted terminal.")
	}
	result, err := config.ApplyFeedbackSetupWithExpectedPlan(plan, strings.TrimSpace(req.PlanID), strings.TrimSpace(req.IdempotencyKey), time.Now().UTC())
	if err != nil {
		if errors.Is(err, config.ErrFeedbackSetupStalePlan) {
			return nil, adminhttp.ControlError{Status: http.StatusConflict, Code: "stale_plan", Field: "plan_id", Message: "The reviewed feedback setup plan no longer matches current configuration or a retained receipt.", Remediation: "Refresh feedback readiness and render a new plan."}
		}
		return nil, adminControlError(err)
	}
	return AdminFeedbackSetupResult{FeedbackSetupResult: result, Feedback: m.manager.adminFeedbackObservation(ctx)}, nil
}

func (m *AdminControlManager) feedbackSetupEffective(ctx context.Context, repoID string) (config.EffectiveConfig, error) {
	if !feedback.ValidRepositoryID(repoID) {
		return config.EffectiveConfig{}, adminhttp.ControlError{Status: http.StatusBadRequest, Code: "invalid_request", Field: "repo_id", Message: "repo_id must be an exact owner/repository identity.", Remediation: "Select a repository already bound in the effective cache."}
	}
	src := m.manager.Source
	if src == nil {
		src = config.OSSource{}
	}
	effective, err := config.LoadEffective(src, config.Overrides{})
	if err != nil {
		return config.EffectiveConfig{}, controlError(http.StatusConflict, "feedback_setup_unavailable", "Trusted feedback configuration cannot be loaded.", "Repair the global configuration from a trusted terminal, then refresh Admin.")
	}
	cachePath := effective.Config.CachePath
	if strings.TrimSpace(m.manager.AdminCachePath) != "" {
		cachePath = m.manager.AdminCachePath
	}
	store, err := cache.NewSQLiteReadOnlyStore(ctx, cachePath)
	if err != nil {
		return config.EffectiveConfig{}, controlError(http.StatusConflict, "repository_unbound", "The selected feedback repository is not available in the effective cache.", "Bind the repository in the effective cache before feedback setup.")
	}
	defer store.Close()
	if _, err := store.GetRepository(ctx, repoID); err != nil {
		parts := strings.Split(repoID, "/")
		handoff := "gitcode-mcp repo add --repo " + repoID + " --owner " + parts[0] + " --name " + parts[1] + " --scopes issues"
		return config.EffectiveConfig{}, adminhttp.ControlError{Status: http.StatusConflict, Code: "repository_unbound", Field: "repo_id", Message: "The selected feedback repository is not bound in the effective cache.", Remediation: "Bind the repository in the effective cache, then render a new feedback setup plan.", CLIHandoff: handoff}
	}
	return effective, nil
}

func (m *AdminControlManager) DisableMaintenance(ctx context.Context, req adminhttp.RegistrationControlRequest) (any, error) {
	if m.maintenance == nil {
		return nil, controlError(http.StatusNotImplemented, "capability_unavailable", "Maintenance controls are unavailable.", "Use the CLI maintenance surface.")
	}
	return m.receipts.Apply(ctx, "maintenance_disable", req.RegistrationID, req.IdempotencyKey, struct {
		RegistrationID string `json:"registration_id"`
	}{req.RegistrationID}, func() (map[string]any, error) {
		entry, err := m.maintenance.Disable(ctx, req.RegistrationID)
		if err != nil {
			return nil, adminControlError(err)
		}
		return map[string]any{"outcome": "disabled", "registration": adminMaintenanceObservation(entry)}, nil
	})
}

func (m *AdminControlManager) ReconcileMaintenance(ctx context.Context, req adminhttp.RegistrationControlRequest) (any, error) {
	if m.maintenance == nil {
		return nil, controlError(http.StatusNotImplemented, "capability_unavailable", "Maintenance controls are unavailable.", "Use the CLI maintenance surface.")
	}
	return m.receipts.Apply(ctx, "maintenance_reconcile", req.RegistrationID, req.IdempotencyKey, struct {
		RegistrationID string `json:"registration_id"`
	}{req.RegistrationID}, func() (map[string]any, error) {
		result, err := m.maintenance.ReconcileRegistration(ctx, req.RegistrationID)
		if err != nil {
			return nil, adminControlError(err)
		}
		return map[string]any{"outcome": maintenanceReconcileOutcome(result), "jobs_started": result.JobsStarted, "jobs_coalesced": result.JobsCoalesced, "checked_at": result.CheckedAt}, nil
	})
}

func (m *AdminControlManager) PlanMaintenanceConflictResolution(ctx context.Context, req adminhttp.MaintenanceConflictResolutionRequest) (any, error) {
	if m.maintenance == nil {
		return nil, controlError(http.StatusNotImplemented, "capability_unavailable", "Maintenance conflict resolution is unavailable.", "Use a daemon that exposes the canonical identity resolution capability.")
	}
	mapped := MaintenanceConflictResolutionRequest{RegistrationID: req.RegistrationID, CandidateRef: req.CandidateRef, ExpectedGeneration: req.ExpectedGeneration}
	if err := validateMaintenanceConflictResolutionRequest(mapped, false); err != nil {
		return nil, controlError(http.StatusBadRequest, "invalid_request", "registration, candidate, and current generation are required.", "Refresh maintenance state and select one explicit candidate.")
	}
	plan, err := m.maintenance.PlanConflictResolution(ctx, mapped)
	if err != nil {
		return nil, adminControlError(err)
	}
	return plan, nil
}

func (m *AdminControlManager) ApplyMaintenanceConflictResolution(ctx context.Context, req adminhttp.MaintenanceConflictResolutionRequest) (any, error) {
	if m.maintenance == nil {
		return nil, controlError(http.StatusNotImplemented, "capability_unavailable", "Maintenance conflict resolution is unavailable.", "Use a daemon that exposes the canonical identity resolution capability.")
	}
	mapped := MaintenanceConflictResolutionRequest{RegistrationID: req.RegistrationID, CandidateRef: req.CandidateRef, ExpectedGeneration: req.ExpectedGeneration, PlanID: req.PlanID, IdempotencyKey: req.IdempotencyKey}
	if err := validateMaintenanceConflictResolutionRequest(mapped, true); err != nil {
		return nil, controlError(http.StatusBadRequest, "invalid_request", "candidate, generation, plan_id, and idempotency_key are required.", "Render and confirm one current conflict resolution plan.")
	}
	result, err := m.maintenance.ApplyConflictResolution(ctx, mapped)
	if err != nil {
		return nil, adminControlError(err)
	}
	return result, nil
}

func (m *AdminControlManager) SearchRepositoryDocs(ctx context.Context, req adminhttp.RepositoryDocsSearchRequest) (any, error) {
	if m.maintenance == nil {
		return nil, controlError(http.StatusNotImplemented, "capability_unavailable", "Repository documentation search is unavailable.", "Use the CLI repository documentation surface.")
	}
	req.Query = strings.TrimSpace(req.Query)
	req.Revision = strings.TrimSpace(req.Revision)
	req.Mode = strings.ReplaceAll(strings.ToLower(strings.TrimSpace(req.Mode)), "_", "")
	if req.Query == "" || len(req.Query) > 512 {
		return nil, controlError(http.StatusBadRequest, "invalid_request", "query must contain between 1 and 512 characters.", "Enter a bounded repository documentation query.")
	}
	if len(req.Revision) > 256 {
		return nil, controlError(http.StatusBadRequest, "invalid_request", "revision is too long.", "Use a Git ref or object id with at most 256 characters.")
	}
	if req.Mode == "" {
		req.Mode = repositorydocs.SearchModeHybrid
	}
	if req.Mode != repositorydocs.SearchModeHybrid && req.Mode != repositorydocs.SearchModeFullText {
		return nil, controlError(http.StatusBadRequest, "invalid_request", "mode must be hybrid or fulltext.", "Select a supported repository documentation search mode.")
	}
	if req.Limit == 0 {
		req.Limit = 8
	}
	if req.Limit < 1 || req.Limit > 20 {
		return nil, controlError(http.StatusBadRequest, "invalid_request", "limit must be between 1 and 20.", "Select a bounded result limit.")
	}
	source, err := m.maintenance.repositoryDocsSourceForSelector(RepositoryDocsSourceSelector{RegistrationID: req.RegistrationID, SourceRegistrationID: req.SourceRegistrationID, SourceRegistrationGeneration: req.SourceRegistrationGeneration})
	if err != nil {
		return nil, adminControlError(err)
	}
	store, err := cache.NewSQLiteReadOnlyStore(ctx, source.CachePath)
	if err != nil {
		return nil, adminControlError(err)
	}
	defer store.Close()
	repo, err := repositorydocs.OpenRepository(ctx, source.RepositoryPath)
	if err != nil {
		return nil, adminControlError(err)
	}
	var provider rag.EmbeddingProvider
	if req.Mode == repositorydocs.SearchModeHybrid {
		provider, _ = rag.NewEmbeddingProviderFromConfig(source.Config, source.Profile, rag.ProviderOptions{})
	}
	result, err := repositorydocs.NewRetriever(store, provider).Search(ctx, repositorydocs.SearchRequest{
		RepoID: source.RepoID, SourceRegistrationID: source.SourceRegistrationID, SourceRegistrationGeneration: source.SourceRegistrationGeneration, Repository: repo, Revision: req.Revision,
		IncludeWorktree: req.IncludeWorktree, Query: req.Query, Mode: req.Mode, Limit: req.Limit,
	})
	if err != nil {
		return nil, adminControlError(err)
	}
	return result, nil
}

func (m *AdminControlManager) PlanRepositoryDocs(ctx context.Context, req adminhttp.RepositoryDocsPlanRequest) (any, error) {
	if m.maintenance == nil {
		return nil, controlError(http.StatusNotImplemented, "capability_unavailable", "Repository documentation planning is unavailable.", "Use the CLI repository documentation surface.")
	}
	if len(strings.TrimSpace(req.Revision)) > 256 {
		return nil, controlError(http.StatusBadRequest, "invalid_request", "revision is too long.", "Use a Git ref or object id with at most 256 characters.")
	}
	source, err := m.maintenance.repositoryDocsSourceForSelector(RepositoryDocsSourceSelector{RegistrationID: req.RegistrationID, SourceRegistrationID: req.SourceRegistrationID, SourceRegistrationGeneration: req.SourceRegistrationGeneration})
	if err != nil {
		return nil, adminControlError(err)
	}
	repo, err := repositorydocs.OpenRepository(ctx, source.RepositoryPath)
	if err != nil {
		return nil, adminControlError(err)
	}
	return repositorydocs.InspectPlan(ctx, repo, repositorydocs.PlanRequest{
		RepoID: source.RepoID, RegistrationID: source.RegistrationID,
		SourceRegistrationID: source.SourceRegistrationID, SourceRegistrationGeneration: source.SourceRegistrationGeneration,
		Revision: req.Revision, IncludeWorktree: req.IncludeWorktree,
	})
}

func (m *AdminControlManager) IndexRepositoryDocs(ctx context.Context, req adminhttp.RegistrationControlRequest) (any, error) {
	if m.maintenance == nil || m.jobs == nil {
		return nil, controlError(http.StatusNotImplemented, "capability_unavailable", "Repository documentation indexing is unavailable.", "Use the CLI repository documentation surface.")
	}
	source, err := m.maintenance.repositoryDocsSourceForSelector(RepositoryDocsSourceSelector{RegistrationID: req.RegistrationID, SourceRegistrationID: req.SourceRegistrationID, SourceRegistrationGeneration: req.SourceRegistrationGeneration})
	if err != nil {
		return nil, adminControlError(err)
	}
	intent := struct {
		RegistrationID               string `json:"registration_id"`
		SourceRegistrationID         string `json:"source_registration_id"`
		SourceRegistrationGeneration int64  `json:"source_registration_generation"`
		CommitScope                  string `json:"commit_scope"`
	}{source.RegistrationID, source.SourceRegistrationID, source.SourceRegistrationGeneration, "HEAD"}
	return m.receipts.Apply(ctx, "repository_docs_index", source.RegistrationID, req.IdempotencyKey, intent, func() (map[string]any, error) {
		jobManager := m.manager
		jobManager.EffectiveConfig = &source.Config
		job, err := m.jobs.StartRepositoryDocsIndex(context.Background(), jobManager, StartRepositoryDocsIndexJobRequest{
			RepoID: source.RepoID, RepositoryPath: source.RepositoryPath, Profile: source.Profile,
			CachePath: source.CachePath, CacheUUID: source.CacheUUID, RegistrationID: source.RegistrationID,
			SourceRegistrationID:         source.SourceRegistrationID,
			SourceRegistrationGeneration: source.SourceRegistrationGeneration,
		})
		if err != nil {
			return nil, adminControlError(err)
		}
		return map[string]any{"outcome": "accepted", "job_id": job.ID, "job_status": job.Status, "commit_scope": "HEAD"}, nil
	})
}

func (m *AdminControlManager) PlanBinding(ctx context.Context, req adminhttp.BindingControlRequest) (any, error) {
	plan, _, err := m.bindingPlan(ctx, req)
	if err != nil {
		return nil, err
	}
	return plan, nil
}

func (m *AdminControlManager) ApplyBinding(ctx context.Context, req adminhttp.BindingControlRequest) (any, error) {
	if strings.TrimSpace(req.PlanID) == "" {
		return nil, controlError(http.StatusBadRequest, "invalid_request", "plan_id is required.", "Render and review a current binding plan.")
	}
	intent := struct {
		CacheRef string `json:"cache_ref"`
		RepoID   string `json:"repo_id"`
		PlanID   string `json:"plan_id"`
	}{strings.TrimSpace(req.CacheRef), strings.TrimSpace(req.RepoID), strings.TrimSpace(req.PlanID)}
	return m.receipts.Apply(ctx, "binding_apply", intent.CacheRef+"/"+intent.RepoID, req.IdempotencyKey, intent, func() (map[string]any, error) {
		path, err := m.resolveManagedCachePath(ctx, intent.CacheRef)
		if err != nil {
			return nil, err
		}
		identityStore, err := cache.NewSQLiteReadOnlyStore(ctx, path)
		if err != nil {
			return nil, adminControlError(err)
		}
		identity, identityErr := identityStore.CacheIdentity(ctx)
		_ = identityStore.Close()
		if identityErr != nil {
			return nil, adminControlError(identityErr)
		}
		releaseWriter := func() {}
		if m.jobs != nil {
			writerHash := strings.TrimPrefix(maintenanceHash(intent), "sha256:")
			releaseWriter, err = m.jobs.BeginDirectCacheWriter(identity.UUID, "admin-binding-"+writerHash[:16])
			if err != nil {
				return nil, adminControlError(err)
			}
		}
		defer releaseWriter()
		plan, plannedPath, err := m.bindingPlan(ctx, req)
		if err != nil {
			return nil, err
		}
		if plannedPath != path {
			return nil, controlError(http.StatusConflict, "stale_plan", "The reviewed cache authority changed before apply.", "Render and confirm a new binding plan.")
		}
		if req.PlanID != plan.PlanID {
			return nil, controlError(http.StatusConflict, "stale_plan", "The reviewed binding plan no longer matches current cache state.", "Render and confirm a new binding plan.")
		}
		if plan.Status == "blocked" {
			return map[string]any{"outcome": "blocked", "plan": plan}, nil
		}
		if plan.Action == "no_op" {
			return map[string]any{"outcome": "already_applied", "plan_id": plan.PlanID, "binding": plan.Binding}, nil
		}
		store, err := cache.NewSQLiteStore(ctx, path)
		if err != nil {
			return nil, adminControlError(err)
		}
		defer store.Close()
		svc := service.New(store)
		request := service.AddRepositoryRequest{RepoID: plan.Binding.RepoID, Owner: plan.Binding.Owner, Name: plan.Binding.Name, APIBaseURL: plan.Binding.APIBaseURL, DisplayName: plan.Binding.DisplayName, Aliases: plan.Binding.Aliases}
		for _, scope := range plan.Binding.Scopes {
			request.Scopes = append(request.Scopes, string(scope))
		}
		var binding service.RepositoryBinding
		if plan.Action == "add" {
			binding, err = svc.AddRepository(ctx, request)
		} else {
			binding, err = svc.UpdateRepository(ctx, request)
		}
		if err != nil {
			return nil, adminControlError(err)
		}
		outcome := "updated"
		if plan.Action == "add" {
			outcome = "added"
		}
		return map[string]any{"outcome": outcome, "plan_id": plan.PlanID, "binding": binding}, nil
	})
}

func (m *AdminControlManager) bindingPlan(ctx context.Context, req adminhttp.BindingControlRequest) (AdminBindingPlan, string, error) {
	path, err := m.resolveManagedCachePath(ctx, strings.TrimSpace(req.CacheRef))
	if err != nil {
		return AdminBindingPlan{}, "", err
	}
	repoID := strings.Trim(strings.TrimSpace(req.RepoID), "/")
	owner, name := strings.TrimSpace(req.Owner), strings.TrimSpace(req.Name)
	parts := strings.Split(repoID, "/")
	if owner == "" && len(parts) == 2 {
		owner = parts[0]
	}
	if name == "" && len(parts) == 2 {
		name = parts[1]
	}
	effective, err := effectiveJobConfig(m.manager, path)
	if err != nil {
		return AdminBindingPlan{}, "", adminControlError(err)
	}
	store, err := cache.NewSQLiteReadOnlyStore(ctx, path)
	if err != nil {
		return AdminBindingPlan{}, "", adminControlError(err)
	}
	defer store.Close()
	version, err := store.SchemaVersion(ctx)
	if err != nil {
		return AdminBindingPlan{}, "", adminControlError(err)
	}
	identity, err := store.CacheIdentity(ctx)
	if err != nil {
		return AdminBindingPlan{}, "", adminControlError(err)
	}
	repositories, err := store.ListRepositories(ctx)
	if err != nil {
		return AdminBindingPlan{}, "", adminControlError(err)
	}
	var current *cache.RepositoryBinding
	for index := range repositories {
		if repositories[index].RepoID == repoID {
			copy := repositories[index]
			current = &copy
			break
		}
	}
	apiURL := strings.TrimSpace(req.APIBaseURL)
	scopes := append([]string(nil), req.Scopes...)
	aliases := append([]string(nil), req.Aliases...)
	displayName := ""
	if req.DisplayName != nil {
		displayName = *req.DisplayName
	}
	if current != nil {
		if owner == "" {
			owner = current.Owner
		}
		if name == "" {
			name = current.Name
		}
		if apiURL == "" {
			apiURL = current.APIBaseURL
		}
		if req.Scopes == nil {
			for _, scope := range current.Scopes {
				scopes = append(scopes, string(scope))
			}
		}
		if req.Aliases == nil {
			aliases = append(aliases, current.Aliases...)
		}
		if req.DisplayName == nil {
			displayName = current.DisplayName
		}
	}
	if apiURL == "" {
		apiURL = strings.TrimSpace(effective.Config.GitCodeBaseURL)
	}
	desired, err := service.ValidateRepositoryBinding(service.AddRepositoryRequest{RepoID: repoID, Owner: owner, Name: name, APIBaseURL: apiURL, Scopes: scopes, Aliases: aliases, DisplayName: displayName}, time.Unix(0, 0).UTC())
	if err != nil {
		return AdminBindingPlan{}, "", controlError(http.StatusBadRequest, "invalid_binding", "The repository binding is not valid.", "Use owner/repository, supported scopes, unique aliases, and a valid API URL.")
	}
	plan := AdminBindingPlan{SchemaVersion: "gitcode-mcp.binding-plan.v1", Status: "ready", CacheRef: publicCacheRef(identity.UUID, path), RepoID: desired.RepoID, Action: "add", Binding: desired}
	plan.Effects = []MaintenancePlanAction{{ID: "validate-binding", Class: "inspect", Status: "complete", Summary: "validate repository identity, API route, scopes, and aliases"}, {ID: "write-binding", Class: "cache_write", Status: "required", Summary: "atomically write repository binding metadata", ConfirmationRequired: true}, {ID: "gitcode-network", Class: "network_read", Status: "not_performed", Summary: "no GitCode request is performed while planning or applying the binding"}}
	if version != cache.CurrentSchemaVersion() {
		plan.Status = "blocked"
		plan.Blockers = append(plan.Blockers, "cache schema must be migrated before binding changes")
		plan.Effects[1].Status = "blocked"
		plan.Effects[1].Handoff = "gitcode-mcp migrate-cache --confirm"
	}
	for _, existing := range repositories {
		if existing.RepoID == desired.RepoID {
			plan.Action = "update"
			plan.BeforeHash = maintenanceHash(existing)
			if bindingEquivalent(existing, desired) {
				plan.Action = "no_op"
				plan.Effects[1].Status = "complete"
				plan.Effects[1].Summary = "binding already matches the requested intent"
			}
			continue
		}
		for _, existingAlias := range existing.Aliases {
			if desired.RepoID == existingAlias {
				plan.Status = "blocked"
				plan.Blockers = append(plan.Blockers, "repository id "+desired.RepoID+" is an alias of another repository binding")
			}
		}
		for _, alias := range desired.Aliases {
			if alias == existing.RepoID {
				plan.Status = "blocked"
				plan.Blockers = append(plan.Blockers, "alias "+alias+" belongs to another repository binding")
			}
			for _, existingAlias := range existing.Aliases {
				if alias == existingAlias {
					plan.Status = "blocked"
					plan.Blockers = append(plan.Blockers, "alias "+alias+" belongs to another repository binding")
				}
			}
		}
	}
	if len(plan.Blockers) > 0 {
		plan.Effects[1].Status = "blocked"
	}
	plan.AfterHash = maintenanceHash(desired)
	plan.PlanID = "binding-plan-" + strings.TrimPrefix(maintenanceHash(struct {
		Schema     string `json:"schema"`
		CacheUUID  string `json:"cache_uuid"`
		Version    int    `json:"schema_version"`
		BeforeHash string `json:"before_hash"`
		AfterHash  string `json:"after_hash"`
	}{Schema: plan.SchemaVersion, CacheUUID: identity.UUID, Version: version, BeforeHash: plan.BeforeHash, AfterHash: plan.AfterHash}), "sha256:")
	return plan, path, nil
}

func bindingEquivalent(existing cache.RepositoryBinding, desired service.RepositoryBinding) bool {
	if existing.RepoID != desired.RepoID || existing.Owner != desired.Owner || existing.Name != desired.Name || existing.APIBaseURL != desired.APIBaseURL || existing.DisplayName != desired.DisplayName {
		return false
	}
	existingScopes := make([]string, 0, len(existing.Scopes))
	for _, scope := range existing.Scopes {
		existingScopes = append(existingScopes, string(scope))
	}
	desiredScopes := make([]string, 0, len(desired.Scopes))
	for _, scope := range desired.Scopes {
		desiredScopes = append(desiredScopes, string(scope))
	}
	sort.Strings(existingScopes)
	sort.Strings(desiredScopes)
	existingAliases := append([]string(nil), existing.Aliases...)
	desiredAliases := append([]string(nil), desired.Aliases...)
	sort.Strings(existingAliases)
	sort.Strings(desiredAliases)
	return strings.Join(existingScopes, "\x00") == strings.Join(desiredScopes, "\x00") && strings.Join(existingAliases, "\x00") == strings.Join(desiredAliases, "\x00")
}

func (m *AdminControlManager) maintenanceSetup(ctx context.Context, req adminhttp.MaintenanceControlRequest) (MaintenanceSetup, MaintenanceSetupRequest, error) {
	if err := validateAdminMaintenanceRequest(req); err != nil {
		return MaintenanceSetup{}, MaintenanceSetupRequest{}, err
	}
	path, err := m.resolveManagedCachePath(ctx, strings.TrimSpace(req.CacheRef))
	if err != nil {
		return MaintenanceSetup{}, MaintenanceSetupRequest{}, err
	}
	effective, err := effectiveJobConfig(m.manager, path)
	if err != nil {
		return MaintenanceSetup{}, MaintenanceSetupRequest{}, adminControlError(err)
	}
	mapped := MaintenanceSetupRequest{
		RepoID: req.RepoID, Profile: req.Profile, SyncMode: req.SyncMode, Collections: req.Collections, RAGMode: req.RAGMode,
		HeadIntervalSeconds: req.HeadIntervalSeconds, RAGIntervalSeconds: req.RAGIntervalSeconds,
		HeadMaxPages: req.HeadMaxPages, TailSlicePages: req.TailSlicePages, PerPage: req.PerPage,
		NoServiceInstall: true, NoModelDownload: true, Detach: true,
	}
	setup := MaintenanceSetup{Manager: m.manager, Config: effective.Config, CachePath: path, CachePathSource: "daemon-managed", StartupTimeout: effective.Config.DefaultTimeout}
	return setup, mapped, nil
}

func (m *AdminControlManager) resolveManagedCachePath(ctx context.Context, cacheRef string) (string, error) {
	if cacheRef == "" {
		return "", controlError(http.StatusBadRequest, "invalid_request", "cache_ref is required.", "Select one cache exposed by the admin snapshot.")
	}
	entries := m.maintenance.adminEntries()
	groups := groupAdminCaches(strings.TrimSpace(m.manager.AdminCachePath), entries)
	matches := []string{}
	for _, group := range groups {
		uuid := group.uuid
		if store, err := cache.NewSQLiteReadOnlyStore(ctx, group.path); err == nil {
			if identity, identityErr := store.CacheIdentity(ctx); identityErr == nil && identity.UUID != "" {
				uuid = identity.UUID
			}
			store.Close()
		}
		if publicCacheRef(uuid, group.path) == cacheRef {
			matches = append(matches, group.path)
		}
	}
	if len(matches) == 0 {
		return "", controlError(http.StatusNotFound, "cache_not_found", "The selected managed cache is no longer available.", "Refresh the admin snapshot and select a listed cache.")
	}
	if len(matches) > 1 {
		return "", controlError(http.StatusConflict, "ambiguous_cache", "The cache reference resolves to multiple managed locations.", "Run service doctor and resolve cloned cache identity.")
	}
	return matches[0], nil
}

func validateAdminMaintenanceRequest(req adminhttp.MaintenanceControlRequest) error {
	if strings.TrimSpace(req.RepoID) == "" {
		return controlFieldError("repo_id", "repo_id is required.", "Select a repository in the managed cache.")
	}
	if req.SyncMode != "" && req.SyncMode != "off" && req.SyncMode != "head" && req.SyncMode != "head-and-backfill" {
		return controlFieldError("sync_mode", "sync_mode must be off, head, or head-and-backfill.", "Choose a supported synchronization mode and render a new plan.")
	}
	if req.RAGMode != "" && req.RAGMode != "off" && req.RAGMode != "maintain" {
		return controlFieldError("rag_mode", "rag_mode must be off or maintain.", "Choose a supported RAG mode and render a new plan.")
	}
	allowedCollections := map[string]bool{"issues": true, "issue-comments": true, "wiki": true, "pulls": true, "pr-comments": true}
	for _, collection := range req.Collections {
		if !allowedCollections[strings.TrimSpace(strings.ToLower(collection))] {
			return controlFieldError("collections", "collections contains an unsupported value.", "Select only the collections exposed by this daemon.")
		}
	}
	for _, bound := range []struct {
		name  string
		value int
		max   int
	}{{"head_interval_seconds", req.HeadIntervalSeconds, 86400 * 30}, {"rag_interval_seconds", req.RAGIntervalSeconds, 86400 * 30}, {"head_max_pages", req.HeadMaxPages, 1000}, {"tail_slice_pages", req.TailSlicePages, 1000}, {"per_page", req.PerPage, 100}} {
		if bound.value < 0 || bound.value > bound.max {
			return controlFieldError(bound.name, bound.name+" is outside the supported bound.", "Use a non-negative bounded policy value.")
		}
	}
	return nil
}

func controlFieldError(field, message, remediation string) error {
	return adminhttp.ControlError{Status: http.StatusBadRequest, Code: "invalid_policy", Field: field, Message: message, Remediation: remediation, Blockers: []string{message}}
}

func maintenanceReconcileOutcome(result MaintenanceReconcileResult) string {
	if len(result.JobsStarted) > 0 {
		return "created"
	}
	if len(result.JobsCoalesced) > 0 {
		return "coalesced"
	}
	return "no_work_needed"
}

func adminControlError(err error) error {
	var stale StaleMaintenancePlanError
	if errors.As(err, &stale) {
		return controlError(http.StatusConflict, "stale_plan", "The reviewed plan no longer matches current state.", "Render and confirm a new plan.")
	}
	var scopeErr maintenancePolicyScopeError
	if errors.As(err, &scopeErr) {
		scope := string(scopeErr.Scope)
		return adminhttp.ControlError{
			Status:      http.StatusBadRequest,
			Code:        "invalid_policy",
			Field:       "collections",
			Message:     "The selected collections are incompatible with the repository binding.",
			Remediation: "Remove collections outside the configured repository scopes, or update the binding scopes and render a new plan.",
			Blockers:    []string{"The selected collections require the " + scope + " repository scope."},
		}
	}
	if coded, ok := err.(interface{ DiagnosticCode() string }); ok {
		code := coded.DiagnosticCode()
		switch code {
		case "repository_docs_provider_boundary_blocked":
			return controlError(http.StatusConflict, code, "Repository documentation indexing requires a local embedding boundary.", "Configure or select an indexing profile whose provider data_boundary is local_process or local_network, then retry.")
		case "conflict_details_unavailable":
			return controlError(http.StatusConflict, code, "This migrated conflict predates lossless candidate storage.", "Keep it disabled and recover from a current registry backup or re-enroll only after manual identity inspection.")
		case "conflict_generation_stale", "stale_plan":
			return controlError(http.StatusConflict, code, "The conflict candidate set changed after it was reviewed.", "Refresh maintenance state, select one candidate explicitly, and render a new plan.")
		case "conflict_jobs_active":
			return controlError(http.StatusConflict, code, "Conflict resolution is waiting for cache writers to quiesce.", "Cancel or wait for active jobs on this cache, then render and confirm the plan again.")
		case "conflict_candidate_unavailable", "conflict_candidate_identity_changed":
			return controlError(http.StatusConflict, code, "The selected private candidate can no longer be verified.", "Restore the candidate cache authority, refresh maintenance state, and render a new plan.")
		case "cache_clone_retired":
			return controlError(http.StatusConflict, code, "This cache clone path was retired by an earlier confirmed resolution.", "Use the retained canonical cache authority instead of re-enrolling the retired clone.")
		}
		return controlError(http.StatusConflict, code, "The requested control was rejected by current state.", "Refresh diagnostics and render a new plan.")
	}
	var conflict service.ErrConflict
	if errors.As(err, &conflict) {
		return controlError(http.StatusConflict, "binding_conflict", "The repository binding conflicts with current cache state.", "Refresh bindings, resolve repository ids or aliases, and render a new plan.")
	}
	var aliasConflict cache.ErrAliasConflict
	if errors.As(err, &aliasConflict) {
		return controlError(http.StatusConflict, "binding_conflict", "The repository alias conflicts with current cache state.", "Refresh bindings, choose unique aliases, and render a new plan.")
	}
	message := strings.ToLower(err.Error())
	if strings.Contains(message, "not found") || strings.Contains(message, "not bound") {
		return controlError(http.StatusNotFound, "target_not_found", "The selected binding or registration is unavailable.", "Refresh managed caches and bindings.")
	}
	return controlError(http.StatusBadRequest, "invalid_request", "The requested policy cannot be planned safely.", "Review the selected repository and policy fields, then render a new plan.")
}

func controlError(status int, code, message, remediation string) error {
	return adminhttp.ControlError{Status: status, Code: code, Message: message, Remediation: remediation}
}