package openai

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"sync"
	"sync/atomic"

	"iop/packages/go/streamgate"
)

const (
	openAIRebuildEndpointChat      = "/v1/chat/completions"
	openAIRebuildEndpointResponses = "/v1/responses"
	openAIRebuildFamily            = "openai.json"
)

// openAIRepeatResumeDirective is the fixed English continuation instruction the
// resume-notice builder writes when a repeat-loop continuation plan is selected.
// It is byte-stable: recovery never rewrites, summarizes, or translates it, and
// it is the only non-model text added to a resume body.
const openAIRepeatResumeDirective = "The previous model output was stopped after a repetition loop was detected. Continue from the provided content and reasoning output without repeating already generated text."

// openAIRebuildContextReserveTokens is the completion-token headroom reserved on
// top of the measured resume prompt when comparing against the target model's
// context window. A resume body whose prompt plus this reserve exceeds the
// window (or whose window is unknown) is refused before any dispatch.
const openAIRebuildContextReserveTokens = 256

var openAIRebuiltSequence atomic.Uint64

var (
	errOpenAIRecoveryPatchDuplicate    = errors.New("duplicate OpenAI recovery patch")
	errOpenAIRebuildContextOverflow    = errors.New("OpenAI resume request exceeds the target model context window")
	errOpenAIRecoverySourceUnavailable = errors.New("OpenAI recovery source snapshot is unavailable")
	errOpenAIRecoverySourceCursorRange = errors.New("OpenAI recovery source cursor is out of range")
)

// openAIRecoverySourceStore is the request-local recorder of the current
// attempt's model output. It captures text and reasoning deltas in event order
// per channel behind a bounded byte cap and exposes only a stable snapshot ref
// and cursor to filters, never the raw output. The Rebuilder consumes a snapshot
// once, after the attempt that produced it is aborted, to assemble an
// endpoint-specific continuation body. Its ref is the ingress recovery ref so a
// continuation directive can address it without leaking model output.
type openAIRecoverySourceStore struct {
	mu        sync.Mutex
	ref       string
	maxBytes  int
	content   []byte
	reasoning []byte
	// suppressContent/suppressReasoning are one-attempt known-prefix guards.
	// consume installs the safe continuation prefix and the replacement event
	// source removes it only when the provider echoes it byte-for-byte.
	suppressContent   []byte
	suppressReasoning []byte
	overflow          bool
	consumed          bool
	closed            bool
}

func newOpenAIRecoverySourceStore(ingress *openAIIngressSnapshot) *openAIRecoverySourceStore {
	store := &openAIRecoverySourceStore{}
	if ref, err := ingress.recoveryRef(); err == nil {
		store.ref = ref.SnapshotRef()
		store.maxBytes = int(ref.MaxBytes())
	}
	return store
}

func (s *openAIRecoverySourceStore) snapshotRef() string {
	if s == nil {
		return ""
	}
	return s.ref
}

func (s *openAIRecoverySourceStore) recordText(text string)      { s.record(true, text) }
func (s *openAIRecoverySourceStore) recordReasoning(text string) { s.record(false, text) }

func (s *openAIRecoverySourceStore) record(isContent bool, text string) {
	if s == nil || text == "" {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return
	}
	if s.maxBytes > 0 && len(s.content)+len(s.reasoning)+len(text) > s.maxBytes {
		s.overflow = true
		return
	}
	if isContent {
		s.content = append(s.content, text...)
	} else {
		s.reasoning = append(s.reasoning, text...)
	}
}

// resetAttempt clears the recorded output at the start of a new attempt so a
// snapshot only ever contains the single attempt that produced it.
func (s *openAIRecoverySourceStore) resetAttempt() {
	if s == nil {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return
	}
	s.content = s.content[:0]
	s.reasoning = s.reasoning[:0]
	s.overflow = false
	s.consumed = false
}

