package runner

import (
	"context"
	"fmt"
	"strings"

	"github.com/yazanabuashour/openclerk/internal/domain"
	"github.com/yazanabuashour/openclerk/internal/runclient"
)

func RunRetrievalTask(ctx context.Context, config runclient.Config, request RetrievalTaskRequest) (RetrievalTaskResult, error) {
	normalized, rejection := normalizeRetrievalTaskRequest(request)
	if rejection != "" {
		return RetrievalTaskResult{
			Rejected:        true,
			RejectionReason: rejection,
			Summary:         rejection,
		}, nil
	}

	if normalized.Action == RetrievalTaskActionValidate {
		return RetrievalTaskResult{Summary: "valid"}, nil
	}

	autonomy, err := applyProfileDefaults(ctx, config, request.Autonomy)
	if err != nil {
		return RetrievalTaskResult{}, err
	}
	request.Autonomy = autonomy
	normalized, rejection = normalizeRetrievalTaskRequest(request)
	if rejection != "" {
		return RetrievalTaskResult{
			Rejected:        true,
			RejectionReason: rejection,
			Summary:         rejection,
		}, nil
	}

	if normalized.Action == RetrievalTaskActionWorkflowGuide {
		report := runWorkflowGuideReport(normalized.WorkflowGuide)
		return RetrievalTaskResult{
			WorkflowGuide: &report,
			Summary:       "returned workflow guide report",
		}, nil
	}

	if normalized.Action == RetrievalTaskActionRetrievalEvalCapture {
		var result RetrievalTaskResult
		err := runclient.WithWriteLock(ctx, config, func() error {
			client, err := runclient.Open(config)
			if err != nil {
				return err
			}
			defer func() {
				_ = client.Close()
			}()
			result, err = runRetrievalTaskWithClient(ctx, client, normalized, config)
			return err
		})
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return result, nil
	}

	if isMutatingRetrievalAction(normalized) {
		var result RetrievalTaskResult
		err := runclient.WithWriteLock(ctx, config, func() error {
			client, err := runclient.OpenForWrite(config)
			if err != nil {
				return err
			}
			defer func() {
				_ = client.Close()
			}()
			result, err = runRetrievalTaskWithClient(ctx, client, normalized, config)
			return err
		})
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return result, nil
	}

	client, err := runclient.OpenReadOnly(config)
	if err != nil {
		return RetrievalTaskResult{}, err
	}
	defer func() {
		_ = client.Close()
	}()

	return runRetrievalTaskWithClient(ctx, client, normalized, config)
}

func isMutatingRetrievalAction(normalized normalizedRetrievalTaskRequest) bool {
	return (normalized.Action == RetrievalTaskActionAuditContradictions && normalized.Audit.Mode == "repair_existing") ||
		(normalized.Action == RetrievalTaskActionSourceAuditReport && normalized.SourceAudit.Mode == "repair_existing")
}

func requiresRetrievalWriteApproval(normalized normalizedRetrievalTaskRequest) bool {
	return normalized.Action == RetrievalTaskActionRetrievalEvalCapture || isMutatingRetrievalAction(normalized)
}

