package openai

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"
	"sync"
	"time"

	"go.uber.org/zap"

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

type runService interface {
	SubmitRun(context.Context, edgeservice.SubmitRunRequest) (edgeservice.RunResult, error)
	SubmitProviderTunnel(context.Context, edgeservice.SubmitProviderTunnelRequest) (edgeservice.ProviderTunnelResult, error)
	SubmitProviderPool(context.Context, edgeservice.ProviderPoolDispatchRequest) (*edgeservice.ProviderPoolDispatchResult, error)
	OllamaAPI(context.Context, edgeservice.OllamaAPIRequest) (edgeservice.OllamaAPIView, error)
	CancelRun(context.Context, edgeservice.CancelRunRequest) (edgeservice.CommandResult, error)
}

// singleRequestService is an optional, narrow capability used only by an
// admitted marked Anthropic request. Keeping it separate from runService means
// ordinary OpenAI/Anthropic handlers and their test doubles do not acquire the
// coordinator contract.
type singleRequestService interface {
	StartSingleRequest(context.Context, edgeservice.SingleRequestRequest) (edgeservice.SingleRequestExecution, error)
}

// cancelRunOnHTTPGiveUp sends CancelRun to Node when the HTTP caller gave up
// (request cancellation/timeout) before the run reached a terminal state.
// Terminal run outcomes are not cancel-worthy; see isCancelWorthyRunError.
func (s *Server) cancelRunOnHTTPGiveUp(dispatch edgeservice.RunDispatch, err error) {
	if !isCancelWorthyRunError(err) || dispatch.RunID == "" {
		return
	}
	s.sendCancelRun(dispatch)
}

// sendCancelRun propagates cancellation for a dispatched run/tunnel to the
// Node cancel path.
func (s *Server) sendCancelRun(dispatch edgeservice.RunDispatch) {
	if dispatch.RunID == "" {
		return
	}
	if _, cancelErr := s.service.CancelRun(context.Background(), edgeservice.CancelRunRequest{
		NodeRef: dispatch.NodeID,
		RunID:   dispatch.RunID,
	}); cancelErr != nil {
		s.logger.Warn("openai cancel run failed",
			zap.String("run_id", dispatch.RunID),
			zap.Error(cancelErr),
		)
	}
}

type Server struct {
	mu                            sync.RWMutex
	cfg                           config.EdgeOpenAIConf
	edgeID                        string
	modelCatalog                  []config.ModelCatalogEntry
	longContextThresholdTokens    int
	longContextThresholdTokensSet bool
	service                       runService
	logger                        *zap.Logger
	server                        *http.Server
	obsSink                       streamgate.ObservationSink
	obsSinkIsDefault              bool
	livenessCollectors            *livenessRecoveryCollectors
	principalProjection           authprojection.Reader
	credentialMode                credentialMode
	executionPresets              []config.ExecutionPreset
	requestCoordinator            *logicalRequestCoordinator
	artifactFrontiers             *artifactFrontierStore
	lightFlows                    *hotPathLightStore
	hotPathObserver               hotPathObserver
	hotPathObserverHook           hotPathObserverFailureHook
}

// SetCredentialPlaneManaged selects the request authentication and provider
// credential source from the single Edge credential_plane.enabled switch.
// Runtime assembly calls this before the server starts; it is not a live mode
// switch.
func (s *Server) SetCredentialPlaneManaged(enabled bool) {
	s.mu.Lock()
	if enabled {
		s.credentialMode = credentialModeManaged
	} else {
		s.credentialMode = credentialModeLegacy
	}
	s.mu.Unlock()
}

func (s *Server) managedCredentialPlane() bool {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.credentialMode == credentialModeManaged
}

// CredentialPlaneManaged reports the immutable startup credential mode for
// runtime assembly tests and diagnostics.
func (s *Server) CredentialPlaneManaged() bool {
	return s.managedCredentialPlane()
}

func NewServer(cfg config.EdgeOpenAIConf, svc runService, logger *zap.Logger) *Server {
	if logger == nil {
		logger = zap.NewNop()
	}
	s := &Server{
		cfg:                cfg,
		service:            svc,
		logger:             logger,
		obsSink:            newZapFilterObservationSink(logger),
		obsSinkIsDefault:   true,
		livenessCollectors: defaultLivenessRecoveryCollectors,
		requestCoordinator: newLogicalRequestCoordinator(logicalRequestCoordinatorOptions{}),
		artifactFrontiers:  newArtifactFrontierStore(defaultArtifactFrontierCapacity),
		lightFlows:         newHotPathLightStore(defaultHotPathLightCapacity),
		hotPathObserver:    newZapHotPathObserver(logger),
	}
	if s.hotPathObserver == nil {
		s.hotPathObserver = hotPathNoopObserver{}
	}
	return s
}

