package middleware

import (
	"context"
	"errors"
	"sync"
	"time"

	"github.com/rs/zerolog/log"

	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
)

const defaultPermissionStateAsyncWarmLimit = 32
const defaultPermissionStateAsyncWarmTimeout = 5 * time.Second

// PermissionStateWarmer binds the strong aggregate provider and
// version-qualified cache for login-time warming. Warming is bounded,
// best-effort work; request admission remains the correctness authority.
type PermissionStateWarmer struct {
	cache          *PermissionStateCache
	provider       hostruntime.EffectivePermissionSnapshotProvider
	requestContext hostruntime.PermissionRequestContext
	slots          chan struct{}
	timeout        time.Duration
	warming        sync.Map
}

// NewPermissionStateWarmer rejects legacy or incomplete warming composition.
func NewPermissionStateWarmer(
	cache *PermissionStateCache,
	provider hostruntime.EffectivePermissionSnapshotProvider,
	requestContext hostruntime.PermissionRequestContext,
) (*PermissionStateWarmer, error) {
	if !cache.configured() {
		return nil, errors.New(
			"permission state warmer: version-qualified cache is required",
		)
	}
	if provider == nil {
		return nil, errors.New(
			"permission state warmer: aggregate snapshot provider is required",
		)
	}
	return &PermissionStateWarmer{
		cache:          cache,
		provider:       provider,
		requestContext: hostruntime.NormalizePermissionRequestContext(requestContext),
		slots:          make(chan struct{}, defaultPermissionStateAsyncWarmLimit),
		timeout:        defaultPermissionStateAsyncWarmTimeout,
	}, nil
}

// WarmUser loads one current aggregate synchronously through the strong
// provider. Callers that do not need completion should use WarmUserAsync.
func (warmer *PermissionStateWarmer) WarmUser(
	ctx context.Context,
	userID string,
	profileID string,
) error {
	if warmer == nil ||
		warmer.cache == nil ||
		warmer.provider == nil ||
		warmer.requestContext == nil {
		return permissionStateFailure(
			permissionstatev1.FailureAuthorityUnavailable,
		)
	}
	subject, err := permissionstatev1.NewSubject(userID, profileID)
	if err != nil {
		return err
	}
	if ctx == nil {
		return permissionStateFailure(permissionstatev1.FailureInvalidValue)
	}
	return warmer.cache.Warm(
		warmer.requestContext.Install(ctx),
		warmer.provider,
		subject,
	)
}

// WarmUserAsync schedules at most one warm per subject under a process-wide
// bound owned by this production warmer. Saturation drops only the
// optimization; the next request still performs strong admission and resolve.
func (warmer *PermissionStateWarmer) WarmUserAsync(
	sourceCtx context.Context,
	requestID string,
	userID string,
	profileID string,
) {
	if warmer == nil || warmer.cache == nil || warmer.provider == nil {
		return
	}
	subject, err := permissionstatev1.NewSubject(userID, profileID)
	if err != nil {
		log.Warn().Err(err).Msg(
			"PermissionStateWarmer: invalid subject; dropping warm request",
		)
		return
	}
	key := permissionStateSubjectKey(subject)
	if _, inFlight := warmer.warming.LoadOrStore(key, struct{}{}); inFlight {
		return
	}
	select {
	case warmer.slots <- struct{}{}:
	default:
		warmer.warming.Delete(key)
		log.Warn().
			Str("user_id", subject.UserID()).
			Msg("PermissionStateWarmer: warm gate full; dropping warm request")
		return
	}

	go func() {
		defer func() {
			<-warmer.slots
			warmer.warming.Delete(key)
		}()
		defer recoverPermissionStateWarmPanic(
			requestID,
			subject.UserID(),
			subject.ProfileID(),
		)
		ctx, cancel := permissionWarmAsyncContext(
			sourceCtx,
			requestID,
			warmer.timeout,
		)
		defer cancel()
		started := time.Now()
		err := warmer.WarmUser(ctx, subject.UserID(), subject.ProfileID())
		event := log.Info()
		if err != nil {
			event = log.Warn().Err(err)
		}
		event.
			Str("event", "permission_state_warm").
			Str("request_id", requestID).
			Str("user_id", subject.UserID()).
			Str("profile_id", subject.ProfileID()).
			Int64("elapsed_ms", time.Since(started).Milliseconds()).
			Msg("Permission State cache warm completed")
	}()
}

func recoverPermissionStateWarmPanic(
	requestID string,
	userID string,
	profileID string,
) {
	if recovered := recover(); recovered != nil {
		log.Error().
			Interface("panic", recovered).
			Str("event", "permission_state_warm_panic").
			Str("request_id", requestID).
			Str("user_id", userID).
			Str("profile_id", profileID).
			Msg("Recovered from Permission State cache warm panic")
	}
}