func runRetrievalTaskWithClient(ctx context.Context, client *runclient.Client, normalized normalizedRetrievalTaskRequest, config runclient.Config) (RetrievalTaskResult, error) {
	switch normalized.Action {
	case RetrievalTaskActionSearch:
		search, err := client.Search(ctx, domain.SearchQuery{
			Text:          normalized.Search.Text,
			PathPrefix:    normalized.Search.PathPrefix,
			MetadataKey:   normalized.Search.MetadataKey,
			MetadataValue: normalized.Search.MetadataValue,
			Tag:           normalized.Search.Tag,
			Limit:         normalized.Search.Limit,
			Cursor:        normalized.Search.Cursor,
		})
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toSearchResult(search)
		return RetrievalTaskResult{
			Search:  &converted,
			Summary: fmt.Sprintf("returned %d search hits", len(converted.Hits)),
		}, nil
	case RetrievalTaskActionDocumentLinks:
		links, err := client.GetDocumentLinks(ctx, normalized.DocID)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toDocumentLinksResult(links)
		return RetrievalTaskResult{
			Links:   &converted,
			Summary: fmt.Sprintf("returned links for document %s", normalized.DocID),
		}, nil
	case RetrievalTaskActionGraph:
		graph, err := client.GraphNeighborhood(ctx, domain.GraphNeighborhoodInput{
			DocID:   normalized.DocID,
			ChunkID: normalized.ChunkID,
			NodeID:  normalized.NodeID,
			Limit:   normalized.Limit,
		})
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toGraphNeighborhood(graph)
		return RetrievalTaskResult{
			Graph:   &converted,
			Summary: fmt.Sprintf("returned %d graph nodes and %d edges", len(converted.Nodes), len(converted.Edges)),
		}, nil
	case RetrievalTaskActionRecordsLookup:
		records, err := client.LookupRecords(ctx, domain.RecordLookupInput{
			Text:       normalized.Records.Text,
			EntityType: normalized.Records.EntityType,
			Limit:      normalized.Records.Limit,
			Cursor:     normalized.Records.Cursor,
		})
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toRecordLookupResult(records)
		return RetrievalTaskResult{
			Records: &converted,
			Summary: fmt.Sprintf("returned %d record entities", len(converted.Entities)),
		}, nil
	case RetrievalTaskActionRecordEntity:
		entity, err := client.GetRecordEntity(ctx, normalized.EntityID)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toRecordEntity(entity)
		return RetrievalTaskResult{
			Entity:  &converted,
			Summary: fmt.Sprintf("returned record entity %s", converted.EntityID),
		}, nil
	case RetrievalTaskActionServicesLookup:
		services, err := client.LookupServices(ctx, domain.ServiceLookupInput{
			Text:      normalized.Services.Text,
			Status:    normalized.Services.Status,
			Owner:     normalized.Services.Owner,
			Interface: normalized.Services.Interface,
			Limit:     normalized.Services.Limit,
			Cursor:    normalized.Services.Cursor,
		})
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toServiceLookupResult(services)
		return RetrievalTaskResult{
			Services: &converted,
			Summary:  fmt.Sprintf("returned %d services", len(converted.Services)),
		}, nil
	case RetrievalTaskActionServiceRecord:
		service, err := client.GetServiceRecord(ctx, normalized.ServiceID)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toServiceRecord(service)
		return RetrievalTaskResult{
			Service: &converted,
			Summary: fmt.Sprintf("returned service %s", converted.ServiceID),
		}, nil
	case RetrievalTaskActionDecisionsLookup:
		decisions, err := client.LookupDecisions(ctx, domain.DecisionLookupInput{
			Text:   normalized.Decisions.Text,
			Status: normalized.Decisions.Status,
			Scope:  normalized.Decisions.Scope,
			Owner:  normalized.Decisions.Owner,
			Limit:  normalized.Decisions.Limit,
			Cursor: normalized.Decisions.Cursor,
		})
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toDecisionLookupResult(decisions)
		return RetrievalTaskResult{
			Decisions: &converted,
			Summary:   fmt.Sprintf("returned %d decisions", len(converted.Decisions)),
		}, nil
	case RetrievalTaskActionDecisionRecord:
		decision, err := client.GetDecisionRecord(ctx, normalized.DecisionID)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toDecisionRecord(decision)
		return RetrievalTaskResult{
			Decision: &converted,
			Summary:  fmt.Sprintf("returned decision %s", converted.DecisionID),
		}, nil
	case RetrievalTaskActionProvenanceEvents:
		events, err := client.ListProvenanceEvents(ctx, domain.ProvenanceEventQuery{
			RefKind:   normalized.Provenance.RefKind,
			RefID:     normalized.Provenance.RefID,
			SourceRef: normalized.Provenance.SourceRef,
			Limit:     normalized.Provenance.Limit,
			Cursor:    normalized.Provenance.Cursor,
		})
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toProvenanceEventList(events)
		return RetrievalTaskResult{
			Provenance: &converted,
			Summary:    fmt.Sprintf("returned %d provenance events", len(converted.Events)),
		}, nil
	case RetrievalTaskActionProjectionStates:
		projections, err := client.ListProjectionStates(ctx, domain.ProjectionStateQuery{
			Projection: normalized.Projection.Projection,
			RefKind:    normalized.Projection.RefKind,
			RefID:      normalized.Projection.RefID,
			Limit:      normalized.Projection.Limit,
			Cursor:     normalized.Projection.Cursor,
		})
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		converted := toProjectionStateList(projections)
		return RetrievalTaskResult{
			Projections: &converted,
			Summary:     fmt.Sprintf("returned %d projection states", len(converted.Projections)),
		}, nil
	case RetrievalTaskActionAuditContradictions:
		audit, err := runAuditContradictions(ctx, client, normalized.Audit)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			Audit:   &audit,
			Summary: auditContradictionsSummary(audit),
		}, nil
	case RetrievalTaskActionMemoryRouterRecall:
		report, err := runMemoryRouterRecallReport(ctx, client, normalized.MemoryRouterRecall)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			MemoryRouterRecall: &report,
			Summary:            "returned memory/router recall report",
		}, nil
	case RetrievalTaskActionSourceDiscovery:
		report, err := runSourceDiscoveryReport(ctx, client, normalized.SourceDiscovery)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			SourceDiscovery: &report,
			Summary:         "returned source discovery report",
		}, nil
	case RetrievalTaskActionSourceAuditReport:
		report, err := runSourceAuditReport(ctx, client, normalized.SourceAudit)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			SourceAudit: &report,
			Summary:     sourceAuditReportSummary(report),
		}, nil
	case RetrievalTaskActionEvidenceBundle:
		report, err := runEvidenceBundleReport(ctx, client, normalized.EvidenceBundle)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			EvidenceBundle: &report,
			Summary:        "returned evidence bundle report",
		}, nil
	case RetrievalTaskActionDecisionLookup:
		report, err := runDecisionLookupReport(ctx, client, normalized.DecisionLookup)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			DecisionLookup: &report,
			Summary:        "returned decision lookup report",
		}, nil
	case RetrievalTaskActionDuplicateCandidate:
		report, err := runDuplicateCandidateReport(ctx, client, normalized.DuplicateCandidate)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			DuplicateCandidate: &report,
			Summary:            "returned duplicate candidate report",
		}, nil
	case RetrievalTaskActionWorkflowGuide:
		report := runWorkflowGuideReport(normalized.WorkflowGuide)
		return RetrievalTaskResult{
			WorkflowGuide: &report,
			Summary:       "returned workflow guide report",
		}, nil
	case RetrievalTaskActionStructuredStore:
		report, err := runStructuredStoreReport(ctx, client, normalized.StructuredStore)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			StructuredStore: &report,
			Summary:         "returned structured store report",
		}, nil
	case RetrievalTaskActionHybridRetrieval:
		report, err := runHybridRetrievalReport(ctx, client, normalized.HybridRetrieval)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			HybridRetrieval: &report,
			Summary:         "returned hybrid retrieval report",
		}, nil
	case RetrievalTaskActionGraphContext:
		report, err := runGraphContextReport(ctx, client, normalized.GraphContext)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			GraphContext: &report,
			Summary:      "returned graph context report",
		}, nil
	case RetrievalTaskActionGraphRelationship:
		report, err := runGraphRelationshipReport(ctx, client, normalized.GraphRelationship)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			GraphRelationship: &report,
			Summary:           "returned graph relationship report",
		}, nil
	case RetrievalTaskActionGraphRelationshipMaintenance:
		plan, err := runGraphRelationshipMaintenancePlan(ctx, client, normalized.GraphRelationshipMaintenance)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			GraphRelationshipMaintenance: &plan,
			Summary:                      "returned graph relationship maintenance plan",
		}, nil
	case RetrievalTaskActionSemanticSearch:
		result, err := runSemanticSearch(ctx, client, normalized.SemanticSearch)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			SemanticSearch: &result,
			Summary:        fmt.Sprintf("returned %d semantic search hits", len(result.Hits)),
		}, nil
	case RetrievalTaskActionRetrievalEvalCapture:
		report, err := runRetrievalEvalCapture(ctx, client, normalized.RetrievalEval)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			RetrievalEvalCapture: &report,
			Summary:              "captured sanitized retrieval eval case",
		}, nil
	case RetrievalTaskActionRetrievalEvalReplay:
		report, err := runRetrievalEvalReplay(ctx, client, normalized.RetrievalReplay)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			RetrievalEvalReplay: &report,
			Summary:             fmt.Sprintf("replayed %d retrieval eval cases", report.ComparedCases),
		}, nil
	case RetrievalTaskActionSearchDiagnostics:
		report, err := runSearchDiagnosticsReport(ctx, client, config, normalized.SearchDiagnostics)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			SearchDiagnostics: &report,
			Summary:           "returned search diagnostics report",
		}, nil
	case RetrievalTaskActionMaintenanceReport:
		report, err := runMaintenanceReport(ctx, client, config, normalized.Maintenance)
		if err != nil {
			return RetrievalTaskResult{}, err
		}
		return RetrievalTaskResult{
			Maintenance: &report,
			Summary:     "returned maintenance report",
		}, nil
	default:
		return RetrievalTaskResult{}, fmt.Errorf("unsupported retrieval task action %q", normalized.Action)
	}
}

