package openai

import (
	"context"
	"encoding/json"
	"net/http"
	"strings"

	edgeservice "iop/apps/edge/internal/service"
)

// openAIRequestContext carries the routing, identity, and metrics values that
// every OpenAI-compatible surface resolves once at ingress, before any
// path-specific decode or dispatch. Chat and Responses share it so both
// surfaces classify and meter a request the same way.
//
// It is built at a single ingress point per surface and read-only afterwards.
type openAIRequestContext struct {
	r     *http.Request
	route routeDispatch
	// rawBody is the caller's original body, preserved verbatim for the tunnel
	// passthrough path. The rewritten provider body is produced separately by
	// the tunnel BuildBody hook and never replaces rawBody.
	rawBody []byte
	// callerMetadata is the parsed request metadata with the authenticated
	// principal applied over any caller-supplied identity. Context constructors
	// copy it before adding path-specific fields, so this ingress snapshot stays
	// immutable while the request moves through tunnel and normalized branches.
	callerMetadata map[string]string
	workspace      string
	estimate       int
	contextClass   string
	endpoint       string
}

// cloneMetadata derives a request-local metadata map. OpenAI ingress metadata
// is shared by multiple possible dispatch paths, while service requests need
// to add execution fields such as openai_stream and context_class. Copying at
// that boundary prevents one derived path from changing another's view.
func cloneMetadata(metadata map[string]string) map[string]string {
	cloned := make(map[string]string, len(metadata))
	for key, value := range metadata {
		cloned[key] = value
	}
	return cloned
}

// resolveCallerIdentity parses the caller metadata, applies the authenticated
// principal over it, and validates the workspace against the resolved route.
// Every OpenAI-compatible surface resolves identity through this one path so a
// caller-supplied iop_principal_* value can never win on any of them.
func resolveCallerIdentity(r *http.Request, route routeDispatch, rawMetadata json.RawMessage) (map[string]string, string, error) {
	runMeta, workspace, err := parseOpenAIMetadata(rawMetadata)
	if err != nil {
		return nil, "", err
	}
	// Overwrite (not merge-if-absent): the authenticated caller identity must
	// win over any caller-supplied metadata.iop_principal_* spoof attempt.
	for k, v := range principalMetadata(r.Context()) {
		runMeta[k] = v
	}
	if err := validateWorkspaceForRoute(route, workspace); err != nil {
		return nil, "", err
	}
	return runMeta, workspace, nil
}

// chatDispatchContext is the immutable per-request context of a dispatched chat
// completion. Everything downstream of ingress — provider-pool dispatch, live
// SSE, buffered SSE, and the non-stream completion — reads this context instead
// of taking the same values as a long parameter list.
//
// req is the effective request: the model-catalog generation policy is applied
// once at ingress, before the context is built, so no consumer sees a
// pre-policy request or re-applies the policy.
type chatDispatchContext struct {
	openAIRequestContext

	req          chatCompletionRequest
	basePrompt   string
	prompt       string
	input        map[string]any
	runMetadata  map[string]string
	outputPolicy strictOutputPolicy
	validation   toolValidationContract

	// submitReq is the run request for the resolved path: provider-pool shaped
	// (adapter/target resolved per candidate at admission) or direct.
	submitReq edgeservice.SubmitRunRequest
	// retrySubmit resubmits the run for a bounded tool-validation replay,
	// preserving the path the request was dispatched on.
	retrySubmit func(ctx context.Context, req edgeservice.SubmitRunRequest) (any, error)
}

// responsesRequestContext is the immutable ingress context for a
// /v1/responses request. It keeps the lenient routing envelope next to the
// common request context so raw provider tunnel paths never need to pass a
// parallel list of route, body, metadata, and estimate arguments.
type responsesRequestContext struct {
	openAIRequestContext
	envelope responsesEnvelope
}

// responsesDispatchContext is the normalized /v1/responses dispatch context.
// It is derived only after strict decode and policy application. The raw body
// remains on responsesRequestContext for the tunnel path; the normalized run
// request is represented separately by submitReq.
type responsesDispatchContext struct {
	*responsesRequestContext

	req          responsesRequest
	prompt       string
	input        map[string]any
	outputPolicy strictOutputPolicy
	runMetadata  map[string]string
	submitReq    edgeservice.SubmitRunRequest
}

func (dc *responsesDispatchContext) usageLabels(s *Server, responseMode string) usageLabels {
	return s.usageLabelsFor(dc.r.Context(), strings.TrimSpace(dc.req.Model), dc.endpoint, responseMode)
}

// withRetrySubmit derives a context bound to a retry function. The
// provider-pool retry closure can only be built once the pool request exists,
// so the pool path derives a new context rather than mutating this one.
func (dc *chatDispatchContext) withRetrySubmit(fn func(ctx context.Context, req edgeservice.SubmitRunRequest) (any, error)) *chatDispatchContext {
	derived := *dc
	derived.retrySubmit = fn
	return &derived
}

// usageLabels resolves the metric labels for this request's response mode.
func (dc *chatDispatchContext) usageLabels(s *Server, responseMode string) usageLabels {
	return s.usageLabelsFor(dc.r.Context(), strings.TrimSpace(dc.req.Model), dc.endpoint, responseMode)
}
