package gateway

import (
	"sort"
	"strconv"
	"strings"
)

type payoutFailureAction int

const (
	payoutFailureActionFail payoutFailureAction = iota + 1
	payoutFailureActionRetry
)

type payoutRetryStrategy int

const (
	payoutRetryStrategyImmediate payoutRetryStrategy = iota + 1
	payoutRetryStrategyDelayed
	payoutRetryStrategyStatusRefresh
)

type payoutFailureDecision struct {
	Action   payoutFailureAction
	Strategy payoutRetryStrategy
	Reason   string
}

type payoutFailurePolicy struct {
	providerCodeStrategies  map[string]payoutRetryStrategy
	documentedProviderCodes map[string]struct{}
}

type retryCodeBucket struct {
	strategy  payoutRetryStrategy
	retryable bool
	codes     []string
}

var providerRetryOnlyCodeBuckets = []retryCodeBucket{
	// GTX "repeat request now / temporary issue" style codes.
	{
		strategy:  payoutRetryStrategyImmediate,
		retryable: true,
		codes: []string{
			// General codes.
			"104", "108", "301", "320", "601", "602", "603", "3025", "3198",
			// External card PS codes.
			"10000", "10100", "10104", "10105", "10107", "10202", "102051", "10301", "105012", "10505", "10601", "10602", "10603",
			// External alternate PS codes.
			"20000", "20100", "20104", "20105", "20202", "20301", "20304", "20601", "20602", "20603",
		},
	},
	// GTX "retry later / limits / period restrictions" style codes.
	{
		strategy:  payoutRetryStrategyDelayed,
		retryable: true,
		codes: []string{
			// General codes.
			"312", "314", "315", "316", "325", "2466",
			"3106", "3108", "3109", "3110", "3111", "3112",
			"3285", "3297", "3298",
			"3305", "3306", "3307", "3308", "3309", "3310", "3311", "3312", "3313", "3314", "3315", "3316", "3317", "3318", "3319", "3320", "3321", "3322", "3323", "3324", "3325", "3326", "3327", "3328", "3329", "3330", "3331", "3332", "3333", "3334", "3335", "3336", "3337", "3338", "3339", "3340",
			"3342", "3343", "3344", "3345", "3346", "3347", "3348", "3349", "3350", "3351", "3352", "3353", "3355", "3357",
			"3407", "3408", "3450", "3451", "3452", "3613",
			// External card PS codes.
			"10101", "10109", "10112", "10114", "101012", "101013", "101014",
			// External alternate PS codes.
			"20109", "20206", "20505", "201012", "201013", "201014",
		},
	},
	// GTX status refresh/polling conditions.
	{
		strategy:  payoutRetryStrategyStatusRefresh,
		retryable: true,
		codes: []string{
			"3061", "3062",
			"9999", "19999", "20802", "29999",
		},
	},
}

var providerDocumentedNonRetryCodes = buildDocumentedNonRetryCodes(providerDocumentedCodes, providerRetryOnlyCodeBuckets)

var providerRetryCodeBuckets = func() []retryCodeBucket {
	buckets := make([]retryCodeBucket, 0, len(providerRetryOnlyCodeBuckets)+1)
	buckets = append(buckets, providerRetryOnlyCodeBuckets...)
	buckets = append(buckets, retryCodeBucket{
		strategy:  payoutRetryStrategyImmediate,
		retryable: false,
		codes:     providerDocumentedNonRetryCodes,
	})
	return buckets
}()

func defaultPayoutFailurePolicy() payoutFailurePolicy {
	strategies := map[string]payoutRetryStrategy{}
	for _, bucket := range providerRetryCodeBuckets {
		if !bucket.retryable {
			continue
		}
		registerRetryStrategy(strategies, bucket.strategy, bucket.codes...)
	}

	return payoutFailurePolicy{
		providerCodeStrategies:  strategies,
		documentedProviderCodes: newCodeSet(providerDocumentedCodes),
	}
}

