package openai

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

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

var (
	ErrRouteNotFound     = errors.New("route not found")
	ErrProjectionExpired = errors.New("projection is expired")
	ErrPrincipalRequired = errors.New("principal is required")
)

func (s *Server) advertisedModelsForPrincipal(ctx context.Context) ([]advertisedModel, error) {
	projection := s.PrincipalProjection()
	if projection == nil || projection.State() == authprojection.StateUnmanaged {
		return s.advertisedModels(), nil
	}
	view, ok := authenticatedProjectionViewFromContext(ctx)
	if !ok || strings.TrimSpace(view.Principal.PrincipalRef) == "" {
		if projection.State() == authprojection.StateExpired {
			return nil, ErrProjectionExpired
		}
		return nil, ErrPrincipalRequired
	}
	routes := view.Routes
	catalog := s.modelCatalogSnapshot()

	seen := make(map[string]struct{})
	var models []advertisedModel

	addModel := func(id, displayName string) {
		id = strings.TrimSpace(id)
		if id == "" {
			return
		}
		if _, exists := seen[id]; !exists {
			seen[id] = struct{}{}
			if displayName == "" {
				displayName = id
			}
			models = append(models, advertisedModel{
				ID:          id,
				DisplayName: displayName,
			})
		}
	}

	for _, r := range routes {
		id := strings.TrimSpace(r.RouteAlias)
		if id == "" {
			id = strings.TrimSpace(r.RouteID)
		}
		addModel(id, id)
	}

	for _, entry := range catalog {
		if entry.ExecutionPreset != "" {
			if _, err := s.resolveVirtualPresetModelForPrincipal(view, catalog, entry.ID, entry); err == nil {
				displayName := strings.TrimSpace(entry.DisplayName)
				addModel(entry.ID, displayName)
			}
		}
	}

	sort.Slice(models, func(i, j int) bool {
		return models[i].ID < models[j].ID
	})

	return models, nil
}

func (s *Server) resolveRouteDispatchForPrincipal(ctx context.Context, model string) (routeDispatch, error) {
	projection := s.PrincipalProjection()
	if _, ok := authenticatedProjectionViewFromContext(ctx); ok {
		return s.resolveProjectedRoute(ctx, model)
	}
	if projection != nil && projection.State() != authprojection.StateUnmanaged {
		if projection.State() == authprojection.StateExpired {
			return routeDispatch{}, ErrProjectionExpired
		}
		return routeDispatch{}, ErrPrincipalRequired
	}
	dispatch, ok := s.resolveRouteDispatch(model)
	if !ok {
		return routeDispatch{}, ErrRouteNotFound
	}
	return dispatch, nil
}