// consume returns the recorded channels with the repeated tail removed. A
// cursor at or below content length addresses the content channel. A cursor
// above content length uses content-length+1 as a reasoning-channel sentinel
// and addresses the reasoning prefix after it. This keeps the public directive
// raw-free and single-field while preserving a rune-boundary byte cursor for
// either model-output channel.
func (s *openAIRecoverySourceStore) consume(cursor int) (string, string, error) {
	if s == nil {
		return "", "", streamgate.ErrIngressSnapshotClosed
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return "", "", streamgate.ErrIngressSnapshotClosed
	}
	if s.consumed {
		return "", "", errOpenAIRecoverySourceUnavailable
	}
	if s.overflow {
		return "", "", errOpenAIRebuildContextOverflow
	}
	if len(s.content)+len(s.reasoning) == 0 {
		return "", "", errOpenAIRecoverySourceUnavailable
	}
	if cursor < 0 {
		return "", "", errOpenAIRecoverySourceCursorRange
	}
	contentEnd, reasoningEnd := len(s.content), len(s.reasoning)
	if cursor <= len(s.content) {
		contentEnd = cursor
	} else {
		reasoningEnd = cursor - len(s.content) - 1
		if reasoningEnd < 0 || reasoningEnd > len(s.reasoning) {
			return "", "", errOpenAIRecoverySourceCursorRange
		}
	}
	s.consumed = true
	content := string(s.content[:contentEnd])
	reasoning := string(s.reasoning[:reasoningEnd])
	s.suppressContent = append(s.suppressContent[:0], content...)
	s.suppressReasoning = append(s.suppressReasoning[:0], reasoning...)
	return content, reasoning, nil
}

func (s *openAIRecoverySourceStore) suppressKnownPrefix(isContent bool, value string) string {
	if s == nil || value == "" {
		return value
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	var expected *[]byte
	if isContent {
		expected = &s.suppressContent
	} else {
		expected = &s.suppressReasoning
	}
	if len(*expected) == 0 {
		return value
	}
	incoming := []byte(value)
	switch {
	case len(incoming) <= len(*expected) && string((*expected)[:len(incoming)]) == value:
		*expected = (*expected)[len(incoming):]
		return ""
	case len(incoming) > len(*expected) && string(incoming[:len(*expected)]) == string(*expected):
		value = string(incoming[len(*expected):])
		*expected = nil
		return value
	default:
		// The replacement started with novel output. Disable suppression
		// immediately so a coincidental later substring is never removed.
		*expected = nil
		return value
	}
}

func (s *openAIRecoverySourceStore) close() {
	if s == nil {
		return
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return
	}
	s.closed = true
	s.content = nil
	s.reasoning = nil
	s.suppressContent = nil
	s.suppressReasoning = nil
}

// openAIRecoverySourceEventSource wraps a NormalizedEventSource so every text and
// reasoning delta the Core reads is recorded into the request-local source store
// before it reaches any filter. It resets the store on each response start so
// each attempt records only its own output. It is otherwise a transparent
// passthrough and never mutates events.
type openAIRecoverySourceEventSource struct {
	inner streamgate.NormalizedEventSource
	store *openAIRecoverySourceStore
}

func newOpenAIRecoverySourceEventSource(inner streamgate.NormalizedEventSource, store *openAIRecoverySourceStore) streamgate.NormalizedEventSource {
	if store == nil {
		return inner
	}
	return &openAIRecoverySourceEventSource{inner: inner, store: store}
}

func (s *openAIRecoverySourceEventSource) NextEvent(ctx context.Context) (streamgate.NormalizedEvent, error) {
	for {
		ev, err := s.inner.NextEvent(ctx)
		if err != nil {
			return ev, err
		}
		switch ev.Kind() {
		case streamgate.EventKindResponseStart:
			s.store.resetAttempt()
		case streamgate.EventKindTextDelta:
			if value, valueErr := ev.AsTextDelta(); valueErr == nil {
				value = s.store.suppressKnownPrefix(true, value)
				if value == "" {
					continue
				}
				s.store.recordText(value)
				ev, err = streamgate.NewTextDeltaEvent(ev.Channel(), value, ev.Timestamp())
				if err != nil {
					return streamgate.NormalizedEvent{}, err
				}
			}
		case streamgate.EventKindReasoningDelta:
			if value, valueErr := ev.AsReasoningDelta(); valueErr == nil {
				value = s.store.suppressKnownPrefix(false, value)
				if value == "" {
					continue
				}
				s.store.recordReasoning(value)
				ev, err = streamgate.NewReasoningDeltaEvent(ev.Channel(), value, ev.Timestamp())
				if err != nil {
					return streamgate.NormalizedEvent{}, err
				}
			}
		}
		return ev, err
	}
}

var _ streamgate.NormalizedEventSource = (*openAIRecoverySourceEventSource)(nil)

type openAIRecoveryPatchEntry struct {
	cursor int
	value  []byte
	guard  *streamgate.IngressSnapshotRebuildGuard
	once   sync.Once
}

func (e *openAIRecoveryPatchEntry) release() {
	if e == nil {
		return
	}
	e.once.Do(func() {
		e.value = nil
		if e.guard != nil {
			e.guard.Close()
			e.guard = nil
		}
	})
}

// openAIRecoveryPatchStore is request-local. It retains only the typed subtree
// named by a safe directive reference; it never retains a second request tree,
// caller headers, or provider credentials.
type openAIRecoveryPatchStore struct {
	mu           sync.Mutex
	ingress      *openAIIngressSnapshot
	continuation map[string]*openAIRecoveryPatchEntry
	schema       map[string]*openAIRecoveryPatchEntry
	closed       bool
}

func newOpenAIRecoveryPatchStore(ingress *openAIIngressSnapshot) *openAIRecoveryPatchStore {
	return &openAIRecoveryPatchStore{
		ingress:      ingress,
		continuation: make(map[string]*openAIRecoveryPatchEntry),
		schema:       make(map[string]*openAIRecoveryPatchEntry),
	}
}

func marshalOpenAIPatchValue(value any) ([]byte, error) {
	if raw, ok := value.(json.RawMessage); ok {
		if !json.Valid(raw) {
			return nil, fmt.Errorf("invalid OpenAI recovery patch JSON")
		}
		return raw, nil
	}
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil, err
	}
	return encoded, nil
}

