package openai

import (
	"errors"
	"fmt"
	"reflect"

	"iop/apps/edge/internal/authprojection"
	edgeservice "iop/apps/edge/internal/service"
	"iop/packages/go/config"
	"iop/packages/go/singlerequesttemplate"
)

var (
	errSingleRequestBindingMissingStage = errors.New("single-request binding: missing stage")
	errSingleRequestBindingDuplicate    = errors.New("single-request binding: duplicate stage role")
	errSingleRequestBindingUnauthorized = errors.New("single-request binding: stage model not authorized for principal")
	errSingleRequestBindingDynamic      = errors.New("single-request binding: stage model dynamically selected")
	errSingleRequestBindingInconsistent = errors.New("single-request binding: option-inconsistent stage")
)

// compileSingleRequestBinding builds the surface-neutral immutable admission
// value from an authorized execution preset and its resolved canonical
// bindings. It is called only after the preset's selector and every referenced
// stage model have been verified through their canonical catalog bindings for
// the authenticated principal.
//
// The function rejects missing, duplicate, unauthorized, dynamically selected,
// or option-inconsistent inputs without generic fallback. It keeps the
// external model echo equal to the requested public model.
func compileSingleRequestBinding(
	publicModel string,
	preset config.ExecutionPreset,
	bindings map[string]routeDispatch,
	view authprojection.AuthenticatedView,
) (*edgeservice.SingleRequestBinding, error) {
	if preset.SingleRequest == nil {
		return nil, nil
	}

	sr := preset.SingleRequest

	// Defense-in-depth: independently re-verify the approved fixed shape at the
	// admission boundary instead of trusting only the load-time config
	// validation. A refreshed or crafted preset that no longer matches the frozen
	// plan→work→review light shape must not compile an immutable admission.
	if err := validateFixedSingleRequestShape(preset, sr); err != nil {
		return nil, err
	}

	// Build the stage bindings from the preset's approved plan/work/review stage
	// map, resolved through the canonical bindings authorized for the principal.
	// Each stage's approved options come from the frozen policy config, never
	// from dynamic provider dispatch metadata.
	planBinding, err := resolveStageBinding("plan", sr.Stages.Plan, bindings, view)
	if err != nil {
		return nil, fmt.Errorf("single-request plan stage: %w", err)
	}
	workBinding, err := resolveStageBinding("work", sr.Stages.Work, bindings, view)
	if err != nil {
		return nil, fmt.Errorf("single-request work stage: %w", err)
	}
	reviewBinding, err := resolveStageBinding("review", sr.Stages.Review, bindings, view)
	if err != nil {
		return nil, fmt.Errorf("single-request review stage: %w", err)
	}

	srLimits := sr.Limits
	limits := edgeservice.SingleRequestLimits{
		WallClockMS:       srLimits.WallClockMS,
		StageTimeoutMS:    srLimits.StageTimeoutMS,
		MaxToolIterations: srLimits.MaxToolIterations,
		MaxOutputBytes:    srLimits.MaxOutputBytes,
	}

	effectivePlan := sr.Templates.EffectivePlan
	if effectivePlan == "" {
		effectivePlan = singlerequesttemplate.DefaultPlanTemplate
	}
	effectiveReview := sr.Templates.EffectiveReview
	if effectiveReview == "" {
		effectiveReview = singlerequesttemplate.DefaultReviewTemplate
	}

	return edgeservice.NewSingleRequestBindingWithTemplates(
		publicModel,
		sr.WorkspaceRef,
		*planBinding,
		*workBinding,
		*reviewBinding,
		limits,
		edgeservice.SingleRequestTemplateBinding{
			Plan:   effectivePlan,
			Review: effectiveReview,
		},
	)
}

// validateFixedSingleRequestShape re-verifies the approved immutable
// single-request shape at admission time. It independently confirms the
// selector, allowed modes, and the single light route match the frozen
// plan→work→review policy stages, including high reasoning on plan/review and
// no reasoning option on work. Every violation maps to a typed single-request
// binding error without generic fallback.
func validateFixedSingleRequestShape(preset config.ExecutionPreset, sr *config.ExecutionSingleRequestPolicy) error {
	// Allowed modes must be exactly ["light"].
	if len(preset.AllowedModes) != 1 || preset.AllowedModes[0] != config.ModeLight {
		return errSingleRequestBindingInconsistent
	}

	// The fused selector must exactly match the fixed plan stage (model and
	// options); the selector cannot diverge from the frozen plan binding.
	if preset.Selector.Model != sr.Stages.Plan.Model || !singleRequestOptionsEqual(preset.Selector.Options, sr.Stages.Plan.Options) {
		return errSingleRequestBindingInconsistent
	}

	// Plan and review must declare high reasoning; work must not declare it.
	if singleRequestReasoningEffort(sr.Stages.Plan.Options) != config.SingleRequestReasoningEffortHigh {
		return errSingleRequestBindingInconsistent
	}
	if singleRequestReasoningEffort(sr.Stages.Review.Options) != config.SingleRequestReasoningEffortHigh {
		return errSingleRequestBindingInconsistent
	}
	if _, present := sr.Stages.Work.Options["reasoning_effort"]; present {
		return errSingleRequestBindingInconsistent
	}

	// Exactly one light route with ordered, unique plan→work→review roles whose
	// model and options exactly match the frozen policy stages.
	if len(preset.Routes) != 1 {
		return errSingleRequestBindingInconsistent
	}
	route, ok := preset.Routes[config.ModeLight]
	if !ok {
		return errSingleRequestBindingMissingStage
	}
	expected := []struct {
		role  string
		stage config.ExecutionSingleRequestStageConfig
	}{
		{"plan", sr.Stages.Plan},
		{"work", sr.Stages.Work},
		{"review", sr.Stages.Review},
	}
	if len(route.Stages) != len(expected) {
		return errSingleRequestBindingMissingStage
	}
	seenRoles := make(map[string]struct{}, len(route.Stages))
	for _, stage := range route.Stages {
		if _, dup := seenRoles[stage.Role]; dup {
			return errSingleRequestBindingDuplicate
		}
		seenRoles[stage.Role] = struct{}{}
	}
	for i, want := range expected {
		st := route.Stages[i]
		if st.Role != want.role {
			return errSingleRequestBindingInconsistent
		}
		if st.Model != want.stage.Model {
			// The route would dynamically select a downstream model other than
			// the frozen policy stage model.
			return errSingleRequestBindingDynamic
		}
		if !singleRequestOptionsEqual(st.Options, want.stage.Options) {
			return errSingleRequestBindingInconsistent
		}
	}

	return nil
}

