package openai

import (
	edgeservice "iop/apps/edge/internal/service"
	"iop/packages/go/config"
	"strings"
)

// routeUsesProviderTunnel reports whether the resolved dispatch targets an
// OpenAI-compatible provider that serves raw tunnel passthrough. Provider-pool
// catalog routes and openai_compat/vllm type routes qualify; other
// legacy adapters keep the normalized RunEvent path.
func routeUsesProviderTunnel(d routeDispatch) bool {
	if d.ProviderPool {
		return true
	}
	switch strings.TrimSpace(d.Adapter) {
	case "openai_compat", "vllm":
		return true
	default:
		return false
	}
}

func routeSupportsNativeToolCalls(dispatch routeDispatch) bool {
	if dispatch.ProviderPool {
		return true
	}
	switch strings.TrimSpace(dispatch.Adapter) {
	case "openai_compat", "vllm", "ollama":
		return true
	default:
		return false
	}
}

func (s *Server) resolveAdapter() string {
	if s.cfg.Adapter != "" {
		return s.cfg.Adapter
	}
	return "ollama"
}

func (s *Server) resolveTarget(model string) string {
	if s.cfg.Target != "" {
		return s.cfg.Target
	}
	return strings.TrimSpace(model)
}

// routeDispatch holds fully-resolved dispatch parameters for a single request.
type routeDispatch struct {
	NodeRef          string
	ProviderID       string
	UsageAttribution string
	Adapter          string
	Target           string
	SessionID        string
	TimeoutSec       int
	MaxQueue         int
	QueueTimeoutMS   int
	// ProviderPool is true when the request model matched a provider-pool
	// catalog entry. Adapter and Target are empty; the service layer resolves
	// them per-candidate and rewrites Target after admission.
	ProviderPool bool

	Managed              bool
	ModelGroupKey        string
	RouteID              string
	CredentialSlotRef    string
	ProfileID            string
	UpstreamModel        string
	ResourceSelector     string
	RouteRevision        uint64
	CredentialRevision   uint64
	PrincipalRef         string
	ProjectionGeneration uint64
	ManagedPredicate     edgeservice.ProviderPoolCandidatePredicate

	IsPreset               bool
	PresetID               string
	ExternalModelID        string
	Preset                 config.ExecutionPreset
	PresetResolvedBindings map[string]routeDispatch

	// SingleRequest is the optional surface-neutral immutable admission value
	// compiled at request start for authorized single-request presets. When
	// non-nil it freezes public identity, canonical plan/work/review bindings,
	// opaque workspace capability, and absolute limits. It is independent of
	// the credential/route identity and is never echoed to the caller.
	SingleRequest *edgeservice.SingleRequestBinding
}

func (d routeDispatch) credentialBinding() *edgeservice.CredentialBinding {
	if !d.Managed {
		return nil
	}
	return &edgeservice.CredentialBinding{
		PrincipalRef: d.PrincipalRef, CredentialSlotRef: d.CredentialSlotRef,
		RouteID: d.RouteID, ProfileID: d.ProfileID, CredentialRevision: d.CredentialRevision,
		RouteRevision: d.RouteRevision, ProjectionGeneration: d.ProjectionGeneration,
	}
}

func (d routeDispatch) CandidatePredicate() edgeservice.ProviderPoolCandidatePredicate {
	return d.ManagedPredicate
}

func (d routeDispatch) effectiveModelGroupKey(requestModel string) string {
	if key := strings.TrimSpace(d.ModelGroupKey); key != "" {
		return key
	}
	return strings.TrimSpace(requestModel)
}

// resolveRoute returns the first catalog entry whose Model matches model.
// Entries with an empty Target are skipped.
func (s *Server) resolveRoute(model string) *config.OpenAIRouteEntry {
	model = strings.TrimSpace(model)
	if model == "" {
		return nil
	}
	for i := range s.cfg.ModelRoutes {
		r := &s.cfg.ModelRoutes[i]
		if strings.TrimSpace(r.Model) == model && r.Target != "" {
			return r
		}
	}
	return nil
}

// findProviderPoolEntry returns the catalog entry matching model, or nil.
func (s *Server) findProviderPoolEntry(model string) *config.ModelCatalogEntry {
	model = strings.TrimSpace(model)
	if model == "" {
		return nil
	}
	modelCatalog := s.modelCatalogSnapshot()
	for i := range modelCatalog {
		if modelCatalog[i].ID == model {
			entry := modelCatalog[i]
			return &entry
		}
	}
	return nil
}