func (s *openAIRecoveryPatchStore) PutContinuation(snapshotRef string, cursor int, value any) error {
	if _, err := streamgate.NewStableTokenRequired("snapshotRef", snapshotRef); err != nil {
		return err
	}
	if cursor < 0 {
		return fmt.Errorf("continuation cursor must be non-negative")
	}
	encoded, err := marshalOpenAIPatchValue(value)
	if err != nil {
		return err
	}
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return streamgate.ErrIngressSnapshotClosed
	}
	if _, duplicate := s.continuation[snapshotRef]; duplicate {
		s.mu.Unlock()
		return errOpenAIRecoveryPatchDuplicate
	}
	ingress := s.ingress
	s.mu.Unlock()
	guard, err := ingress.reserveRebuild(int64(len(encoded)))
	if err != nil {
		return err
	}
	entry := &openAIRecoveryPatchEntry{cursor: cursor, value: encoded, guard: guard}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		entry.release()
		return streamgate.ErrIngressSnapshotClosed
	}
	if _, duplicate := s.continuation[snapshotRef]; duplicate {
		entry.release()
		return errOpenAIRecoveryPatchDuplicate
	}
	s.continuation[snapshotRef] = entry
	return nil
}

func openAISchemaPatchKey(schemaRef, patchCode string) string {
	return schemaRef + ":" + patchCode
}

func (s *openAIRecoveryPatchStore) PutSchema(schemaRef, patchCode string, value any) error {
	if _, err := streamgate.NewStableTokenRequired("schemaRef", schemaRef); err != nil {
		return err
	}
	if _, err := streamgate.NewStableTokenRequired("patchCode", patchCode); err != nil {
		return err
	}
	encoded, err := marshalOpenAIPatchValue(value)
	if err != nil {
		return err
	}
	key := openAISchemaPatchKey(schemaRef, patchCode)
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return streamgate.ErrIngressSnapshotClosed
	}
	if _, duplicate := s.schema[key]; duplicate {
		s.mu.Unlock()
		return errOpenAIRecoveryPatchDuplicate
	}
	ingress := s.ingress
	s.mu.Unlock()
	guard, err := ingress.reserveRebuild(int64(len(encoded)))
	if err != nil {
		return err
	}
	entry := &openAIRecoveryPatchEntry{value: encoded, guard: guard}
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		entry.release()
		return streamgate.ErrIngressSnapshotClosed
	}
	if _, duplicate := s.schema[key]; duplicate {
		entry.release()
		return errOpenAIRecoveryPatchDuplicate
	}
	s.schema[key] = entry
	return nil
}