type normalizedRetrievalTaskRequest struct {
	Action                       string
	Autonomy                     AutonomyModes
	Search                       SearchOptions
	DocID                        string
	ChunkID                      string
	NodeID                       string
	EntityID                     string
	ServiceID                    string
	DecisionID                   string
	Records                      RecordLookupOptions
	Services                     ServiceLookupOptions
	Decisions                    DecisionLookupOptions
	Provenance                   ProvenanceEventOptions
	Projection                   ProjectionStateOptions
	Audit                        AuditContradictionsOptions
	MemoryRouterRecall           MemoryRouterRecallOptions
	SourceDiscovery              SourceDiscoveryOptions
	SourceAudit                  SourceAuditReportOptions
	EvidenceBundle               EvidenceBundleOptions
	DecisionLookup               DecisionLookupReportOptions
	DuplicateCandidate           DuplicateCandidateOptions
	WorkflowGuide                WorkflowGuideOptions
	StructuredStore              StructuredStoreOptions
	HybridRetrieval              HybridRetrievalOptions
	GraphContext                 GraphContextOptions
	GraphRelationship            GraphRelationshipOptions
	GraphRelationshipMaintenance GraphRelationshipMaintenanceOptions
	SemanticSearch               SemanticSearchOptions
	RetrievalEval                RetrievalEvalOptions
	RetrievalReplay              RetrievalReplayOptions
	SearchDiagnostics            SearchDiagnosticsOptions
	Maintenance                  MaintenanceReportOptions
	Limit                        int
}

