package openai

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"sync"

	edgeservice "iop/apps/edge/internal/service"
	"iop/packages/go/streamgate"
)

type openAIAdmissionKind string

const (
	openAIAdmissionRun    openAIAdmissionKind = "run"
	openAIAdmissionTunnel openAIAdmissionKind = "tunnel"
	openAIAdmissionPool   openAIAdmissionKind = "pool"
)

// openAIAttemptAdmission is built fresh for every recovery dispatch. The auth
// callback deliberately lives here rather than on ingress/rebuilt snapshots.
type openAIAttemptAdmission struct {
	kind      openAIAdmissionKind
	run       edgeservice.SubmitRunRequest
	tunnel    edgeservice.SubmitProviderTunnelRequest
	pool      edgeservice.ProviderPoolDispatchRequest
	authorize func(context.Context) (map[string]string, error)
}

func (a openAIAttemptAdmission) validate() error {
	switch a.kind {
	case openAIAdmissionRun:
		if a.run.ModelGroupKey == "" && a.run.Target == "" {
			return fmt.Errorf("OpenAI recovery run admission requires a model")
		}
	case openAIAdmissionTunnel:
		if a.tunnel.Path == "" {
			return fmt.Errorf("OpenAI recovery tunnel admission requires a path")
		}
	case openAIAdmissionPool:
		if !a.pool.Run.ProviderPool || a.pool.Run.ModelGroupKey == "" {
			return fmt.Errorf("OpenAI recovery pool admission is incomplete")
		}
	default:
		return fmt.Errorf("unknown OpenAI recovery admission kind")
	}
	return nil
}

type openAIAttemptAdmissionBuilder func(
	ctx context.Context,
	request streamgate.RebuiltRequest,
	body []byte,
) (openAIAttemptAdmission, error)

type openAIAttemptTransport struct {
	path         openAIAdmissionKind
	run          edgeservice.RunResult
	tunnel       edgeservice.ProviderTunnelResult
	usage        *openAIAttemptUsage
	usageBinding usageDispatchBinding
}

func (t *openAIAttemptTransport) bindUsage(dispatch edgeservice.RunDispatch) {
	if t == nil {
		return
	}
	t.usage = &openAIAttemptUsage{}
	t.usageBinding = newUsageDispatchBinding(dispatch, openAIAttemptResponseMode(t.path))
}

func openAIAttemptResponseMode(path openAIAdmissionKind) string {
	if path == openAIAdmissionTunnel {
		return responseModePassthrough
	}
	return responseModeNormalized
}

type openAIAttemptEventSourceFactory func(openAIAttemptTransport) (streamgate.NormalizedEventSource, error)

// openAIRecoveryAdmissionState carries only the sanitized classification of a
// recovery dispatch failure from the AttemptDispatcher to the release sink.
// Core intentionally hides raw host dispatcher errors, while the OpenAI host
// still has to preserve the public admission contract: an all-candidates
// capability rejection is a pre-dispatch 400 on initial, queued, and recovery
// admission alike.
type openAIRecoveryAdmissionState struct {
	mu                       sync.Mutex
	candidateRejected        bool
	toolValidationRetryError string
}

func (s *openAIRecoveryAdmissionState) record(err error) {
	if s == nil || !errors.Is(err, edgeservice.ErrProviderPoolCandidateRejected) {
		return
	}
	s.mu.Lock()
	s.candidateRejected = true
	s.mu.Unlock()
}