// logicalRequests returns the Edge-local coordinator installed for this server.
// Preset-backed Chat and Messages ingress join this coordinator before dispatch.
func (s *Server) logicalRequests() *logicalRequestCoordinator {
	return s.requestCoordinator
}

// SetPrincipalProjection installs the shared, transport-neutral projection
// reader. A nil or unmanaged reader preserves the legacy static auth mode.
func (s *Server) SetPrincipalProjection(projection authprojection.Reader) {
	s.mu.Lock()
	s.principalProjection = projection
	s.mu.Unlock()
}

// PrincipalProjection returns the exact reader installed on this server.
// Manager uses this narrow accessor to verify that all ingress auth reads the
// shared cache instance.
func (s *Server) PrincipalProjection() authprojection.Reader {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.principalProjection
}

// SetModelCatalog provides the provider-pool model catalog to the OpenAI server.
// When set, /v1/models lists catalog IDs and requests matching catalog entries
// are dispatched via the provider pool instead of the legacy model_routes path.
func (s *Server) SetModelCatalog(catalog []config.ModelCatalogEntry) {
	s.mu.Lock()
	s.modelCatalog = cloneModelCatalog(catalog)
	s.mu.Unlock()
}

func (s *Server) modelCatalogSnapshot() []config.ModelCatalogEntry {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return cloneModelCatalog(s.modelCatalog)
}

func cloneModelCatalog(catalog []config.ModelCatalogEntry) []config.ModelCatalogEntry {
	if len(catalog) == 0 {
		return nil
	}
	out := make([]config.ModelCatalogEntry, len(catalog))
	for i, entry := range catalog {
		out[i] = entry
		if entry.Providers != nil {
			out[i].Providers = make(map[string]string, len(entry.Providers))
			for k, v := range entry.Providers {
				out[i].Providers[k] = v
			}
		}
		if entry.TokenCounter != nil {
			counter := *entry.TokenCounter
			out[i].TokenCounter = &counter
		}
	}
	return out
}

// SetExecutionPresets provides the execution preset catalog to the OpenAI server using a deep clone snapshot.
func (s *Server) SetExecutionPresets(presets []config.ExecutionPreset) {
	s.mu.Lock()
	s.executionPresets = config.CloneExecutionPresetCatalog(presets)
	s.mu.Unlock()
}

// ExecutionPresetsSnapshot returns a deep cloned snapshot of the current execution preset catalog.
func (s *Server) ExecutionPresetsSnapshot() []config.ExecutionPreset {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return config.CloneExecutionPresetCatalog(s.executionPresets)
}

// ExecutionPreset returns a deep copy of the execution preset matching id.
func (s *Server) ExecutionPreset(id string) (config.ExecutionPreset, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for _, p := range s.executionPresets {
		if p.ID == id {
			return p.Clone(), true
		}
	}
	return config.ExecutionPreset{}, false
}

func (s *Server) Enabled() bool {
	return s != nil && s.cfg.Enabled
}

// SetEdgeID provides this Edge instance's stable identity, used as the low-
// cardinality edge_id label on OpenAI-compatible usage metrics.
func (s *Server) SetEdgeID(id string) {
	s.mu.Lock()
	s.edgeID = id
	s.mu.Unlock()
}

func (s *Server) edgeIDValue() string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if s.edgeID != "" {
		return s.edgeID
	}
	return "edge-local"
}

// SetHotPathObserver installs a distinct observer for Hot Path lifecycle
// events. It is separate from Server.obsSink (Stream Gate) so the two
// observability contracts never share ownership. A nil observer installs a
// noop observer so observer failures can never alter response behavior.
func (s *Server) SetHotPathObserver(observer hotPathObserver) {
	s.mu.Lock()
	if observer == nil {
		s.hotPathObserver = hotPathNoopObserver{}
	} else {
		s.hotPathObserver = observer
	}
	s.mu.Unlock()
}

// HotPathObserver returns the exact observer installed on this server. It is
// exported for tests and diagnostics only; production code routes through
// emitHotPathObservation, which wraps the observer with failure isolation.
func (s *Server) HotPathObserver() hotPathObserver {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if s.hotPathObserver == nil {
		return hotPathNoopObserver{}
	}
	return s.hotPathObserver
}

// SetHotPathObserverHook installs the optional failure hook for the Hot Path
// observer. It is called whenever the observer returns an error or panics. The
// hook is isolated from request results.
func (s *Server) SetHotPathObserverHook(hook hotPathObserverFailureHook) {
	s.mu.Lock()
	s.hotPathObserverHook = hook
	s.mu.Unlock()
}