func normalizeRetrievalTaskRequest(request RetrievalTaskRequest) (normalizedRetrievalTaskRequest, string) {
	action := strings.TrimSpace(request.Action)
	if action == "" {
		action = RetrievalTaskActionValidate
	}
	normalized := normalizedRetrievalTaskRequest{
		Action:                       action,
		Autonomy:                     request.Autonomy,
		Search:                       request.Search,
		DocID:                        strings.TrimSpace(request.DocID),
		ChunkID:                      strings.TrimSpace(request.ChunkID),
		NodeID:                       strings.TrimSpace(request.NodeID),
		EntityID:                     strings.TrimSpace(request.EntityID),
		ServiceID:                    strings.TrimSpace(request.ServiceID),
		DecisionID:                   strings.TrimSpace(request.DecisionID),
		Records:                      request.Records,
		Services:                     request.Services,
		Decisions:                    request.Decisions,
		Provenance:                   request.Provenance,
		Projection:                   request.Projection,
		Audit:                        request.Audit,
		MemoryRouterRecall:           request.MemoryRouterRecall,
		SourceDiscovery:              request.SourceDiscovery,
		SourceAudit:                  request.SourceAudit,
		EvidenceBundle:               request.EvidenceBundle,
		DecisionLookup:               request.DecisionLookup,
		DuplicateCandidate:           request.DuplicateCandidate,
		WorkflowGuide:                request.WorkflowGuide,
		StructuredStore:              request.StructuredStore,
		HybridRetrieval:              request.HybridRetrieval,
		GraphContext:                 request.GraphContext,
		GraphRelationship:            request.GraphRelationship,
		GraphRelationshipMaintenance: request.GraphRelationshipMaintenance,
		SemanticSearch:               request.SemanticSearch,
		RetrievalEval:                request.RetrievalEval,
		RetrievalReplay:              request.RetrievalReplay,
		SearchDiagnostics:            request.SearchDiagnostics,
		Maintenance:                  request.Maintenance,
		Limit:                        request.Limit,
	}

	autonomy, rejection := normalizeAutonomyModes(request.Autonomy)
	if rejection != "" {
		return normalizedRetrievalTaskRequest{}, rejection
	}
	normalized.Autonomy = autonomy

	if rejection := rejectNegativeRunnerLimits(
		request.Limit,
		request.Search.Limit,
		request.Records.Limit,
		request.Services.Limit,
		request.Decisions.Limit,
		request.Provenance.Limit,
		request.Projection.Limit,
		request.Audit.Limit,
		request.MemoryRouterRecall.Limit,
		request.SourceDiscovery.Limit,
		request.SourceAudit.Limit,
		request.EvidenceBundle.Limit,
		request.DecisionLookup.Limit,
		request.DuplicateCandidate.Limit,
		request.StructuredStore.Limit,
		request.HybridRetrieval.Limit,
		request.GraphContext.Limit,
		request.GraphRelationship.Limit,
		request.GraphRelationshipMaintenance.Limit,
		request.SemanticSearch.Limit,
		request.RetrievalEval.Search.Limit,
		request.RetrievalEval.SemanticSearch.Limit,
		request.RetrievalReplay.Limit,
		request.SearchDiagnostics.Limit,
		request.Maintenance.Limit,
	); rejection != "" {
		return normalizedRetrievalTaskRequest{}, rejection
	}

	switch action {
	case RetrievalTaskActionValidate:
		return normalized, ""
	case RetrievalTaskActionSearch:
		if strings.TrimSpace(request.Search.Text) == "" {
			return normalizedRetrievalTaskRequest{}, "search.text is required"
		}
		if rejection := normalizeSearchTagFilter(&normalized.Search); rejection != "" {
			return normalizedRetrievalTaskRequest{}, rejection
		}
		return normalized, ""
	case RetrievalTaskActionDocumentLinks:
		if normalized.DocID == "" {
			return normalizedRetrievalTaskRequest{}, "doc_id is required"
		}
		return normalized, ""
	case RetrievalTaskActionGraph:
		if normalized.DocID == "" && normalized.ChunkID == "" && normalized.NodeID == "" {
			return normalizedRetrievalTaskRequest{}, "doc_id, chunk_id, or node_id is required"
		}
		return normalized, ""
	case RetrievalTaskActionRecordsLookup:
		if strings.TrimSpace(request.Records.Text) == "" {
			return normalizedRetrievalTaskRequest{}, "records.text is required"
		}
		return normalized, ""
	case RetrievalTaskActionRecordEntity:
		if normalized.EntityID == "" {
			return normalizedRetrievalTaskRequest{}, "entity_id is required"
		}
		return normalized, ""
	case RetrievalTaskActionServicesLookup:
		return normalized, ""
	case RetrievalTaskActionServiceRecord:
		if normalized.ServiceID == "" {
			return normalizedRetrievalTaskRequest{}, "service_id is required"
		}
		return normalized, ""
	case RetrievalTaskActionDecisionsLookup:
		return normalized, ""
	case RetrievalTaskActionDecisionRecord:
		if normalized.DecisionID == "" {
			return normalizedRetrievalTaskRequest{}, "decision_id is required"
		}
		return normalized, ""
	case RetrievalTaskActionProvenanceEvents, RetrievalTaskActionProjectionStates:
		return normalized, ""
	case RetrievalTaskActionAuditContradictions:
		normalized.Audit.Query = strings.TrimSpace(request.Audit.Query)
		normalized.Audit.TargetPath = strings.TrimSpace(request.Audit.TargetPath)
		normalized.Audit.Mode = strings.TrimSpace(request.Audit.Mode)
		normalized.Audit.ConflictQuery = strings.TrimSpace(request.Audit.ConflictQuery)
		if normalized.Audit.Mode == "" {
			normalized.Audit.Mode = "plan_only"
		}
		if normalized.Audit.Query == "" {
			return normalizedRetrievalTaskRequest{}, "audit.query is required"
		}
		if normalized.Audit.TargetPath == "" {
			return normalizedRetrievalTaskRequest{}, "audit.target_path is required"
		}
		if normalized.Audit.Mode != "plan_only" && normalized.Audit.Mode != "repair_existing" {
			return normalizedRetrievalTaskRequest{}, "audit.mode must be plan_only or repair_existing"
		}
		if rejection := rejectRetrievalAutonomyWrite(normalized); rejection != "" {
			return normalizedRetrievalTaskRequest{}, rejection
		}
		return normalized, ""
	case RetrievalTaskActionMemoryRouterRecall:
		normalized.MemoryRouterRecall.Query = strings.TrimSpace(request.MemoryRouterRecall.Query)
		return normalized, ""
	case RetrievalTaskActionSourceDiscovery:
		normalized.SourceDiscovery.Query = strings.TrimSpace(request.SourceDiscovery.Query)
		normalized.SourceDiscovery.PathPrefix = strings.TrimSpace(request.SourceDiscovery.PathPrefix)
		if normalized.SourceDiscovery.Query == "" {
			return normalizedRetrievalTaskRequest{}, "source_discovery.query is required"
		}
		return normalized, ""
	case RetrievalTaskActionSourceAuditReport:
		normalized.SourceAudit.Query = strings.TrimSpace(request.SourceAudit.Query)
		normalized.SourceAudit.TargetPath = strings.TrimSpace(request.SourceAudit.TargetPath)
		normalized.SourceAudit.Mode = strings.TrimSpace(request.SourceAudit.Mode)
		normalized.SourceAudit.ConflictQuery = strings.TrimSpace(request.SourceAudit.ConflictQuery)
		if normalized.SourceAudit.Mode == "" {
			normalized.SourceAudit.Mode = "explain"
		}
		if normalized.SourceAudit.Query == "" {
			return normalizedRetrievalTaskRequest{}, "source_audit.query is required"
		}
		if normalized.SourceAudit.TargetPath == "" {
			return normalizedRetrievalTaskRequest{}, "source_audit.target_path is required"
		}
		if normalized.SourceAudit.Mode != "explain" && normalized.SourceAudit.Mode != "repair_existing" {
			return normalizedRetrievalTaskRequest{}, "source_audit.mode must be explain or repair_existing"
		}
		if rejection := rejectRetrievalAutonomyWrite(normalized); rejection != "" {
			return normalizedRetrievalTaskRequest{}, rejection
		}
		return normalized, ""
	case RetrievalTaskActionEvidenceBundle:
		normalized.EvidenceBundle.Query = strings.TrimSpace(request.EvidenceBundle.Query)
		normalized.EvidenceBundle.EntityID = strings.TrimSpace(request.EvidenceBundle.EntityID)
		normalized.EvidenceBundle.DecisionID = strings.TrimSpace(request.EvidenceBundle.DecisionID)
		normalized.EvidenceBundle.RefKind = strings.TrimSpace(request.EvidenceBundle.RefKind)
		normalized.EvidenceBundle.RefID = strings.TrimSpace(request.EvidenceBundle.RefID)
		normalized.EvidenceBundle.Projection = strings.TrimSpace(request.EvidenceBundle.Projection)
		if normalized.EvidenceBundle.Query == "" &&
			normalized.EvidenceBundle.EntityID == "" &&
			normalized.EvidenceBundle.DecisionID == "" &&
			(normalized.EvidenceBundle.RefKind == "" || normalized.EvidenceBundle.RefID == "") &&
			normalized.EvidenceBundle.Projection == "" {
			return normalizedRetrievalTaskRequest{}, "evidence_bundle query, entity_id, decision_id, ref_kind/ref_id, or projection is required"
		}
		if (normalized.EvidenceBundle.RefKind == "") != (normalized.EvidenceBundle.RefID == "") {
			return normalizedRetrievalTaskRequest{}, "evidence_bundle.ref_kind and evidence_bundle.ref_id must be provided together"
		}
		return normalized, ""
	case RetrievalTaskActionDecisionLookup:
		normalized.DecisionLookup.Query = strings.TrimSpace(request.DecisionLookup.Query)
		normalized.DecisionLookup.DecisionID = strings.TrimSpace(request.DecisionLookup.DecisionID)
		normalized.DecisionLookup.Status = strings.TrimSpace(request.DecisionLookup.Status)
		normalized.DecisionLookup.Scope = strings.TrimSpace(request.DecisionLookup.Scope)
		normalized.DecisionLookup.Owner = strings.TrimSpace(request.DecisionLookup.Owner)
		if normalized.DecisionLookup.Query == "" && normalized.DecisionLookup.DecisionID == "" {
			return normalizedRetrievalTaskRequest{}, "decision_lookup.query or decision_lookup.decision_id is required"
		}
		return normalized, ""
	case RetrievalTaskActionDuplicateCandidate:
		normalized.DuplicateCandidate.Query = strings.TrimSpace(request.DuplicateCandidate.Query)
		normalized.DuplicateCandidate.PathPrefix = strings.TrimSpace(request.DuplicateCandidate.PathPrefix)
		if normalized.DuplicateCandidate.Query == "" {
			return normalizedRetrievalTaskRequest{}, "duplicate_candidate.query is required"
		}
		return normalized, ""
	case RetrievalTaskActionWorkflowGuide:
		normalized.WorkflowGuide.Intent = strings.TrimSpace(request.WorkflowGuide.Intent)
		if normalized.WorkflowGuide.Intent == "" {
			return normalizedRetrievalTaskRequest{}, "workflow_guide.intent is required"
		}
		return normalized, ""
	case RetrievalTaskActionStructuredStore:
		normalized.StructuredStore.Domain = strings.TrimSpace(request.StructuredStore.Domain)
		normalized.StructuredStore.Query = strings.TrimSpace(request.StructuredStore.Query)
		normalized.StructuredStore.EntityType = strings.TrimSpace(request.StructuredStore.EntityType)
		normalized.StructuredStore.Status = strings.TrimSpace(request.StructuredStore.Status)
		normalized.StructuredStore.Owner = strings.TrimSpace(request.StructuredStore.Owner)
		normalized.StructuredStore.Interface = strings.TrimSpace(request.StructuredStore.Interface)
		normalized.StructuredStore.Scope = strings.TrimSpace(request.StructuredStore.Scope)
		if normalized.StructuredStore.Domain == "" {
			normalized.StructuredStore.Domain = "records"
		}
		if normalized.StructuredStore.Domain != "records" &&
			normalized.StructuredStore.Domain != "services" &&
			normalized.StructuredStore.Domain != "decisions" {
			return normalizedRetrievalTaskRequest{}, "structured_store.domain must be records, services, or decisions"
		}
		if rejection := structuredStoreFilterRejection(normalized.StructuredStore); rejection != "" {
			return normalizedRetrievalTaskRequest{}, rejection
		}
		return normalized, ""
	case RetrievalTaskActionHybridRetrieval:
		normalized.HybridRetrieval.Query = strings.TrimSpace(request.HybridRetrieval.Query)
		normalized.HybridRetrieval.PathPrefix = strings.TrimSpace(request.HybridRetrieval.PathPrefix)
		if normalized.HybridRetrieval.Query == "" {
			return normalizedRetrievalTaskRequest{}, "hybrid_retrieval.query is required"
		}
		return normalized, ""
	case RetrievalTaskActionGraphContext:
		normalized.GraphContext.DocID = strings.TrimSpace(request.GraphContext.DocID)
		normalized.GraphContext.Path = strings.TrimSpace(request.GraphContext.Path)
		normalized.GraphContext.Query = strings.TrimSpace(request.GraphContext.Query)
		normalized.GraphContext.PathPrefix = strings.TrimSpace(request.GraphContext.PathPrefix)
		selectors := 0
		for _, value := range []string{normalized.GraphContext.DocID, normalized.GraphContext.Path, normalized.GraphContext.Query} {
			if value != "" {
				selectors++
			}
		}
		if selectors == 0 {
			return normalizedRetrievalTaskRequest{}, "graph_context doc_id, path, or query is required"
		}
		if selectors > 1 {
			return normalizedRetrievalTaskRequest{}, "graph_context accepts exactly one of doc_id, path, or query"
		}
		if normalized.GraphContext.PathPrefix != "" && normalized.GraphContext.Query == "" {
			return normalizedRetrievalTaskRequest{}, "graph_context.path_prefix is only valid with graph_context.query"
		}
		return normalized, ""
	case RetrievalTaskActionGraphRelationship:
		normalized.GraphRelationship.DocID = strings.TrimSpace(request.GraphRelationship.DocID)
		normalized.GraphRelationship.Path = strings.TrimSpace(request.GraphRelationship.Path)
		normalized.GraphRelationship.Query = strings.TrimSpace(request.GraphRelationship.Query)
		normalized.GraphRelationship.PathPrefix = strings.TrimSpace(request.GraphRelationship.PathPrefix)
		selectors := 0
		for _, value := range []string{normalized.GraphRelationship.DocID, normalized.GraphRelationship.Path, normalized.GraphRelationship.Query} {
			if value != "" {
				selectors++
			}
		}
		if selectors == 0 {
			return normalizedRetrievalTaskRequest{}, "graph_relationship doc_id, path, or query is required"
		}
		if selectors > 1 {
			return normalizedRetrievalTaskRequest{}, "graph_relationship accepts exactly one of doc_id, path, or query"
		}
		if normalized.GraphRelationship.PathPrefix != "" && normalized.GraphRelationship.Query == "" {
			return normalizedRetrievalTaskRequest{}, "graph_relationship.path_prefix is only valid with graph_relationship.query"
		}
		return normalized, ""
	case RetrievalTaskActionGraphRelationshipMaintenance:
		normalized.GraphRelationshipMaintenance.DocID = strings.TrimSpace(request.GraphRelationshipMaintenance.DocID)
		normalized.GraphRelationshipMaintenance.Path = strings.TrimSpace(request.GraphRelationshipMaintenance.Path)
		normalized.GraphRelationshipMaintenance.Query = strings.TrimSpace(request.GraphRelationshipMaintenance.Query)
		normalized.GraphRelationshipMaintenance.PathPrefix = strings.TrimSpace(request.GraphRelationshipMaintenance.PathPrefix)
		selectors := 0
		for _, value := range []string{normalized.GraphRelationshipMaintenance.DocID, normalized.GraphRelationshipMaintenance.Path, normalized.GraphRelationshipMaintenance.Query} {
			if value != "" {
				selectors++
			}
		}
		if selectors == 0 {
			return normalizedRetrievalTaskRequest{}, "graph_relationship_maintenance doc_id, path, or query is required"
		}
		if selectors > 1 {
			return normalizedRetrievalTaskRequest{}, "graph_relationship_maintenance accepts exactly one of doc_id, path, or query"
		}
		if normalized.GraphRelationshipMaintenance.PathPrefix != "" && normalized.GraphRelationshipMaintenance.Query == "" {
			return normalizedRetrievalTaskRequest{}, "graph_relationship_maintenance.path_prefix is only valid with graph_relationship_maintenance.query"
		}
		return normalized, ""
	case RetrievalTaskActionSemanticSearch:
		normalized.SemanticSearch.Query = strings.TrimSpace(request.SemanticSearch.Query)
		pathPrefix, rejection := normalizeSemanticSearchPathPrefix(request.SemanticSearch.PathPrefix)
		if rejection != "" {
			return normalizedRetrievalTaskRequest{}, rejection
		}
		normalized.SemanticSearch.PathPrefix = pathPrefix
		normalized.SemanticSearch.MetadataKey = strings.TrimSpace(request.SemanticSearch.MetadataKey)
		normalized.SemanticSearch.MetadataValue = strings.TrimSpace(request.SemanticSearch.MetadataValue)
		normalized.SemanticSearch.Tag = request.SemanticSearch.Tag
		normalized.SemanticSearch.Provider = strings.ToLower(strings.TrimSpace(request.SemanticSearch.Provider))
		if normalized.SemanticSearch.Provider == "" {
			normalized.SemanticSearch.Provider = runclient.SemanticModuleProviderOllama
		}
		normalized.SemanticSearch.OllamaURL = strings.TrimRight(strings.TrimSpace(request.SemanticSearch.OllamaURL), "/")
		normalized.SemanticSearch.EmbeddingModel = strings.TrimSpace(request.SemanticSearch.EmbeddingModel)
		normalized.SemanticSearch.GeminiAPIBase = strings.TrimRight(strings.TrimSpace(request.SemanticSearch.GeminiAPIBase), "/")
		normalized.SemanticSearch.CacheDir = strings.TrimSpace(request.SemanticSearch.CacheDir)
		if normalized.SemanticSearch.OllamaURL != "" {
			return normalizedRetrievalTaskRequest{}, "semantic_search.ollama_url must be configured on the verified semantic module"
		}
		if normalized.SemanticSearch.GeminiAPIBase != "" {
			return normalizedRetrievalTaskRequest{}, "semantic_search.gemini_api_base must be configured on the verified semantic module"
		}
		if normalized.SemanticSearch.CacheDir != "" {
			return normalizedRetrievalTaskRequest{}, "semantic_search.cache_dir is managed by OpenClerk state"
		}
		if normalized.SemanticSearch.Query == "" {
			return normalizedRetrievalTaskRequest{}, "semantic_search.query is required"
		}
		if normalized.SemanticSearch.Provider != runclient.SemanticModuleProviderOllama && normalized.SemanticSearch.Provider != runclient.SemanticModuleProviderGemini {
			return normalizedRetrievalTaskRequest{}, "semantic_search.provider must be ollama or gemini"
		}
		if (normalized.SemanticSearch.MetadataKey == "") != (normalized.SemanticSearch.MetadataValue == "") {
			return normalizedRetrievalTaskRequest{}, "semantic_search.metadata_key and metadata_value must be provided together"
		}
		if rejection := normalizeSemanticSearchTagFilter(&normalized.SemanticSearch); rejection != "" {
			return normalizedRetrievalTaskRequest{}, rejection
		}
		if normalized.SemanticSearch.EmbeddingOutputDimensions < 0 {
			return normalizedRetrievalTaskRequest{}, "semantic_search.embedding_output_dimensions must be greater than or equal to 0"
		}
		return normalized, ""
	case RetrievalTaskActionRetrievalEvalCapture:
		normalized.RetrievalEval.Action = strings.TrimSpace(request.RetrievalEval.Action)
		normalized.RetrievalEval.CapturePath = strings.TrimSpace(request.RetrievalEval.CapturePath)
		if normalized.RetrievalEval.Action == "" {
			normalized.RetrievalEval.Action = RetrievalTaskActionSearch
		}
		switch normalized.RetrievalEval.Action {
		case RetrievalTaskActionSearch:
			nested, rejection := normalizeRetrievalTaskRequest(RetrievalTaskRequest{
				Action: RetrievalTaskActionSearch,
				Search: request.RetrievalEval.Search,
			})
			if rejection != "" {
				return normalizedRetrievalTaskRequest{}, strings.Replace(rejection, "search.", "retrieval_eval.search.", 1)
			}
			normalized.RetrievalEval.Search = nested.Search
			normalized.RetrievalEval.Search.Text = strings.TrimSpace(nested.Search.Text)
			if rejection := rejectRetrievalAutonomyWrite(normalized); rejection != "" {
				return normalizedRetrievalTaskRequest{}, rejection
			}
			return normalized, ""
		case RetrievalTaskActionSemanticSearch:
			nested, rejection := normalizeRetrievalTaskRequest(RetrievalTaskRequest{
				Action:         RetrievalTaskActionSemanticSearch,
				SemanticSearch: request.RetrievalEval.SemanticSearch,
			})
			if rejection != "" {
				return normalizedRetrievalTaskRequest{}, strings.Replace(rejection, "semantic_search.", "retrieval_eval.semantic_search.", 1)
			}
			normalized.RetrievalEval.SemanticSearch = nested.SemanticSearch
			if rejection := rejectRetrievalAutonomyWrite(normalized); rejection != "" {
				return normalizedRetrievalTaskRequest{}, rejection
			}
			return normalized, ""
		default:
			return normalizedRetrievalTaskRequest{}, "retrieval_eval.action must be search or semantic_search"
		}
	case RetrievalTaskActionRetrievalEvalReplay:
		normalized.RetrievalReplay.CapturePath = strings.TrimSpace(request.RetrievalReplay.CapturePath)
		return normalized, ""
	case RetrievalTaskActionSearchDiagnostics:
		normalized.SearchDiagnostics.Query = strings.TrimSpace(request.SearchDiagnostics.Query)
		normalized.SearchDiagnostics.Intent = strings.TrimSpace(request.SearchDiagnostics.Intent)
		normalized.SearchDiagnostics.PathPrefix = strings.TrimSpace(request.SearchDiagnostics.PathPrefix)
		normalized.SearchDiagnostics.MetadataKey = strings.TrimSpace(request.SearchDiagnostics.MetadataKey)
		normalized.SearchDiagnostics.MetadataValue = strings.TrimSpace(request.SearchDiagnostics.MetadataValue)
		normalized.SearchDiagnostics.Tag = request.SearchDiagnostics.Tag
		normalized.SearchDiagnostics.Provider = strings.ToLower(strings.TrimSpace(request.SearchDiagnostics.Provider))
		if normalized.SearchDiagnostics.Query == "" {
			return normalizedRetrievalTaskRequest{}, "search_diagnostics.query is required"
		}
		if normalized.SearchDiagnostics.Provider != "" &&
			normalized.SearchDiagnostics.Provider != runclient.SemanticModuleProviderOllama &&
			normalized.SearchDiagnostics.Provider != runclient.SemanticModuleProviderGemini {
			return normalizedRetrievalTaskRequest{}, "search_diagnostics.provider must be ollama or gemini"
		}
		if (normalized.SearchDiagnostics.MetadataKey == "") != (normalized.SearchDiagnostics.MetadataValue == "") {
			return normalizedRetrievalTaskRequest{}, "search_diagnostics.metadata_key and metadata_value must be provided together"
		}
		if rejection := normalizeSearchDiagnosticsTagFilter(&normalized.SearchDiagnostics); rejection != "" {
			return normalizedRetrievalTaskRequest{}, rejection
		}
		return normalized, ""
	case RetrievalTaskActionMaintenanceReport:
		normalized.Maintenance.Query = strings.TrimSpace(request.Maintenance.Query)
		normalized.Maintenance.DuplicateQuery = strings.TrimSpace(request.Maintenance.DuplicateQuery)
		normalized.Maintenance.DocID = strings.TrimSpace(request.Maintenance.DocID)
		normalized.Maintenance.Path = strings.TrimSpace(request.Maintenance.Path)
		normalized.Maintenance.PathPrefix = strings.TrimSpace(request.Maintenance.PathPrefix)
		return normalized, ""
	default:
		return normalizedRetrievalTaskRequest{}, fmt.Sprintf("unsupported retrieval task action %q", action)
	}
}