func (s *Server) resolveVirtualPresetModelForPrincipal(view authprojection.AuthenticatedView, modelCatalog []config.ModelCatalogEntry, virtualModelID string, entry config.ModelCatalogEntry) (routeDispatch, error) {
	preset, ok := s.ExecutionPreset(entry.ExecutionPreset)
	if !ok {
		return routeDispatch{}, ErrRouteNotFound
	}
	refs := preset.CanonicalModelReferences()
	if len(refs) == 0 {
		return routeDispatch{}, ErrRouteNotFound
	}

	routes := view.Routes
	bindings := make(map[string]routeDispatch, len(refs))

	for _, ref := range refs {
		if ref == virtualModelID {
			return routeDispatch{}, ErrRouteNotFound
		}
		// Authorize each canonical reference through its catalog binding rather
		// than the public route id/alias: exactly one principal route must resolve
		// to the canonical model group named by the preset reference. Routes whose
		// binding fails or names a different model group are simply not candidates.
		var matched []routeDispatch
		for i := range routes {
			binding, err := resolveManagedCatalogBinding(routes[i], modelCatalog)
			if err != nil || binding.ModelGroupKey != ref {
				continue
			}
			matched = append(matched, s.newManagedRouteDispatch(routes[i], binding, view.Generation))
		}
		if len(matched) != 1 {
			return routeDispatch{}, ErrRouteNotFound
		}
		bindings[ref] = matched[0]
	}

	selectorDispatch, ok := bindings[preset.Selector.Model]
	if !ok {
		return routeDispatch{}, ErrRouteNotFound
	}

	// The selector's projected route stays the credential authority: copy its full
	// dispatch and override only preset/public fields. RouteID, revisions, slot,
	// profile, principal, and predicate remain the selector's real projected
	// values, while the virtual model id is confined to public identity via
	// ExternalModelID and never leaks into credential/lease/fence checks.
	result := selectorDispatch
	result.UsageAttribution = entry.EffectiveUsageAttribution()
	result.IsPreset = true
	result.PresetID = entry.ExecutionPreset
	result.ExternalModelID = virtualModelID
	result.Preset = preset
	result.PresetResolvedBindings = bindings

	// Compile the surface-neutral immutable single-request admission when the
	// preset declares one and every canonical reference has been authorized
	// through the principal's managed bindings. The binding freezes public
	// identity, stage routes, workspace capability, and limits without refresh
	// mutation or dynamic fallback. A marked preset must fail closed: when
	// immutable compilation fails the whole resolution is rejected as the public
	// ErrRouteNotFound rather than returned with a nil SingleRequest. Presets
	// without a single-request policy keep the legacy nil binding.
	binding, err := compileSingleRequestBinding(virtualModelID, preset, bindings, view)
	if err != nil {
		return routeDispatch{}, ErrRouteNotFound
	}
	if binding != nil {
		result.SingleRequest = binding
	}

	return result, nil
}

// newManagedRouteDispatch builds the fully-resolved managed dispatch for one
// projected principal route and its resolved catalog binding. The route's public
// RouteID stays the credential authority; ModelGroupKey comes from the canonical
// catalog binding, never from the route id or alias.
func (s *Server) newManagedRouteDispatch(route authprojection.Route, binding managedCatalogBinding, generation uint64) routeDispatch {
	return routeDispatch{
		NodeRef:              s.cfg.NodeRef,
		ProviderID:           binding.ProviderID,
		UsageAttribution:     config.UsageAttributionProvider,
		SessionID:            s.resolveSessionID(),
		TimeoutSec:           s.resolveTimeoutSec(),
		ProviderPool:         true,
		Managed:              true,
		ModelGroupKey:        binding.ModelGroupKey,
		RouteID:              route.RouteID,
		CredentialSlotRef:    route.CredentialSlotRef,
		ProfileID:            route.ProfileID,
		UpstreamModel:        route.UpstreamModel,
		ResourceSelector:     route.ResourceSelector,
		RouteRevision:        route.RouteRevision,
		CredentialRevision:   route.CredentialRevision,
		PrincipalRef:         route.PrincipalRef,
		ProjectionGeneration: generation,
		ManagedPredicate:     managedRouteCandidatePredicate(route, binding.ProviderID),
	}
}

func (s *Server) resolveProjectedRoute(ctx context.Context, model string) (routeDispatch, error) {
	model = strings.TrimSpace(model)
	if model == "" {
		return routeDispatch{}, ErrRouteNotFound
	}

	p, ok := principalFromContext(ctx)
	if !ok || strings.TrimSpace(p.PrincipalRef) == "" {
		return routeDispatch{}, ErrPrincipalRequired
	}

	view, ok := authenticatedProjectionViewFromContext(ctx)
	if !ok || view.Principal.PrincipalRef != p.PrincipalRef {
		return routeDispatch{}, ErrPrincipalRequired
	}

	catalog := s.modelCatalogSnapshot()
	if catalogEntry := s.findProviderPoolEntry(model); catalogEntry != nil && catalogEntry.ExecutionPreset != "" {
		return s.resolveVirtualPresetModelForPrincipal(view, catalog, model, *catalogEntry)
	}

	routes := view.Routes
	var matchedRoute *authprojection.Route
	for i := range routes {
		r := &routes[i]
		if r.RouteID == model || (r.RouteAlias != "" && r.RouteAlias == model) {
			matchedRoute = r
			break
		}
	}

	if matchedRoute == nil {
		return routeDispatch{}, ErrRouteNotFound
	}

	binding, err := resolveManagedCatalogBinding(*matchedRoute, catalog)
	if err != nil {
		return routeDispatch{}, err
	}

	return s.newManagedRouteDispatch(*matchedRoute, binding, view.Generation), nil
}