func (s *openAIRecoveryAdmissionState) rejected() bool {
	if s == nil {
		return false
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.candidateRejected
}

func (s *openAIRecoveryAdmissionState) recordToolValidationRetry(err error) {
	if s == nil || err == nil {
		return
	}
	s.mu.Lock()
	s.toolValidationRetryError = err.Error()
	s.mu.Unlock()
}

func (s *openAIRecoveryAdmissionState) toolValidationRetryFailure() (string, bool) {
	if s == nil {
		return "", false
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.toolValidationRetryError, s.toolValidationRetryError != ""
}

// openAIAttemptDispatcher adapts the three existing Edge admission surfaces
// to Core AttemptDispatcher. Provider/model/path values are never accepted
// from the rebuilder; they come exclusively from RunDispatch after admission.
type openAIAttemptDispatcher struct {
	service     runService
	store       *openAIRebuiltRequestStore
	build       openAIAttemptAdmissionBuilder
	eventSource openAIAttemptEventSourceFactory
	state       *openAIRecoveryAdmissionState
	usage       *openAIUsageRecorder
	stall       *openAIStallRecoveryState
	sink        openAIStreamGateSink
	holder      *openAIBufferedResultHolder
}

func newOpenAIAttemptDispatcher(
	service runService,
	store *openAIRebuiltRequestStore,
	build openAIAttemptAdmissionBuilder,
	eventSource openAIAttemptEventSourceFactory,
	args ...any,
) (*openAIAttemptDispatcher, error) {
	if service == nil || store == nil || build == nil || eventSource == nil {
		return nil, fmt.Errorf("OpenAI attempt dispatcher dependencies are required")
	}
	dispatcher := &openAIAttemptDispatcher{
		service: service, store: store, build: build, eventSource: eventSource,
		state: &openAIRecoveryAdmissionState{},
	}
	for _, arg := range args {
		switch value := arg.(type) {
		case *openAIUsageRecorder:
			dispatcher.usage = value
		case *openAIStallRecoveryState:
			dispatcher.stall = value
		case openAIStreamGateSink:
			dispatcher.sink = value
		case *openAIBufferedResultHolder:
			dispatcher.holder = value
		}
	}
	return dispatcher, nil
}

func (d *openAIAttemptDispatcher) admissionState() *openAIRecoveryAdmissionState {
	return d.state
}

func (d *openAIAttemptDispatcher) DispatchAttempt(ctx context.Context, request streamgate.RebuiltRequest) (streamgate.AttemptBinding, error) {
	if err := ctx.Err(); err != nil {
		return streamgate.AttemptBinding{}, err
	}
	if err := request.Validate(); err != nil {
		return streamgate.AttemptBinding{}, err
	}
	lease, err := d.store.take(request.RequestRef())
	if err != nil {
		return streamgate.AttemptBinding{}, err
	}
	owned := true
	defer func() {
		if owned {
			lease.release()
		}
	}()

	body, err := lease.body()
	if err != nil {
		return streamgate.AttemptBinding{}, err
	}
	admission, err := d.build(ctx, request, body)
	if err != nil {
		return streamgate.AttemptBinding{}, err
	}
	if err := admission.validate(); err != nil {
		return streamgate.AttemptBinding{}, err
	}
	if admission.kind == openAIAdmissionPool {
		if providerID, allowFallback, ok := d.stall.consumeAdmission(); ok {
			admission.pool.AvoidProviderID = providerID
			admission.pool.AllowAvoidedProviderFallback = allowFallback
		}
	}

	transport, dispatch, closeTransport, err := d.dispatch(ctx, admission)
	if err != nil {
		d.state.record(err)
		if d.holder != nil && d.holder.validationFailure() != nil {
			d.state.recordToolValidationRetry(err)
		}
		return streamgate.AttemptBinding{}, err
	}
	transport.bindUsage(dispatch)
	transportOwned := true
	defer func() {
		if transportOwned {
			closeTransport()
		}
	}()

	controller := &openAIAttemptController{
		service:           d.service,
		dispatch:          dispatch,
		closeTransport:    closeTransport,
		lease:             lease,
		usageRecorder:     d.usage,
		usageBinding:      transport.usageBinding,
		usage:             transport.usage,
		stall:             d.stall,
		compatibilitySink: d.sink,
	}
	abortDispatched := func() {
		owned = false
		transportOwned = false
		_ = controller.AbortAttempt(context.Background())
	}
	source, err := d.eventSource(transport)
	if err != nil {
		if d.holder != nil && d.holder.validationFailure() != nil {
			d.state.recordToolValidationRetry(err)
		}
		abortDispatched()
		return streamgate.AttemptBinding{}, err
	}
	binding, err := streamgate.NewAttemptBinding(
		openAIStreamGateSafeToken("attempt", dispatch.RunID),
		actualOpenAIModel(dispatch),
		openAIAttemptBindingProvider(dispatch),
		actualOpenAIExecutionPath(dispatch, transport.path),
		source,
		controller,
	)
	if err != nil {
		abortDispatched()
		return streamgate.AttemptBinding{}, err
	}
	owned = false
	transportOwned = false
	return binding, nil
}

func (d *openAIAttemptDispatcher) dispatch(
	ctx context.Context,
	admission openAIAttemptAdmission,
) (openAIAttemptTransport, edgeservice.RunDispatch, func(), error) {
	switch admission.kind {
	case openAIAdmissionRun:
		handle, err := d.service.SubmitRun(ctx, admission.run)
		if err != nil {
			return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, err
		}
		return openAIAttemptTransport{path: openAIAdmissionRun, run: handle}, handle.Dispatch(), handle.Close, nil

	case openAIAdmissionTunnel:
		request := admission.tunnel
		if admission.authorize != nil {
			headers, err := admission.authorize(ctx)
			if err != nil {
				return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, err
			}
			request.Headers = headers
		}
		handle, err := d.service.SubmitProviderTunnel(ctx, request)
		if err != nil {
			return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, err
		}
		return openAIAttemptTransport{path: openAIAdmissionTunnel, tunnel: handle}, handle.Dispatch(), handle.Close, nil

	case openAIAdmissionPool:
		request := admission.pool
		if admission.authorize != nil {
			previous := request.PrepareTunnel
			request.PrepareTunnel = func(tunnel edgeservice.SubmitProviderTunnelRequest) (edgeservice.SubmitProviderTunnelRequest, error) {
				headers, err := admission.authorize(ctx)
				if err != nil {
					return tunnel, err
				}
				tunnel.Headers = headers
				if previous != nil {
					return previous(tunnel)
				}
				return tunnel, nil
			}
		}
		result, err := d.service.SubmitProviderPool(ctx, request)
		if err != nil {
			return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, err
		}
		if result == nil {
			return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, fmt.Errorf("OpenAI provider pool returned no result")
		}
		switch result.Path {
		case edgeservice.ProviderPoolPathNormalized:
			if result.Run == nil {
				return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, fmt.Errorf("OpenAI provider pool normalized result is missing")
			}
			return openAIAttemptTransport{path: openAIAdmissionRun, run: result.Run}, result.DispatchInfo, result.Run.Close, nil
		case edgeservice.ProviderPoolPathTunnel:
			if result.Tunnel == nil {
				return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, fmt.Errorf("OpenAI provider pool tunnel result is missing")
			}
			return openAIAttemptTransport{path: openAIAdmissionTunnel, tunnel: result.Tunnel}, result.DispatchInfo, result.Tunnel.Close, nil
		default:
			return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, fmt.Errorf("OpenAI provider pool returned an unknown path")
		}
	default:
		return openAIAttemptTransport{}, edgeservice.RunDispatch{}, nil, fmt.Errorf("unknown OpenAI recovery admission kind")
	}
}

func actualOpenAIModel(dispatch edgeservice.RunDispatch) string {
	if model := strings.TrimSpace(dispatch.Target); model != "" {
		return model
	}
	return strings.TrimSpace(dispatch.ModelGroupKey)
}

func actualOpenAIProvider(dispatch edgeservice.RunDispatch) string {
	return strings.TrimSpace(dispatch.ProviderID)
}

func openAIAttemptBindingProvider(dispatch edgeservice.RunDispatch) string {
	if provider := actualOpenAIProvider(dispatch); provider != "" {
		return provider
	}
	// Core requires a non-empty attempt binding even for legacy direct routes
	// that predate stable provider ids. The liveness handoff never admits this
	// sentinel as a recovery candidate.
	return openAIUnspecifiedProviderID
}

func actualOpenAIExecutionPath(dispatch edgeservice.RunDispatch, path openAIAdmissionKind) string {
	if executionPath := strings.TrimSpace(dispatch.ExecutionPath); executionPath != "" {
		return executionPath
	}
	if path == openAIAdmissionTunnel {
		return string(edgeservice.ProviderPoolPathTunnel)
	}
	return string(edgeservice.ProviderPoolPathNormalized)
}

type openAIAttemptController struct {
	mu                sync.Mutex
	closed            bool
	service           runService
	dispatch          edgeservice.RunDispatch
	closeTransport    func()
	lease             *openAIRebuiltLease
	usageRecorder     *openAIUsageRecorder
	usageBinding      usageDispatchBinding
	usage             *openAIAttemptUsage
	stall             *openAIStallRecoveryState
	compatibilitySink openAIStreamGateSink
}

func (c *openAIAttemptController) recordUsage() {
	if c == nil {
		return
	}
	c.usageRecorder.RecordAttempt(c.usageBinding, c.usage.snapshot())
}

// claimOwnership atomically transitions the controller to closed exactly once
// and hands the caller the transport closer and rebuilt lease so both
// AbortAttempt and CloseAttempt release ownership at most once.
func (c *openAIAttemptController) claimOwnership() (func(), *openAIRebuiltLease, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed {
		return nil, nil, false
	}
	c.closed = true
	closeTransport := c.closeTransport
	lease := c.lease
	c.closeTransport = nil
	c.lease = nil
	return closeTransport, lease, true
}

// AbortAttempt cancels the current provider run and releases its transport and
// rebuilt lease. It is idempotent and is the non-graceful teardown used for
// recovery replace, terminal error, and caller cancel.
func (c *openAIAttemptController) AbortAttempt(ctx context.Context) error {
	if c == nil {
		return nil
	}
	closeTransport, lease, claimed := c.claimOwnership()
	if !claimed {
		return nil
	}
	c.recordUsage()

	// A typed response_stalled terminal with an Edge-confirmed local fence has
	// already closed Node ownership. Preserve that authority by closing only the
	// request-local transport; all other recoveries retain CancelRun behavior.
	confirmedTerminal := c.stall.claimConfirmedClose(openAIStreamGateSafeToken("attempt", c.dispatch.RunID))
	compatibilityTerminal := openAICompatibilityProviderTerminal(c.compatibilitySink)
	var cancelErr error
	if !confirmedTerminal && !compatibilityTerminal && c.dispatch.RunID != "" {
		_, cancelErr = c.service.CancelRun(ctx, edgeservice.CancelRunRequest{
			NodeRef: c.dispatch.NodeID, RunID: c.dispatch.RunID,
		})
	}
	if closeTransport != nil {
		closeTransport()
	}
	lease.release()
	return cancelErr
}

// CloseAttempt releases the current provider transport and rebuilt lease
// without issuing a provider-side cancel. It is the graceful teardown used
// after a normal (successful) terminal, where the provider run already
// completed, and is idempotent with AbortAttempt through the shared claim.
func (c *openAIAttemptController) CloseAttempt(ctx context.Context) error {
	if c == nil {
		return nil
	}
	closeTransport, lease, claimed := c.claimOwnership()
	if !claimed {
		return nil
	}
	c.recordUsage()
	if closeTransport != nil {
		closeTransport()
	}
	lease.release()
	return nil
}

var _ streamgate.AttemptDispatcher = (*openAIAttemptDispatcher)(nil)
var _ streamgate.AttemptController = (*openAIAttemptController)(nil)