func rejectRetrievalAutonomyWrite(normalized normalizedRetrievalTaskRequest) string {
	if !requiresRetrievalWriteApproval(normalized) {
		return ""
	}
	if normalized.Autonomy.ApprovalMode == ApprovalModeProposeOnly {
		return "autonomy.approval_mode propose_only does not allow mutating retrieval actions"
	}
	return ""
}

func normalizeSearchTagFilter(search *SearchOptions) string {
	return normalizeTagFilter("search", search.Tag, search.tagProvided, &search.MetadataKey, &search.MetadataValue, &search.Tag)
}

func normalizeSemanticSearchTagFilter(search *SemanticSearchOptions) string {
	return normalizeTagFilter("semantic_search", search.Tag, search.tagProvided, &search.MetadataKey, &search.MetadataValue, &search.Tag)
}

func normalizeSemanticSearchPathPrefix(raw string) (string, string) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return "", ""
	}
	if containsParentPathSegment(trimmed) {
		return "", "semantic_search.path_prefix must stay inside the vault root"
	}
	clean, issue := domain.NormalizeOptionalVaultRelativePrefix(trimmed)
	switch issue {
	case domain.VaultPathOK:
		return clean, ""
	case domain.VaultPathAbsolute:
		return "", "semantic_search.path_prefix must be relative to the vault root"
	default:
		return "", "semantic_search.path_prefix must stay inside the vault root"
	}
}