type managedCatalogBinding struct{ ModelGroupKey, ProviderID string }

func resolveManagedCatalogBinding(route authprojection.Route, catalog []config.ModelCatalogEntry) (managedCatalogBinding, error) {
	selector := strings.TrimSpace(route.ResourceSelector)
	if selector == "" {
		return managedCatalogBinding{}, ErrRouteNotFound
	}
	routeAlias := strings.TrimSpace(route.RouteAlias)
	explicit := !strings.EqualFold(selector, "default")
	var matches []managedCatalogBinding
	var aliasMatches []managedCatalogBinding
	for _, entry := range catalog {
		group := strings.TrimSpace(entry.ID)
		if group == "" {
			continue
		}
		for providerID, servedModel := range entry.Providers {
			if strings.TrimSpace(servedModel) != strings.TrimSpace(route.UpstreamModel) {
				continue
			}
			if explicit && strings.TrimSpace(providerID) != selector {
				continue
			}
			binding := managedCatalogBinding{ModelGroupKey: group}
			if explicit {
				binding.ProviderID = strings.TrimSpace(providerID)
			}
			matches = append(matches, binding)
			if routeAlias != "" && routeAlias == group {
				aliasMatches = append(aliasMatches, binding)
			}
			break
		}
	}
	// A principal-owned public alias that exactly names a compatible catalog
	// group is an explicit disambiguator. This is required when two catalog
	// groups intentionally expose the same provider/upstream pair (for example
	// a general model and a preset-pinned fast alias). It never bypasses the
	// resource selector or upstream-model checks above.
	if len(aliasMatches) == 1 {
		return aliasMatches[0], nil
	}
	if len(aliasMatches) > 1 {
		return managedCatalogBinding{}, ErrRouteNotFound
	}
	if len(matches) != 1 {
		return managedCatalogBinding{}, ErrRouteNotFound
	}
	return matches[0], nil
}

func managedRouteCandidatePredicate(route authprojection.Route, providerID string) edgeservice.ProviderPoolCandidatePredicate {
	return func(c edgeservice.ProviderPoolCandidate) bool {
		if c.ExecutionPath != string(edgeservice.ProviderPoolPathTunnel) {
			return false
		}
		if providerID != "" && c.ProviderID != providerID {
			return false
		}
		if route.ProfileID != "" && c.ProfileID != route.ProfileID {
			return false
		}
		if route.UpstreamModel != "" && c.ActualModel != route.UpstreamModel {
			return false
		}
		return true
	}
}

func composeCandidatePredicates(p1, p2 edgeservice.ProviderPoolCandidatePredicate) edgeservice.ProviderPoolCandidatePredicate {
	if p1 == nil {
		return p2
	}
	if p2 == nil {
		return p1
	}
	return func(c edgeservice.ProviderPoolCandidate) bool {
		return p1(c) && p2(c)
	}
}

func (s *Server) writeManagedRouteError(w http.ResponseWriter, r *http.Request, err error) {
	if isAnthropicRequest(r) {
		s.writeAnthropicRouteError(w, err)
		return
	}
	writeError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
}

func (s *Server) writeAnthropicRouteError(w http.ResponseWriter, err error) {
	if errors.Is(err, ErrProjectionExpired) {
		writeAnthropicError(w, http.StatusBadRequest, "invalid_request_error", err.Error())
		return
	}
	writeAnthropicError(w, http.StatusBadRequest, "not_supported_error", "model does not resolve to a protocol profile")
}