func (p payoutFailurePolicy) decideProviderFailure(code string) payoutFailureDecision {
	normalized := normalizeProviderCode(code)
	if normalized == "" {
		return payoutFailureDecision{
			Action:   payoutFailureActionFail,
			Strategy: payoutRetryStrategyImmediate,
			Reason:   "provider_failure",
		}
	}
	if strategy, ok := p.providerCodeStrategies[normalized]; ok {
		return payoutFailureDecision{
			Action:   payoutFailureActionRetry,
			Strategy: strategy,
			Reason:   "provider_code_" + normalized,
		}
	}
	if _, ok := p.documentedProviderCodes[normalized]; ok {
		return payoutFailureDecision{
			Action:   payoutFailureActionFail,
			Strategy: payoutRetryStrategyImmediate,
			Reason:   "provider_code_" + normalized + "_documented_non_retry",
		}
	}
	return payoutFailureDecision{
		Action:   payoutFailureActionFail,
		Strategy: payoutRetryStrategyImmediate,
		Reason:   "provider_code_" + normalized + "_unknown",
	}
}

func (p payoutFailurePolicy) decideTransportFailure() payoutFailureDecision {
	return payoutFailureDecision{
		Action:   payoutFailureActionRetry,
		Strategy: payoutRetryStrategyImmediate,
		Reason:   "transport_failure",
	}
}

func payoutFailureReason(code, message string) string {
	cleanCode := strings.TrimSpace(code)
	cleanMessage := strings.TrimSpace(message)
	switch {
	case cleanCode != "" && cleanMessage != "":
		return cleanCode + ": " + cleanMessage
	case cleanCode != "":
		return cleanCode
	default:
		return cleanMessage
	}
}

func retryDelayForAttempt(attempt uint32, strategy payoutRetryStrategy) int {
	strategy = normalizeRetryStrategy(strategy)

	// Backoff in seconds by strategy and attempt number (attempt starts at 1).
	if strategy == payoutRetryStrategyStatusRefresh {
		switch {
		case attempt <= 1:
			return 10
		case attempt == 2:
			return 20
		case attempt == 3:
			return 40
		case attempt == 4:
			return 80
		default:
			return 160
		}
	}

	if strategy == payoutRetryStrategyDelayed {
		switch {
		case attempt <= 1:
			return 30
		case attempt == 2:
			return 120
		case attempt == 3:
			return 600
		case attempt == 4:
			return 1800
		default:
			return 7200
		}
	}

	switch {
	case attempt <= 1:
		return 5
	case attempt == 2:
		return 15
	case attempt == 3:
		return 30
	default:
		return 60
	}
}

func registerRetryStrategy(dst map[string]payoutRetryStrategy, strategy payoutRetryStrategy, codes ...string) {
	if dst == nil || len(codes) == 0 {
		return
	}
	strategy = normalizeRetryStrategy(strategy)
	for _, code := range codes {
		normalized := normalizeProviderCode(code)
		if normalized == "" {
			continue
		}
		dst[normalized] = strategy
	}
}

func newCodeSet(codes []string) map[string]struct{} {
	set := map[string]struct{}{}
	for _, code := range codes {
		normalized := normalizeProviderCode(code)
		if normalized == "" {
			continue
		}
		set[normalized] = struct{}{}
	}
	return set
}

func buildDocumentedNonRetryCodes(documented []string, retryBuckets []retryCodeBucket) []string {
	documentedSet := newCodeSet(documented)
	retrySet := map[string]struct{}{}
	for _, bucket := range retryBuckets {
		for _, code := range bucket.codes {
			normalized := normalizeProviderCode(code)
			if normalized == "" {
				continue
			}
			retrySet[normalized] = struct{}{}
		}
	}

	nonRetry := make([]string, 0, len(documentedSet))
	for code := range documentedSet {
		if _, ok := retrySet[code]; ok {
			continue
		}
		nonRetry = append(nonRetry, code)
	}

	sort.Slice(nonRetry, func(i, j int) bool {
		left, leftErr := strconv.Atoi(nonRetry[i])
		right, rightErr := strconv.Atoi(nonRetry[j])
		if leftErr != nil || rightErr != nil {
			return nonRetry[i] < nonRetry[j]
		}
		return left < right
	})

	return nonRetry
}

func normalizeProviderCode(code string) string {
	return strings.TrimSpace(code)
}

func normalizeRetryStrategy(strategy payoutRetryStrategy) payoutRetryStrategy {
	switch strategy {
	case payoutRetryStrategyDelayed, payoutRetryStrategyStatusRefresh:
		return strategy
	default:
		return payoutRetryStrategyImmediate
	}
}

func (s payoutRetryStrategy) String() string {
	switch normalizeRetryStrategy(s) {
	case payoutRetryStrategyDelayed:
		return "delayed"
	case payoutRetryStrategyStatusRefresh:
		return "status_refresh"
	default:
		return "immediate"
	}
}