func normalizeSearchDiagnosticsTagFilter(search *SearchDiagnosticsOptions) string {
	return normalizeTagFilter("search_diagnostics", search.Tag, search.tagProvided, &search.MetadataKey, &search.MetadataValue, &search.Tag)
}

func validateSemanticSearchOllamaURL(raw string) string {
	return validateOptionalRunnerLoopbackHTTPURL(raw, "semantic_search.ollama_url")
}

func validateSemanticSearchGeminiAPIBase(raw string) string {
	return validateOptionalRunnerCanonicalGeminiAPIBase(raw, "semantic_search.gemini_api_base")
}

func normalizeTagFilter(fieldPrefix string, tag string, tagProvided bool, metadataKey *string, metadataValue *string, normalizedTag *string) string {
	trimmedTag := strings.TrimSpace(tag)
	if trimmedTag == "" && (tagProvided || tag != "") {
		return fieldPrefix + ".tag must be non-empty"
	}
	if trimmedTag == "" {
		*normalizedTag = ""
		return ""
	}
	if strings.TrimSpace(*metadataKey) != "" || strings.TrimSpace(*metadataValue) != "" {
		return fieldPrefix + ".tag cannot be combined with metadata_key or metadata_value"
	}
	*metadataKey = "tag"
	*metadataValue = trimmedTag
	*normalizedTag = ""
	return ""
}