func (s *openAIRecoveryPatchStore) takeContinuation(snapshotRef string, cursor int) (*openAIRecoveryPatchEntry, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return nil, streamgate.ErrIngressSnapshotClosed
	}
	entry := s.continuation[snapshotRef]
	if entry == nil || entry.cursor != cursor {
		return nil, fmt.Errorf("OpenAI continuation patch is unavailable")
	}
	delete(s.continuation, snapshotRef)
	return entry, nil
}

func (s *openAIRecoveryPatchStore) takeSchema(schemaRef, patchCode string) (*openAIRecoveryPatchEntry, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return nil, streamgate.ErrIngressSnapshotClosed
	}
	key := openAISchemaPatchKey(schemaRef, patchCode)
	entry := s.schema[key]
	if entry == nil {
		return nil, fmt.Errorf("OpenAI schema patch is unavailable")
	}
	delete(s.schema, key)
	return entry, nil
}

func (s *openAIRecoveryPatchStore) close() {
	if s == nil {
		return
	}
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return
	}
	s.closed = true
	continuation := s.continuation
	schema := s.schema
	s.continuation = make(map[string]*openAIRecoveryPatchEntry)
	s.schema = make(map[string]*openAIRecoveryPatchEntry)
	s.ingress = nil
	s.mu.Unlock()
	for _, entry := range continuation {
		entry.release()
	}
	for _, entry := range schema {
		entry.release()
	}
}

type openAIRebuiltLease struct {
	mu        sync.Mutex
	ingress   *openAIIngressSnapshot
	guard     *streamgate.IngressSnapshotRebuildGuard
	rebuilt   *streamgate.IngressSnapshot
	bodyAlias []byte
	released  bool
	once      sync.Once
}

func (l *openAIRebuiltLease) body() ([]byte, error) {
	if l == nil {
		return nil, fmt.Errorf("rebuilt OpenAI request is unavailable")
	}
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.released || l.bodyAlias == nil {
		return nil, fmt.Errorf("rebuilt OpenAI request is unavailable")
	}
	return l.bodyAlias, nil
}

func (l *openAIRebuiltLease) release() {
	if l == nil {
		return
	}
	l.once.Do(func() {
		l.mu.Lock()
		rebuilt := l.rebuilt
		guard := l.guard
		l.bodyAlias = nil
		l.rebuilt = nil
		l.guard = nil
		l.ingress = nil
		l.released = true
		l.mu.Unlock()
		if rebuilt != nil {
			rebuilt.Close()
		}
		if guard != nil {
			guard.Close()
		}
	})
}

func (l *openAIRebuiltLease) isReleased() bool {
	if l == nil {
		return true
	}
	l.mu.Lock()
	defer l.mu.Unlock()
	return l.released
}

type openAIRebuiltRequestStore struct {
	mu     sync.Mutex
	leases map[string]*openAIRebuiltLease
	closed bool
}

func newOpenAIRebuiltRequestStore() *openAIRebuiltRequestStore {
	return &openAIRebuiltRequestStore{leases: make(map[string]*openAIRebuiltLease)}
}

func (s *openAIRebuiltRequestStore) put(ref string, lease *openAIRebuiltLease) error {
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return streamgate.ErrIngressSnapshotClosed
	}
	if _, exists := s.leases[ref]; exists {
		s.mu.Unlock()
		return fmt.Errorf("duplicate rebuilt OpenAI request reference")
	}
	s.leases[ref] = lease
	s.mu.Unlock()
	return nil
}

