package ban

import (
	"fmt"
	"strconv"
	"strings"
)

// Weights maps a matcher key to a strike weight for the abuse ban-feed (see ParseWeights, WeightFor).
type Weights map[string]int

// ParseWeights normalizes a list like ["42909:10","403:2","*:1"] into a Weights map. A key is an
// exact ntfy code, a family ("429*"), a bare HTTP status ("403" -> "403*"), or "*"; weights are ints
// >= 0 (0 = exempt). Malformed entries are rejected so misconfiguration fails at startup.
func ParseWeights(entries []string) (Weights, error) {
	out := make(Weights, len(entries))
	for _, entry := range entries {
		key, weightStr, ok := strings.Cut(entry, ":")
		if !ok {
			return nil, fmt.Errorf("invalid ban-weight %q, want KEY:WEIGHT", entry)
		}
		weight, err := strconv.Atoi(strings.TrimSpace(weightStr))
		if err != nil || weight < 0 {
			return nil, fmt.Errorf("invalid ban-weight value in %q, want a non-negative integer", entry)
		}
		key = strings.TrimSpace(key)
		if !validWeightKey(key) {
			return nil, fmt.Errorf("invalid ban-weight key in %q, want %q, an ntfy code, an HTTP status, or a PREFIX*", entry, "*")
		}
		// A bare 3-digit HTTP status is shorthand for the whole family (e.g. "403" -> "403*").
		if len(key) == 3 && isAllDigits(key) {
			key += "*"
		}
		out[key] = weight
	}
	return out, nil
}

// WeightFor returns the strike weight for an ntfy error code, longest-match-wins (exact > family > "*").
// If nothing matches (no "*" catch-all) it returns the implied default 1, so a forgotten "*" still
// bans; use "*:0" to exempt everything not explicitly weighted.
func (w Weights) WeightFor(errorCode int) int {
	code := strconv.Itoa(errorCode)
	weight, bestLen, matched := 0, -1, false
	for key, wt := range w {
		matchLen := -1
		switch {
		case key == "*":
			matchLen = 0
		case strings.HasSuffix(key, "*"):
			if prefix := strings.TrimSuffix(key, "*"); strings.HasPrefix(code, prefix) {
				matchLen = len(prefix)
			}
		case key == code:
			matchLen = len(code)
		}
		if matchLen > bestLen {
			weight, bestLen, matched = wt, matchLen, true
		}
	}
	if !matched {
		return 1
	}
	return weight
}

// validWeightKey reports whether key is a legal matcher: "*", an all-digits code, or DIGITS*.
func validWeightKey(key string) bool {
	if key == "*" {
		return true
	}
	digits := strings.TrimSuffix(key, "*")
	return digits != "" && isAllDigits(digits)
}

func isAllDigits(s string) bool {
	for _, r := range s {
		if r < '0' || r > '9' {
			return false
		}
	}
	return s != ""
}
