package runtimegrpc

import (
	"context"
	"errors"
	"math/rand/v2"
	"time"

	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

const maxAdmissionRetries = 4

var admissionRetryBackoffCaps = [...]time.Duration{
	50 * time.Millisecond,
	100 * time.Millisecond,
	200 * time.Millisecond,
	250 * time.Millisecond,
}

// admissionRetryScope is created once per top-level client operation. Identity,
// data, and reconciliation calls share its deadline and retry count so one HTTP
// route cannot multiply the configured retry budget across Runtime facets.
type admissionRetryScope struct {
	ctx       context.Context
	deadline  time.Time
	retries   int
	exhausted bool
	onRetry   func()
	onExhaust func()
}

func newAdmissionRetryScope(ctx context.Context, budget time.Duration, onRetry, onExhaust func()) *admissionRetryScope {
	if ctx == nil || budget <= 0 {
		return nil
	}
	deadline := time.Now().Add(budget)
	if parentDeadline, ok := ctx.Deadline(); ok && parentDeadline.Before(deadline) {
		deadline = parentDeadline
	}
	return &admissionRetryScope{ctx: ctx, deadline: deadline, onRetry: onRetry, onExhaust: onExhaust}
}

func retryExplicitBackoff[T any](scope *admissionRetryScope, invoke func() (T, error)) (T, error) {
	for {
		result, err := invoke()
		retryAfter, retryable := explicitAdmissionBackoff(err)
		if err == nil || !retryable {
			return result, err
		}
		if scope != nil {
			if contextErr := scope.ctx.Err(); contextErr != nil {
				var zero T
				return zero, semanticContextFailure(contextErr)
			}
		}
		if scope == nil || scope.retries >= maxAdmissionRetries {
			scope.reportExhausted()
			return result, err
		}
		jitterCap := admissionRetryBackoffCaps[scope.retries]
		jitter := time.Duration(rand.Int64N(int64(jitterCap) + 1))
		if retryAfter > time.Duration(1<<63-1)-jitter {
			scope.reportExhausted()
			return result, err
		}
		delay := retryAfter + jitter
		if time.Now().Add(delay).After(scope.deadline) {
			scope.reportExhausted()
			return result, err
		}
		scope.retries++
		if scope.onRetry != nil {
			scope.onRetry()
		}
		if waitErr := waitAdmissionBackoff(scope.ctx, delay); waitErr != nil {
			var zero T
			return zero, waitErr
		}
	}
}

func explicitAdmissionBackoff(err error) (time.Duration, bool) {
	var failure *commonsemantic.Failure
	if !errors.As(err, &failure) || !commonsemantic.ValidFailureRetryAfter(failure) ||
		failure.Category != commonsemantic.FailureResourceExhausted ||
		failure.LimitKind != commonsemantic.LimitAdmission || failure.RetryAfterMillis == 0 {
		return 0, false
	}
	const maxDurationMillis = uint64((1<<63 - 1) / int64(time.Millisecond))
	if failure.RetryAfterMillis > maxDurationMillis {
		return 0, false
	}
	return time.Duration(failure.RetryAfterMillis) * time.Millisecond, true
}

func (scope *admissionRetryScope) reportExhausted() {
	if scope == nil || scope.exhausted {
		return
	}
	scope.exhausted = true
	if scope.onExhaust != nil {
		scope.onExhaust()
	}
}

func waitAdmissionBackoff(ctx context.Context, delay time.Duration) error {
	if err := ctx.Err(); err != nil {
		return semanticContextFailure(err)
	}
	timer := time.NewTimer(delay)
	defer timer.Stop()
	select {
	case <-timer.C:
		return nil
	case <-ctx.Done():
		return semanticContextFailure(ctx.Err())
	}
}

func semanticContextFailure(err error) error {
	if errors.Is(err, context.DeadlineExceeded) {
		return &commonsemantic.Failure{Category: commonsemantic.FailureDeadlineExceeded}
	}
	return &commonsemantic.Failure{Category: commonsemantic.FailureCanceled}
}