func (s *openAIRebuiltRequestStore) take(ref string) (*openAIRebuiltLease, error) {
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return nil, streamgate.ErrIngressSnapshotClosed
	}
	lease := s.leases[ref]
	if lease == nil {
		s.mu.Unlock()
		return nil, fmt.Errorf("rebuilt OpenAI request reference is unavailable")
	}
	delete(s.leases, ref)
	s.mu.Unlock()
	return lease, nil
}

func (s *openAIRebuiltRequestStore) close() {
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return
	}
	s.closed = true
	leases := s.leases
	s.leases = make(map[string]*openAIRebuiltLease)
	s.mu.Unlock()
	for _, lease := range leases {
		lease.release()
	}
}

// openAIRequestRebuilder implements the Core raw-free RequestRebuilder seam.
// Its endpoint and patch sources are fixed for one HTTP request.
// Lifecycle is strictly linear: open → in-flight rebuilds → closed.
// Close blocks until every in-flight rebuild has released its patch/lease
// reservation; nil receivers return ErrIngressSnapshotClosed instead of
// panicking.
type openAIRequestRebuilder struct {
	mu                  sync.Mutex
	cond                *sync.Cond
	inFlight            int
	closeWaiters        int
	closed              bool
	closeDone           bool
	ingress             *openAIIngressSnapshot
	endpoint            string
	recoverySource      *openAIRecoverySourceStore
	contextWindowTokens int
	patches             *openAIRecoveryPatchStore
	rebuilt             *openAIRebuiltRequestStore
}

// newOpenAIRequestRebuilder binds the request-local ingress, the optional model
// output recovery source, and the target model's context window (0 when
// unknown) for one HTTP request. The recovery source powers the S20 resume
// builder; a nil source keeps only the exact/schema/patch continuation paths.
func newOpenAIRequestRebuilder(ingress *openAIIngressSnapshot, endpoint string, recoverySource *openAIRecoverySourceStore, contextWindowTokens int) (*openAIRequestRebuilder, error) {
	switch endpoint {
	case openAIRebuildEndpointChat, openAIRebuildEndpointResponses:
	default:
		return nil, fmt.Errorf("unsupported OpenAI rebuild endpoint")
	}
	if ingress == nil || ingress.isClosed() {
		return nil, streamgate.ErrIngressSnapshotClosed
	}
	r := &openAIRequestRebuilder{
		ingress:             ingress,
		endpoint:            endpoint,
		recoverySource:      recoverySource,
		contextWindowTokens: contextWindowTokens,
		patches:             newOpenAIRecoveryPatchStore(ingress),
		rebuilt:             newOpenAIRebuiltRequestStore(),
	}
	r.cond = sync.NewCond(&r.mu)
	return r, nil
}

func (r *openAIRequestRebuilder) PatchStore() *openAIRecoveryPatchStore    { return r.patches }
func (r *openAIRequestRebuilder) RebuiltStore() *openAIRebuiltRequestStore { return r.rebuilt }

func (r *openAIRequestRebuilder) Close() {
	if r == nil {
		return
	}
	r.mu.Lock()
	if r.closeDone {
		r.mu.Unlock()
		return
	}
	r.closeWaiters++
	r.cond.Broadcast()
	defer r.leaveCloseWaiter()
	r.closed = true
	for r.inFlight > 0 && !r.closeDone {
		r.cond.Wait()
	}
	if r.closeDone {
		r.mu.Unlock()
		return
	}
	r.rebuilt.close()
	r.patches.close()
	r.recoverySource.close()
	r.closeDone = true
	r.cond.Broadcast()
	r.mu.Unlock()
}

