package pipeline

import (
	"fmt"
	"sort"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)

// ModuleDependencies contains run-scoped collaborators shared by constructed
// modules. Implementations retain only the dependencies they use.
type ModuleDependencies struct {
	LLM contracts.StructuredLLMClient
}

// BuildRequest contains the stable dependencies and configured options used to
// construct one module or validator for a run.
type BuildRequest struct {
	Dependencies ModuleDependencies
	Options      map[string]any
	References   contracts.ReferenceSet
}

// OptionValidator validates one module binding without constructing it.
type OptionValidator func(map[string]any) error

func rejectUnconfiguredOptions(options map[string]any) error {
	return RejectUnknownOptions(options)
}

func validateRegisteredOptions(validator OptionValidator, options map[string]any) error {
	if validator == nil {
		return fmt.Errorf("option validator must not be nil")
	}
	return validator(cloneOptions(options))
}

func cloneBuildRequest(request BuildRequest) BuildRequest {
	return BuildRequest{
		Dependencies: request.Dependencies,
		Options:      cloneOptions(request.Options),
		References:   CloneReferenceSet(request.References),
	}
}

// RejectUnknownOptions provides the common strict-map check used by module-
// owned option decoders. Values remain the implementation's responsibility.
func RejectUnknownOptions(options map[string]any, allowed ...string) error {
	known := make(map[string]struct{}, len(allowed))
	for _, key := range allowed {
		key = strings.TrimSpace(key)
		if key != "" {
			known[key] = struct{}{}
		}
	}
	unknown := make([]string, 0)
	for key := range options {
		if _, ok := known[key]; !ok {
			unknown = append(unknown, key)
		}
	}
	if len(unknown) == 0 {
		return nil
	}
	sort.Strings(unknown)
	if len(unknown) == 1 {
		return fmt.Errorf("unknown option %q", unknown[0])
	}
	return fmt.Errorf("unknown options %q", unknown)
}