// resolveStageBinding maps a frozen stage config to its authorized routeDispatch
// binding and copies the approved stage options and managed route facts into a
// service DTO. It verifies that the binding is present, managed,
// principal-consistent, and names exactly the canonical model the frozen stage
// declares. The returned dispatch binding carries only secret-free managed
// route facts for provider-pool dispatch.
func resolveStageBinding(role string, stage config.ExecutionSingleRequestStageConfig, bindings map[string]routeDispatch, view authprojection.AuthenticatedView) (*edgeservice.SingleRequestStageBinding, error) {
	canonicalModel := stage.Model
	dispatch, ok := bindings[canonicalModel]
	if !ok {
		return nil, errSingleRequestBindingMissingStage
	}

	// The binding must come from a managed principal resolution. Unmanaged
	// legacy routes cannot back a single-request admission.
	if !dispatch.Managed {
		return nil, errSingleRequestBindingUnauthorized
	}

	// Verify the binding's model group matches the canonical reference.
	if dispatch.ModelGroupKey != canonicalModel {
		return nil, errSingleRequestBindingInconsistent
	}

	// Verify the binding's principal matches the authenticated view.
	if dispatch.PrincipalRef != view.Principal.PrincipalRef {
		return nil, errSingleRequestBindingUnauthorized
	}

	// Copy the approved stage-level options from the frozen policy stage config,
	// not from dynamic provider dispatch metadata. NewSingleRequestBinding takes
	// a defensive deep copy, so a later config refresh cannot mutate an admitted
	// binding through this reference.
	return &edgeservice.SingleRequestStageBinding{
		Model:    canonicalModel,
		Options:  stage.Options,
		Dispatch: managedRouteDispatchSnapshot(&dispatch),
	}, nil
}

// managedRouteDispatchSnapshot extracts the secret-free managed route facts from
// a resolved routeDispatch into a SingleRequestStageDispatchBinding. The
// returned value is a fresh allocation independent of the source; the caller
// may mutate the original without affecting the snapshot.
func managedRouteDispatchSnapshot(d *routeDispatch) *edgeservice.SingleRequestStageDispatchBinding {
	if d == nil || !d.Managed {
		return nil
	}
	return &edgeservice.SingleRequestStageDispatchBinding{
		Managed:              d.Managed,
		ModelGroupKey:        d.ModelGroupKey,
		RouteID:              d.RouteID,
		ProfileID:            d.ProfileID,
		CredentialSlotRef:    d.CredentialSlotRef,
		CredentialRevision:   d.CredentialRevision,
		RouteRevision:        d.RouteRevision,
		PrincipalRef:         d.PrincipalRef,
		ProjectionGeneration: d.ProjectionGeneration,
		ProviderID:           d.ProviderID,
		UpstreamModel:        d.UpstreamModel,
		TimeoutSec:           d.TimeoutSec,
		MaxQueue:             d.MaxQueue,
		QueueTimeoutMS:       d.QueueTimeoutMS,
		CandidatePredicate:   d.ManagedPredicate,
	}
}

// compileSingleRequestBindingForUnmanaged builds the service binding from an
// unmanaged (legacy) preset resolution. It rejects the compilation because
// single-request admission requires managed principal authorization.
func compileSingleRequestBindingForUnmanaged(publicModel string, preset config.ExecutionPreset) (*edgeservice.SingleRequestBinding, error) {
	if preset.SingleRequest == nil {
		return nil, nil
	}
	// Unmanaged presets cannot back a single-request admission because there
	// is no authenticated principal to verify stage authorization against.
	return nil, errSingleRequestBindingUnauthorized
}

// singleRequestOptionsEqual reports whether two option maps are equal, treating
// nil and empty maps as equal.
func singleRequestOptionsEqual(a, b map[string]any) bool {
	if len(a) == 0 && len(b) == 0 {
		return true
	}
	return reflect.DeepEqual(a, b)
}

// singleRequestReasoningEffort extracts the reasoning_effort option value from a
// stage's options map, returning "" when absent or non-string.
func singleRequestReasoningEffort(opts map[string]any) string {
	if opts == nil {
		return ""
	}
	v, ok := opts["reasoning_effort"]
	if !ok {
		return ""
	}
	s, ok := v.(string)
	if !ok {
		return ""
	}
	return s
}