func (r *openAIRequestRebuilder) RebuildRequest(ctx context.Context, snapshotRef streamgate.RecoveryRequestSnapshotRef, plan streamgate.RecoveryPlan) (streamgate.RebuiltRequestDraft, error) {
	if r == nil {
		return streamgate.RebuiltRequestDraft{}, streamgate.ErrIngressSnapshotClosed
	}
	if err := ctx.Err(); err != nil {
		return streamgate.RebuiltRequestDraft{}, err
	}
	r.mu.Lock()
	if r.closed {
		r.mu.Unlock()
		return streamgate.RebuiltRequestDraft{}, streamgate.ErrIngressSnapshotClosed
	}
	r.inFlight++
	r.mu.Unlock()
	defer func() {
		r.mu.Lock()
		r.inFlight--
		if r.inFlight == 0 {
			r.cond.Broadcast()
		} else {
			r.cond.Signal()
		}
		r.mu.Unlock()
	}()
	if r.ingress == nil || r.ingress.isClosed() {
		return streamgate.RebuiltRequestDraft{}, streamgate.ErrIngressSnapshotClosed
	}
	if err := snapshotRef.Validate(); err != nil {
		return streamgate.RebuiltRequestDraft{}, err
	}
	if err := plan.Validate(); err != nil {
		return streamgate.RebuiltRequestDraft{}, err
	}
	if !plan.ReadyForRebuild() {
		return streamgate.RebuiltRequestDraft{}, streamgate.ErrRecoveryPlanNotReady
	}
	currentRef, err := r.ingress.recoveryRef()
	if err != nil {
		return streamgate.RebuiltRequestDraft{}, err
	}
	if snapshotRef.SnapshotRef() != currentRef.SnapshotRef() {
		return streamgate.RebuiltRequestDraft{}, fmt.Errorf("OpenAI ingress snapshot reference mismatch")
	}

	directive := plan.Directive()
	// S20 resume builder: a continuation directive that addresses the
	// request-local recovery source is assembled from recorded model output and
	// the fixed English directive, without any caller history or extra model
	// call. Continuation directives that address a pre-computed patch keep the
	// existing patch-store path below.
	if directive.Kind() == streamgate.RecoveryDirectiveKindContinuation &&
		r.recoverySource != nil && directive.SnapshotRef() == r.recoverySource.snapshotRef() {
		return r.rebuildResumeContinuation(ctx, plan, directive)
	}
	var patchEntry *openAIRecoveryPatchEntry
	switch directive.Kind() {
	case streamgate.RecoveryDirectiveKindExact:
		if directive.RequestRef() != snapshotRef.SnapshotRef() {
			return streamgate.RebuiltRequestDraft{}, fmt.Errorf("OpenAI exact request reference mismatch")
		}
	case streamgate.RecoveryDirectiveKindContinuation:
		patchEntry, err = r.patches.takeContinuation(directive.SnapshotRef(), directive.Cursor())
	case streamgate.RecoveryDirectiveKindSchema:
		patchEntry, err = r.patches.takeSchema(directive.SchemaRef(), directive.PatchCode())
	default:
		err = fmt.Errorf("unsupported OpenAI recovery directive")
	}
	if err != nil {
		return streamgate.RebuiltRequestDraft{}, err
	}
	if patchEntry != nil {
		defer patchEntry.release()
	}
	if err := ctx.Err(); err != nil {
		return streamgate.RebuiltRequestDraft{}, err
	}

	requestRef := fmt.Sprintf("openai.rebuilt.%d", openAIRebuiltSequence.Add(1))
	lease := &openAIRebuiltLease{ingress: r.ingress}
	retained, peak, maxBytes := currentRef.RetainedBytes(), currentRef.PeakBytes(), currentRef.MaxBytes()
	rebuiltPromptTokens := 0

	if patchEntry == nil {
		body, err := r.ingress.canonicalBody()
		if err != nil {
			return streamgate.RebuiltRequestDraft{}, err
		}
		lease.bodyAlias = body
		rebuiltPromptTokens = estimateInputTokensBytes(body, nil, nil, nil)
	} else {
		field := "messages"
		if r.endpoint == openAIRebuildEndpointResponses {
			field = "input"
		}
		body, err := r.ingress.canonicalBody()
		if err != nil {
			return streamgate.RebuiltRequestDraft{}, err
		}
		patchPlan, err := planTopLevelJSONPatches(body, []topLevelJSONPatch{{name: field, value: patchEntry.value}})
		if err != nil {
			return streamgate.RebuiltRequestDraft{}, err
		}
		guard, err := r.ingress.reserveRebuild(int64(patchPlan.outputSize))
		if err != nil {
			return streamgate.RebuiltRequestDraft{}, err
		}
		output := patchPlan.apply()
		rebuilt, err := guard.CommitOwnedTyped(openAIRebuiltBodyViewName, output)
		if err != nil {
			guard.Close()
			return streamgate.RebuiltRequestDraft{}, err
		}
		lease.guard = guard
		lease.rebuilt = rebuilt
		lease.bodyAlias = output
		accessor := rebuilt.Accessor()
		retained = uint64(accessor.RetainedBytes())
		maxBytes = uint64(accessor.MaxBytes())
		peak = retained
		if current, refErr := r.ingress.recoveryRef(); refErr == nil && current.PeakBytes() > peak {
			peak = current.PeakBytes()
		}
		rebuiltPromptTokens = estimateInputTokensBytes(output, nil, nil, nil)
	}

	draft, err := streamgate.NewRebuiltRequestDraftWithIdempotency(
		plan.PlanID(), plan.IdempotencyKey(), requestRef, r.endpoint, openAIRebuildFamily,
		retained, peak, maxBytes, rebuiltPromptTokens, plan.RequiredCapabilities(),
	)
	if err != nil {
		lease.release()
		return streamgate.RebuiltRequestDraft{}, err
	}
	if err := r.rebuilt.put(requestRef, lease); err != nil {
		lease.release()
		return streamgate.RebuiltRequestDraft{}, err
	}
	return draft, nil
}