func (s *Server) hotPathObservationSnapshot() (hotPathObserver, hotPathObserverFailureHook) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	observer := s.hotPathObserver
	if observer == nil {
		observer = hotPathNoopObserver{}
	}
	return observer, s.hotPathObserverHook
}

// emitHotPathObservation is the single production emission seam for Hot Path
// observations. It snapshots observer state under the server lock, then emits
// through bounded, failure-isolated wrappers.
func (s *Server) emitHotPathObservation(ctx context.Context, projection hotPathLogProjection) {
	if s == nil {
		return
	}
	observer, hook := s.hotPathObservationSnapshot()
	failureHook := func(failed hotPathLogProjection, err error) {
		initHotPathMetrics().recordObserverFailure(s.edgeIDValue())
		invokeHotPathObserverFailureHookSafely(hook, failed, err)
	}
	safe := hotPathSafeObserver{
		inner:     &hotPathBoundedObserver{inner: observer},
		onFailure: failureHook,
	}
	_ = safe.Emit(ctx, projection)
}

// SetObservationSink replaces the default observation sink used to emit
// streamgate_filter_observation entries for this server's request runtimes.
// A nil sink installs a NoopObservationSink so observation failures can never
// alter response behavior. Every call transfers ownership to the application:
// the constructor-owned-default flag is cleared so the request-local liveness
// projection never suppresses forwarding to an explicitly installed sink, even
// when that sink is another *zapFilterObservationSink of the built-in type.
func (s *Server) SetObservationSink(sink streamgate.ObservationSink) {
	s.mu.Lock()
	if sink == nil {
		s.obsSink = streamgate.NoopObservationSink{}
	} else {
		s.obsSink = sink
	}
	s.obsSinkIsDefault = false
	s.mu.Unlock()
}

// observationSink returns a fresh request-local liveness observation projection
// wrapping the configured downstream sink. The wrapper only suppresses the
// private-liveness/ExactReplay rows from the generic writer when the downstream
// is this server's constructor-owned default sink; every explicitly installed
// sink receives the original immutable observations.
func (s *Server) observationSink() streamgate.ObservationSink {
	s.mu.RLock()
	downstream := s.obsSink
	logger := s.logger
	suppressDefault := s.obsSinkIsDefault
	collectors := s.livenessCollectors
	s.mu.RUnlock()
	if downstream == nil {
		downstream = streamgate.NoopObservationSink{}
	}
	return newOpenAILivenessObservationSink(downstream, logger, suppressDefault, collectors)
}

// SetLongContextThreshold sets the input-token threshold at or above which a
// request is classified as long-context for admission policy.
func (s *Server) SetLongContextThreshold(val int) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.longContextThresholdTokens = val
	s.longContextThresholdTokensSet = true
}

func (s *Server) longContextThreshold() int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if s.longContextThresholdTokensSet {
		return s.longContextThresholdTokens
	}
	return defaultLongContextThreshold
}

const defaultLongContextThreshold = 100000

func (s *Server) Start(ctx context.Context) error {
	if !s.Enabled() {
		return nil
	}
	if s.cfg.Listen == "" {
		s.cfg.Listen = "0.0.0.0:18081"
	}

	mux := s.routes()

	s.server = &http.Server{
		Addr:              s.cfg.Listen,
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
	}
	ln, err := net.Listen("tcp", s.cfg.Listen)
	if err != nil {
		return fmt.Errorf("openai server listen %s: %w", s.cfg.Listen, err)
	}
	if s.cfg.TLS.Enabled {
		tlsConfig, loadErr := auth.LoadHTTPServerTLS(s.cfg.TLS.Cert, s.cfg.TLS.Key)
		if loadErr != nil {
			_ = ln.Close()
			return fmt.Errorf("openai server TLS: %w", loadErr)
		}
		ln = tls.NewListener(ln, tlsConfig)
	}
	go func() {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = s.server.Shutdown(shutdownCtx)
	}()
	go func() {
		s.logger.Info("openai-compatible server listening", zap.String("addr", s.cfg.Listen))
		if err := s.server.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
			s.logger.Warn("openai-compatible server exited", zap.Error(err))
		}
	}()
	return nil
}

func (s *Server) Stop(ctx context.Context) error {
	if s == nil || s.server == nil {
		return nil
	}
	return s.server.Shutdown(ctx)
}

func responseModel(requestModel, target string) string {
	if requestModel != "" {
		return requestModel
	}
	return target
}

func httpStatusForRunError(err error) int {
	if errors.Is(err, context.Canceled) {
		return http.StatusRequestTimeout
	}
	if status, ok := providerHTTPStatus(err); ok && status == http.StatusBadRequest {
		return http.StatusBadRequest
	}
	return http.StatusBadGateway
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, code, message string) {
	writeJSON(w, status, errorResponse{Error: errorBody{Type: code, Message: message}})
}