// resolveRouteDispatch returns fully-resolved dispatch params for model.
// Priority: provider-pool catalog → legacy model_routes → single-target fallback.
// Returns (dispatch, true) on success; (zero, false) when no target can be resolved.
func (s *Server) resolveRouteDispatch(model string) (routeDispatch, bool) {
	// Provider-pool catalog takes highest priority.
	if catalogEntry := s.findProviderPoolEntry(model); catalogEntry != nil {
		if catalogEntry.ExecutionPreset != "" {
			preset, ok := s.ExecutionPreset(catalogEntry.ExecutionPreset)
			if !ok {
				return routeDispatch{}, false
			}
			refs := preset.CanonicalModelReferences()
			bindings := make(map[string]routeDispatch, len(refs))
			for _, ref := range refs {
				if ref == model {
					return routeDispatch{}, false
				}
				if len(s.modelCatalogSnapshot()) > 0 {
					if s.findProviderPoolEntry(ref) == nil && s.resolveRoute(ref) == nil {
						return routeDispatch{}, false
					}
				}
				refDispatch, ok := s.resolveRouteDispatch(ref)
				if !ok {
					return routeDispatch{}, false
				}
				bindings[ref] = refDispatch
			}
			selectorDispatch := bindings[preset.Selector.Model]
			disp := routeDispatch{
				NodeRef:                selectorDispatch.NodeRef,
				ProviderID:             selectorDispatch.ProviderID,
				UsageAttribution:       catalogEntry.EffectiveUsageAttribution(),
				Adapter:                selectorDispatch.Adapter,
				Target:                 selectorDispatch.Target,
				SessionID:              s.resolveSessionID(),
				TimeoutSec:             s.resolveTimeoutSec(),
				MaxQueue:               selectorDispatch.MaxQueue,
				QueueTimeoutMS:         selectorDispatch.QueueTimeoutMS,
				ProviderPool:           true,
				IsPreset:               true,
				PresetID:               catalogEntry.ExecutionPreset,
				ExternalModelID:        model,
				Preset:                 preset,
				PresetResolvedBindings: bindings,
			}

			// Fail closed for unmanaged marked single-request presets. There is no
			// authenticated principal to verify stage authorization against, so a
			// marked preset can never compile a valid immutable admission. A preset
			// that cannot produce a binding must not resolve or be advertised, so we
			// reject the dispatch instead of returning it with a nil SingleRequest.
			// Unmarked (legacy) presets keep SingleRequest == nil and resolve normally.
			if preset.SingleRequest != nil {
				if _, err := compileSingleRequestBindingForUnmanaged(model, preset); err != nil {
					return routeDispatch{}, false
				}
			}

			return disp, true
		}
		return routeDispatch{
			UsageAttribution: catalogEntry.EffectiveUsageAttribution(),
			SessionID:        s.resolveSessionID(),
			TimeoutSec:       s.resolveTimeoutSec(),
			ProviderPool:     true,
		}, true
	}

	if route := s.resolveRoute(model); route != nil {
		providerID := strings.TrimSpace(route.ProviderID)
		if providerID == "" {
			providerID = strings.TrimSpace(s.cfg.ProviderID)
		}
		adapter := route.Adapter
		if adapter == "" {
			adapter = s.resolveAdapter()
		}
		nodeRef := route.NodeRef
		if nodeRef == "" {
			nodeRef = s.cfg.NodeRef
		}
		sessionID := route.SessionID
		if sessionID == "" {
			sessionID = s.resolveSessionID()
		}
		timeoutSec := route.TimeoutSec
		if timeoutSec <= 0 {
			timeoutSec = s.resolveTimeoutSec()
		}
		return routeDispatch{
			NodeRef:          nodeRef,
			ProviderID:       providerID,
			UsageAttribution: config.UsageAttributionProvider,
			Adapter:          adapter,
			Target:           route.Target,
			SessionID:        sessionID,
			TimeoutSec:       timeoutSec,
			MaxQueue:         route.MaxQueue,
			QueueTimeoutMS:   route.QueueTimeoutMS,
		}, true
	}
	target := s.resolveTarget(model)
	if target == "" {
		return routeDispatch{}, false
	}
	return routeDispatch{
		NodeRef:          s.cfg.NodeRef,
		ProviderID:       strings.TrimSpace(s.cfg.ProviderID),
		UsageAttribution: config.UsageAttributionProvider,
		Adapter:          s.resolveAdapter(),
		Target:           target,
		SessionID:        s.resolveSessionID(),
		TimeoutSec:       s.resolveTimeoutSec(),
	}, true
}

func (s *Server) resolveSessionID() string {
	if s.cfg.SessionID != "" {
		return s.cfg.SessionID
	}
	return edgeservice.DefaultSessionID
}

func (s *Server) resolveTimeoutSec() int {
	if s.cfg.TimeoutSec > 0 {
		return s.cfg.TimeoutSec
	}
	return edgeservice.DefaultTimeoutSec
}