// rebuildResumeContinuation assembles an endpoint-specific continuation body
// from the request-local recovery source snapshot and the fixed English resume
// directive. It consumes the snapshot once (so it only runs after the aborted
// attempt recorded output), refuses to build when the target context window is
// unknown or the resume prompt plus reserve would exceed it, and only then
// reserves, commits, and leases the rebuilt body. A context-window refusal
// returns before any lease is created so no dispatch or recovery budget is
// consumed.
func (r *openAIRequestRebuilder) rebuildResumeContinuation(ctx context.Context, plan streamgate.RecoveryPlan, directive streamgate.RecoveryDirective) (streamgate.RebuiltRequestDraft, error) {
	content, reasoning, err := r.recoverySource.consume(directive.Cursor())
	if err != nil {
		return streamgate.RebuiltRequestDraft{}, err
	}
	if err := ctx.Err(); err != nil {
		return streamgate.RebuiltRequestDraft{}, err
	}
	output, err := r.buildResumeBody(content, reasoning, plan)
	if err != nil {
		return streamgate.RebuiltRequestDraft{}, err
	}
	rebuiltPromptTokens := estimateInputTokensBytes(output, nil, nil, nil)
	if r.contextWindowTokens <= 0 || rebuiltPromptTokens+openAIRebuildContextReserveTokens > r.contextWindowTokens {
		return streamgate.RebuiltRequestDraft{}, errOpenAIRebuildContextOverflow
	}

	requestRef := fmt.Sprintf("openai.rebuilt.%d", openAIRebuiltSequence.Add(1))
	lease := &openAIRebuiltLease{ingress: r.ingress}
	guard, err := r.ingress.reserveRebuild(int64(len(output)))
	if err != nil {
		return streamgate.RebuiltRequestDraft{}, err
	}
	rebuilt, err := guard.CommitOwnedTyped(openAIRebuiltBodyViewName, output)
	if err != nil {
		guard.Close()
		return streamgate.RebuiltRequestDraft{}, err
	}
	lease.guard = guard
	lease.rebuilt = rebuilt
	lease.bodyAlias = output
	accessor := rebuilt.Accessor()
	retained := uint64(accessor.RetainedBytes())
	maxBytes := uint64(accessor.MaxBytes())
	peak := retained
	if current, refErr := r.ingress.recoveryRef(); refErr == nil && current.PeakBytes() > peak {
		peak = current.PeakBytes()
	}

	draft, err := streamgate.NewRebuiltRequestDraftWithIdempotency(
		plan.PlanID(), plan.IdempotencyKey(), requestRef, r.endpoint, openAIRebuildFamily,
		retained, peak, maxBytes, rebuiltPromptTokens, plan.RequiredCapabilities(),
	)
	if err != nil {
		lease.release()
		return streamgate.RebuiltRequestDraft{}, err
	}
	if err := r.rebuilt.put(requestRef, lease); err != nil {
		lease.release()
		return streamgate.RebuiltRequestDraft{}, err
	}
	return draft, nil
}

// buildResumeBody serializes a fresh endpoint-native continuation request that
// carries only the recorded assistant content/reasoning provenance and the
// fixed resume directive. It drops the caller's original messages/input/
// instructions and never summarizes, truncates (beyond the content cursor
// already applied), or rewrites the recorded output.
func (r *openAIRequestRebuilder) buildResumeBody(content, reasoning string, plan streamgate.RecoveryPlan) ([]byte, error) {
	model, stream, ingressTemperature, err := r.ingressResumeHead()
	if err != nil {
		return nil, err
	}
	temperature := continuationTemperature(plan.StrategyAttempt(), ingressTemperature)
	if r.endpoint == openAIRebuildEndpointResponses {
		return buildOpenAIResponsesResumeBody(model, content, reasoning, temperature)
	}
	return buildOpenAIChatResumeBody(model, stream, content, reasoning, temperature)
}

// ingressResumeHead reads only the target model, stream flag, and caller
// temperature from the canonical body. No caller message or input is copied.
func (r *openAIRequestRebuilder) ingressResumeHead() (string, bool, *float64, error) {
	body, err := r.ingress.canonicalBody()
	if err != nil {
		return "", false, nil, err
	}
	var head struct {
		Model       string   `json:"model"`
		Stream      bool     `json:"stream"`
		Temperature *float64 `json:"temperature"`
	}
	if err := json.Unmarshal(body, &head); err != nil {
		return "", false, nil, fmt.Errorf("decode OpenAI resume ingress head: %w", err)
	}
	return head.Model, head.Stream, head.Temperature, nil
}

func continuationTemperature(strategyAttempt int, ingress *float64) *float64 {
	if ingress != nil {
		value := *ingress
		return &value
	}
	candidates := [...]float64{0.2, 0.4, 0.6}
	index := strategyAttempt - 1
	if index < 0 {
		index = 0
	}
	if index >= len(candidates) {
		index = len(candidates) - 1
	}
	value := candidates[index]
	return &value
}

func buildOpenAIChatResumeBody(model string, stream bool, content, reasoning string, temperature *float64) ([]byte, error) {
	assistant := map[string]any{"role": "assistant", "content": content}
	if reasoning != "" {
		assistant["reasoning_content"] = reasoning
	}
	body := map[string]any{
		"model":  model,
		"stream": stream,
		"messages": []any{
			assistant,
			map[string]any{"role": "user", "content": openAIRepeatResumeDirective},
		},
	}
	if temperature != nil {
		body["temperature"] = *temperature
	}
	return json.Marshal(body)
}

func buildOpenAIResponsesResumeBody(model, content, reasoning string, temperature *float64) ([]byte, error) {
	input := make([]any, 0, 2)
	if reasoning != "" {
		input = append(input, map[string]any{
			"type":    "reasoning",
			"content": []any{map[string]any{"type": "reasoning_text", "text": reasoning}},
		})
	}
	input = append(input, map[string]any{
		"type":    "message",
		"role":    "assistant",
		"content": []any{map[string]any{"type": "output_text", "text": content}},
	})
	body := map[string]any{
		"model":        model,
		"stream":       false,
		"instructions": openAIRepeatResumeDirective,
		"input":        input,
	}
	if temperature != nil {
		body["temperature"] = *temperature
	}
	return json.Marshal(body)
}

func (r *openAIRequestRebuilder) leaveCloseWaiter() {
	r.mu.Lock()
	r.closeWaiters--
	r.cond.Broadcast()
	r.mu.Unlock()
}

var _ streamgate.RequestRebuilder = (*openAIRequestRebuilder)(nil)

func isOpenAIRebuildOverflow(err error) bool {
	return errors.Is(err, streamgate.ErrIngressSnapshotRebuildOverflow) ||
		errors.Is(err, streamgate.ErrRecoverySnapshotLimitExceeded)
}
