// Special thanks to @codemicro for moving this to fiber core
// Original middleware: github.com/codemicro/fiber-cache
package cache

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"math"
	"net/url"
	"slices"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/gofiber/utils/v2"
	utilsstrings "github.com/gofiber/utils/v2/strings"
	"github.com/valyala/fasthttp"

	"github.com/gofiber/fiber/v3"
)

// timestampUpdatePeriod is the period which is used to check the cache expiration.
// It should not be too long to provide more or less acceptable expiration error, and in the same
// time it should not be too short to avoid overwhelming of the system
const timestampUpdatePeriod = 300 * time.Millisecond

// buffer size for hexpool
const (
	hexLen                       = sha256.Size * 2
	maxKeyDimensionSegmentLength = 192
	defaultKeyBufferCap          = 256
	maxQueryParams               = 128  // Maximum number of query parameters to parse
	maxVaryHeaders               = 32   // Maximum number of Vary headers to process
	maxQueryBufferSize           = 4096 // Maximum buffer size for query string canonicalization
)

// cache status
// unreachable: when cache is bypass, or invalid
// hit: cache is served
// miss: do not have cache record
const (
	cacheUnreachable = "unreachable"
	cacheHit         = "hit"
	cacheMiss        = "miss"
)

type expirationSource uint8

const (
	expirationSourceConfig expirationSource = iota
	expirationSourceMaxAge
	expirationSourceSMaxAge
	expirationSourceExpires
	expirationSourceGenerator
)

// directives
const (
	noCache          = "no-cache"
	noStore          = "no-store"
	privateDirective = "private"
)

type requestCacheDirectives struct {
	maxAge   uint64
	maxStale uint64
	minFresh uint64

	maxAgeSet    bool
	maxStaleSet  bool
	maxStaleAny  bool
	minFreshSet  bool
	noStore      bool
	noCache      bool
	onlyIfCached bool
}

var ignoreHeaders = map[string]struct{}{
	"Age":                 {},
	"Cache-Control":       {}, // already stored explicitly by the cache manager
	"Connection":          {},
	"Content-Encoding":    {}, // already stored explicitly by the cache manager
	"Content-Type":        {}, // already stored explicitly by the cache manager
	"Date":                {},
	"ETag":                {}, // already stored explicitly by the cache manager
	"Expires":             {}, // already stored explicitly by the cache manager
	"Keep-Alive":          {},
	"Proxy-Authenticate":  {},
	"Proxy-Authorization": {},
	"TE":                  {},
	"Trailers":            {},
	"Transfer-Encoding":   {},
	"Upgrade":             {},
}

var cacheableStatusCodes = map[int]struct{}{
	fiber.StatusOK:                          {},
	fiber.StatusNonAuthoritativeInformation: {},
	fiber.StatusNoContent:                   {},
	fiber.StatusPartialContent:              {},
	fiber.StatusMultipleChoices:             {},
	fiber.StatusMovedPermanently:            {},
	fiber.StatusPermanentRedirect:           {},
	fiber.StatusNotFound:                    {},
	fiber.StatusMethodNotAllowed:            {},
	fiber.StatusGone:                        {},
	fiber.StatusRequestURITooLong:           {},
	fiber.StatusNotImplemented:              {},
}

var keyBufferPool = sync.Pool{
	New: func() any {
		buf := make([]byte, 0, defaultKeyBufferCap)
		return &buf
	},
}

// New creates a new middleware handler
func New(config ...Config) fiber.Handler {
	// Set default config
	cfg := configDefault(config...)

	type evictionCandidate struct {
		key     string
		size    uint
		exp     uint64
		heapIdx int
	}

	redactKeys := !cfg.DisableValueRedaction

	maskKey := func(key string) string {
		if redactKeys {
			return redactedKey
		}
		return key
	}

	// Nothing to cache
	if int(cfg.Expiration.Seconds()) < 0 {
		return func(c fiber.Ctx) error {
			return c.Next()
		}
	}

	var (
		// Cache settings
		mux       = &sync.RWMutex{}
		timestamp = safeUnixSeconds(time.Now())
	)
	// Create manager to simplify storage operations ( see manager.go )
	manager := newManager(cfg.Storage, redactKeys)
	// Create indexed heap for tracking expirations ( see heap.go )
	heap := &indexedHeap{}
	// count stored bytes (sizes of response bodies)
	var storedBytes uint
	// Pool for hex encoding buffers
	hexBufPool := &sync.Pool{
		New: func() any {
			buf := make([]byte, hexLen)
			return &buf
		},
	}
	hashAuthorization := makeHashAuthFunc(hexBufPool)
	buildVaryKey := makeBuildVaryKeyFunc(hexBufPool)

	// Update timestamp in the configured interval
	go func() {
		ticker := time.NewTicker(timestampUpdatePeriod)
		defer ticker.Stop()
		for range ticker.C {
			atomic.StoreUint64(&timestamp, safeUnixSeconds(time.Now()))
		}
	}()

	// Delete key from both manager and storage
	deleteKey := func(ctx context.Context, dkey string) error {
		if err := manager.del(ctx, dkey); err != nil {
			return err
		}
		// External storage saves body data with different key
		if cfg.Storage != nil {
			if err := manager.del(ctx, dkey+"_body"); err != nil {
				return err
			}
		}
		return nil
	}

	removeHeapEntry := func(entryKey string, heapIdx int) {
		if cfg.MaxBytes == 0 {
			return
		}

		if heapIdx < 0 || heapIdx >= len(heap.indices) {
			return
		}

		indexedIdx := heap.indices[heapIdx]
		if indexedIdx < 0 || indexedIdx >= len(heap.entries) {
			return
		}

		entry := heap.entries[indexedIdx]
		if entry.idx != heapIdx || entry.key != entryKey {
			return
		}

		_, size := heap.remove(heapIdx)
		storedBytes -= size
	}

	refreshHeapIndex := func(ctx context.Context, candidate evictionCandidate) error {
		entry, err := manager.get(ctx, candidate.key)
		if err != nil {
			if errors.Is(err, errCacheMiss) {
				return nil
			}
			return fmt.Errorf("cache: failed to reload key %q after eviction failure: %w", maskKey(candidate.key), err)
		}

		entry.heapidx = candidate.heapIdx

		remainingTTL := max(time.Until(secondsToTime(entry.exp)), 0)

		if err := manager.set(ctx, candidate.key, entry, remainingTTL); err != nil {
			return fmt.Errorf("cache: failed to restore heap index for key %q: %w", maskKey(candidate.key), err)
		}

		return nil
	}

	// Return new handler
	return func(c fiber.Ctx) error {
		hasAuthorization := len(c.Request().Header.Peek(fiber.HeaderAuthorization)) > 0
		reqCacheControl := c.Request().Header.Peek(fiber.HeaderCacheControl)
		reqDirectives := parseRequestCacheControl(reqCacheControl)
		if !reqDirectives.noCache {
			reqPragma := utils.UnsafeString(c.Request().Header.Peek(fiber.HeaderPragma))
			if hasDirective(reqPragma, noCache) {
				reqDirectives.noCache = true
			}
		}

		// Refrain from caching
		if reqDirectives.noStore {
			return c.Next()
		}

		requestMethod := c.Method()

		// Only cache methods listed in cfg.Methods (default: GET, HEAD).
		if !slices.Contains(cfg.Methods, requestMethod) {
			c.Set(cfg.CacheHeader, cacheUnreachable)
			return c.Next()
		}

		// Get key from request
		baseKey := requestMethod + "|" + cfg.KeyGenerator(c)
		manifestKey := baseKey + "|vary"
		if hasAuthorization {
			authHash := hashAuthorization(c.Request().Header.Peek(fiber.HeaderAuthorization))
			baseKey += "_auth_" + authHash
			manifestKey = baseKey + "|vary"
		}
		key := baseKey

		reqCtx := c.Context()

		varyNames := []string(nil)
		hasVaryManifest := false
		var err error
		if !cfg.DisableVaryHeaders {
			varyNames, hasVaryManifest, err = loadVaryManifest(reqCtx, manager, manifestKey)
			if err != nil {
				return err
			}
			if len(varyNames) > 0 {
				key += buildVaryKey(varyNames, &c.Request().Header)
			}
		}

		// Get entry from pool
		e, err := manager.get(reqCtx, key)
		if err != nil && !errors.Is(err, errCacheMiss) {
			return err
		}
		entryAge := uint64(0)
		revalidate := false
		oldHeapIdx := -1 // Track old heap index for replacement during revalidation

		handleMinFresh := func(now uint64) {
			if e == nil || !reqDirectives.minFreshSet {
				return
			}
			remainingFreshness := remainingFreshness(e, now)
			if remainingFreshness < reqDirectives.minFresh {
				revalidate = true
				oldHeapIdx = e.heapidx
				if cfg.Storage != nil {
					manager.release(e)
				}
				e = nil
			}
		}

		// Lock entry
		mux.Lock()
		locked := true
		unlock := func() {
			if locked {
				mux.Unlock()
				locked = false
			}
		}
		relock := func() {
			if !locked {
				mux.Lock()
				locked = true
			}
		}
		// Get timestamp
		ts := atomic.LoadUint64(&timestamp)

		// Cache Entry found
		if e != nil {
			entryAge = cachedResponseAge(e, ts)
			if reqDirectives.maxAgeSet && (reqDirectives.maxAge == 0 || entryAge > reqDirectives.maxAge) {
				revalidate = true
				oldHeapIdx = e.heapidx
				if cfg.Storage != nil {
					manager.release(e)
				}
				e = nil
			}

			handleMinFresh(ts)
		}

		if e != nil && e.ttl == 0 && e.forceRevalidate {
			revalidate = true
			oldHeapIdx = e.heapidx
			if cfg.Storage != nil {
				manager.release(e)
			}
			e = nil
		}

		if e != nil && e.ttl == 0 && e.exp != 0 && ts >= e.exp {
			unlock()
			if err := deleteKey(reqCtx, key); err != nil {
				if cfg.Storage != nil {
					manager.release(e)
				}
				return fmt.Errorf("cache: failed to delete expired key %q: %w", maskKey(key), err)
			}
			relock()
			removeHeapEntry(key, e.heapidx)
			if cfg.Storage != nil {
				manager.release(e)
			}
			e = nil
			unlock()
			c.Set(cfg.CacheHeader, cacheUnreachable)
			goto continueRequest
		}

		if e != nil {
			entryHasPrivate := e != nil && e.private
			if !entryHasPrivate && cfg.StoreResponseHeaders && len(e.headers) > 0 {
				if cc, ok := lookupCachedHeader(e.headers, fiber.HeaderCacheControl); ok && hasDirective(utils.UnsafeString(cc), privateDirective) {
					entryHasPrivate = true
				}
			}
			requestNoCache := reqDirectives.noCache

			// Invalidate cache if requested
			if cfg.CacheInvalidator != nil && cfg.CacheInvalidator(c) {
				e.exp = ts - 1
			}

			entryHasExpiration := e != nil && e.exp != 0
			entryExpired := entryHasExpiration && ts >= e.exp
			staleness := uint64(0)
			if entryExpired {
				staleness = ts - e.exp
			}
			allowStale := entryExpired && (reqDirectives.maxStaleAny || (reqDirectives.maxStaleSet && staleness <= reqDirectives.maxStale))

			if entryExpired && e.revalidate {
				revalidate = true
				oldHeapIdx = e.heapidx
				if cfg.Storage != nil {
					manager.release(e)
				}
				e = nil
			}

			handleMinFresh(ts)

			if revalidate {
				unlock()
				c.Set(cfg.CacheHeader, cacheUnreachable)
				if reqDirectives.onlyIfCached {
					return c.SendStatus(fiber.StatusGatewayTimeout)
				}
				goto continueRequest
			}

			servedStale := false

			switch {
			case entryExpired && !allowStale:
				unlock()
				if err := deleteKey(reqCtx, key); err != nil {
					if e != nil {
						manager.release(e)
					}
					return fmt.Errorf("cache: failed to delete expired key %q: %w", maskKey(key), err)
				}
				relock()
				idx := e.heapidx
				manager.release(e)
				removeHeapEntry(key, idx)
				e = nil
			case entryHasPrivate:
				unlock()
				if err := deleteKey(reqCtx, key); err != nil {
					if e != nil {
						manager.release(e)
					}
					return fmt.Errorf("cache: failed to delete private response for key %q: %w", maskKey(key), err)
				}
				relock()
				removeHeapEntry(key, e.heapidx)
				if cfg.Storage != nil && e != nil {
					manager.release(e)
				}
				e = nil
				unlock()
				c.Set(cfg.CacheHeader, cacheUnreachable)
				if reqDirectives.onlyIfCached {
					return c.SendStatus(fiber.StatusGatewayTimeout)
				}
				return c.Next()
			case entryHasExpiration && !requestNoCache:
				servedStale = entryExpired
				if hasAuthorization && !e.shareable {
					if cfg.Storage != nil {
						manager.release(e)
					}
					unlock()
					c.Set(cfg.CacheHeader, cacheUnreachable)
					return c.Next()
				}

				// Separate body value to avoid msgp serialization
				// We can store raw bytes with Storage 👍
				if cfg.Storage != nil {
					unlock()
					rawBody, err := manager.getRaw(reqCtx, key+"_body")
					if err != nil {
						manager.release(e)
						return cacheBodyFetchError(maskKey, key, err)
					}
					e.body = rawBody
				} else {
					unlock()
				}
				// Set response headers from cache
				c.Response().SetBodyRaw(e.body)
				c.Response().SetStatusCode(e.status)
				c.Response().Header.SetContentTypeBytes(e.ctype)
				if len(e.cencoding) > 0 {
					c.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding)
				}
				if len(e.cacheControl) > 0 {
					c.Response().Header.SetBytesV(fiber.HeaderCacheControl, e.cacheControl)
				}
				if len(e.expires) > 0 {
					c.Response().Header.SetBytesV(fiber.HeaderExpires, e.expires)
				}
				if len(e.etag) > 0 {
					c.Response().Header.SetBytesV(fiber.HeaderETag, e.etag)
				}
				clampedDate := clampDateSeconds(e.date, ts)
				dateValue := fasthttp.AppendHTTPDate(nil, secondsToTime(clampedDate))
				c.Response().Header.SetBytesV(fiber.HeaderDate, dateValue)
				for i := range e.headers {
					h := e.headers[i]
					c.Response().Header.SetBytesKV(h.key, h.value)
				}
				// Set Cache-Control header if not disabled and not already set
				if !cfg.DisableCacheControl && len(c.Response().Header.Peek(fiber.HeaderCacheControl)) == 0 {
					remaining := uint64(0)
					if e.exp > ts {
						remaining = e.exp - ts
					}
					maxAge := utils.FormatUint(remaining)
					c.Set(fiber.HeaderCacheControl, "public, max-age="+maxAge)
				}

				const maxDeltaSeconds = uint64(math.MaxInt32)
				ageSeconds := min(entryAge, maxDeltaSeconds)

				// RFC-compliant Age header (RFC 9111)
				age := utils.FormatUint(ageSeconds)
				c.Response().Header.Set(fiber.HeaderAge, age)
				appendWarningHeaders(&c.Response().Header, servedStale, isHeuristicFreshness(e, &cfg, entryAge))

				c.Set(cfg.CacheHeader, cacheHit)

				// release item allocated from storage
				if cfg.Storage != nil {
					manager.release(e)
				}

				// Return response
				return nil
			default:
				// no cached response to serve
			}
		}

		if e == nil && revalidate {
			unlock()
			c.Set(cfg.CacheHeader, cacheUnreachable)
			if reqDirectives.onlyIfCached {
				return c.SendStatus(fiber.StatusGatewayTimeout)
			}
			goto continueRequest
		}

		if e == nil && reqDirectives.onlyIfCached {
			unlock()
			c.Set(cfg.CacheHeader, cacheUnreachable)
			return c.SendStatus(fiber.StatusGatewayTimeout)
		}

		// make sure we're not blocking concurrent requests - do unlock
		unlock()

	continueRequest:
		// Continue stack, return err to Fiber if exist
		if err := c.Next(); err != nil {
			return err
		}

		cacheControlBytes := c.Response().Header.Peek(fiber.HeaderCacheControl)
		respCacheControl := parseResponseCacheControl(cacheControlBytes)
		varyHeader := utils.UnsafeString(c.Response().Header.Peek(fiber.HeaderVary))
		hasPrivate := respCacheControl.hasPrivate
		hasNoCache := respCacheControl.hasNoCache
		varyNames, varyHasStar := parseVary(varyHeader)

		// Respect server cache-control: no-store
		if respCacheControl.hasNoStore {
			c.Set(cfg.CacheHeader, cacheUnreachable)
			return nil
		}

		// RFC 9111 requires responses with Vary: * to remain uncacheable even when
		// response-driven Vary partitioning is otherwise disabled.
		if hasPrivate || hasNoCache || varyHasStar {
			if e != nil {
				if err := deleteKey(reqCtx, key); err != nil {
					if cfg.Storage != nil {
						manager.release(e)
					}
					return fmt.Errorf("cache: failed to delete cached response for key %q: %w", maskKey(key), err)
				}
				mux.Lock()
				removeHeapEntry(key, e.heapidx)
				if cfg.Storage != nil {
					manager.release(e)
				}
				e = nil
				mux.Unlock()
			}

			if !cfg.DisableVaryHeaders && hasVaryManifest {
				if err := manager.del(reqCtx, manifestKey); err != nil {
					return fmt.Errorf("cache: failed to delete stale vary manifest %q: %w", maskKey(manifestKey), err)
				}
			}

			c.Set(cfg.CacheHeader, cacheUnreachable)
			return nil
		}

		shouldStoreVaryManifest := !cfg.DisableVaryHeaders && len(varyNames) > 0
		if !cfg.DisableVaryHeaders && len(varyNames) > 0 {
			if key == baseKey {
				key += buildVaryKey(varyNames, &c.Request().Header)
			}
		} else if !cfg.DisableVaryHeaders && hasVaryManifest {
			if err := manager.del(reqCtx, manifestKey); err != nil {
				return fmt.Errorf("cache: failed to delete stale vary manifest %q: %w", maskKey(manifestKey), err)
			}
		}

		isSharedCacheAllowed := allowsSharedCacheDirectives(respCacheControl)
		if hasAuthorization && !isSharedCacheAllowed {
			c.Set(cfg.CacheHeader, cacheUnreachable)
			return nil
		}

		sharedCacheMode := !hasAuthorization || isSharedCacheAllowed

		// Don't cache response if status code is not cacheable
		if _, ok := cacheableStatusCodes[c.Response().StatusCode()]; !ok {
			c.Set(cfg.CacheHeader, cacheUnreachable)
			return nil
		}

		// Don't cache response if Next returns true
		if cfg.Next != nil && cfg.Next(c) {
			c.Set(cfg.CacheHeader, cacheUnreachable)
			return nil
		}

		// Don't try to cache if body won't fit into cache
		bodySize := uint(len(c.Response().Body()))
		if cfg.MaxBytes > 0 && bodySize > cfg.MaxBytes {
			c.Set(cfg.CacheHeader, cacheUnreachable)
			return nil
		}

		// Eviction loop: atomically reserve space for new entry and evict old entries.
		// Strategy:
		// 1. Under lock: reserve space by pre-incrementing storedBytes, then collect entries to evict
		// 2. Outside lock: perform I/O deletions
		// 3. On deletion failure: restore storedBytes and return error
		// 4. Track reservation with a flag; unreserve on early return via defer
		var spaceReserved bool
		defer func() {
			// If we reserved space but the entry was not successfully added to heap, unreserve it
			if cfg.MaxBytes > 0 && spaceReserved {
				mux.Lock()
				storedBytes -= bodySize
				mux.Unlock()
			}
		}()

		if cfg.MaxBytes > 0 {
			mux.Lock()
			// Reserve space for the new entry first
			storedBytes += bodySize
			spaceReserved = true

			// Now evict entries until we're under the limit
			var keysToRemove []string
			var sizesToRemove []uint
			var candidates []evictionCandidate

			for storedBytes > cfg.MaxBytes {
				if heap.Len() == 0 {
					// Can't evict more, unreserve space and fail
					storedBytes -= bodySize
					// Set spaceReserved to false so the deferred cleanup does not unreserve again
					spaceReserved = false
					mux.Unlock()
					return errors.New("cache: insufficient space and no entries to evict")
				}
				next := heap.entries[0]
				keyToRemove, size := heap.removeFirst()
				keysToRemove = append(keysToRemove, keyToRemove)
				sizesToRemove = append(sizesToRemove, size)
				candidates = append(candidates, evictionCandidate{
					key:  keyToRemove,
					size: size,
					exp:  next.exp,
				})
				storedBytes -= size
			}
			mux.Unlock()

			// Perform deletions outside the lock
			if len(keysToRemove) > 0 {
				for i, keyToRemove := range keysToRemove {
					delErr := deleteKey(reqCtx, keyToRemove)
					if delErr == nil {
						continue
					}

					// Deletion failed: restore storedBytes for failed deletions
					mux.Lock()
					// Restore sizes of entries we failed to delete
					for j := i; j < len(sizesToRemove); j++ {
						storedBytes += sizesToRemove[j]
					}
					// Unreserve space for the new entry
					storedBytes -= bodySize
					spaceReserved = false

					// Re-add entries to the heap to keep expiration tracking consistent
					var restored []evictionCandidate
					for j := i; j < len(candidates); j++ {
						candidate := candidates[j]
						candidate.heapIdx = heap.put(candidate.key, candidate.exp, candidate.size)
						restored = append(restored, candidate)
					}
					mux.Unlock()

					var restoreErr error
					for _, candidate := range restored {
						if err := refreshHeapIndex(reqCtx, candidate); err != nil {
							restoreErr = errors.Join(restoreErr, err)
						}
					}

					if restoreErr != nil {
						return errors.Join(fmt.Errorf("cache: failed to delete key %q while evicting: %w", maskKey(keyToRemove), delErr), restoreErr)
					}

					return fmt.Errorf("cache: failed to delete key %q while evicting: %w", maskKey(keyToRemove), delErr)
				}
			}
		}

		e = manager.acquire()
		// Cache response
		e.body = utils.CopyBytes(c.Response().Body())
		e.status = c.Response().StatusCode()
		e.ctype = utils.CopyBytes(c.Response().Header.ContentType())
		e.cencoding = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderContentEncoding))
		e.private = false
		e.cacheControl = utils.CopyBytes(cacheControlBytes)
		e.expires = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderExpires))
		e.etag = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderETag))
		e.date = 0

		ageVal := uint64(0)
		if b := c.Response().Header.Peek(fiber.HeaderAge); len(b) > 0 {
			if v, err := fasthttp.ParseUint(b); err == nil {
				if v >= 0 {
					ageVal = uint64(v)
				}
			}
		} else {
			c.Response().Header.Set(fiber.HeaderAge, "0")
		}
		e.age = ageVal
		e.shareable = isSharedCacheAllowed
		now := time.Now().UTC()
		nowUnix := safeUnixSeconds(now)
		dateHeader := c.Response().Header.Peek(fiber.HeaderDate)
		parsedDate, _ := parseHTTPDate(dateHeader)
		e.date = clampDateSeconds(parsedDate, nowUnix)
		dateBytes := fasthttp.AppendHTTPDate(nil, secondsToTime(e.date))
		c.Response().Header.SetBytesV(fiber.HeaderDate, dateBytes)

		// Store all response headers
		// (more: https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1)
		if cfg.StoreResponseHeaders {
			allHeaders := c.Response().Header.All()
			e.headers = e.headers[:0]
			for key, value := range allHeaders {
				keyStr := string(key)
				if _, ok := ignoreHeaders[keyStr]; ok {
					continue
				}

				e.headers = append(e.headers, cachedHeader{
					key:   utils.CopyBytes(utils.UnsafeBytes(keyStr)),
					value: utils.CopyBytes(value),
				})
			}
		}

		expirationSource := expirationSourceConfig
		expiresParseError := false
		mustRevalidate := respCacheControl.mustRevalidate || respCacheControl.proxyRevalidate
		// default cache expiration
		expiration := cfg.Expiration
		if sharedCacheMode && respCacheControl.sMaxAgeSet {
			expiration = secondsToDuration(respCacheControl.sMaxAge)
			expirationSource = expirationSourceSMaxAge
		}
		if expirationSource == expirationSourceConfig {
			if respCacheControl.maxAgeSet {
				expiration = secondsToDuration(respCacheControl.maxAge)
				expirationSource = expirationSourceMaxAge
			} else if expiresBytes := c.Response().Header.Peek(fiber.HeaderExpires); len(expiresBytes) > 0 {
				expiresAt, err := fasthttp.ParseHTTPDate(expiresBytes)
				if err != nil {
					expiration = time.Nanosecond
					expiresParseError = true
				} else {
					expiration = time.Until(expiresAt)
				}
				expirationSource = expirationSourceExpires
			}
		}
		// Calculate expiration by response header or other setting
		if cfg.ExpirationGenerator != nil {
			expiration = cfg.ExpirationGenerator(c, &cfg)
			expirationSource = expirationSourceGenerator
		}
		e.forceRevalidate = expiresParseError
		e.revalidate = mustRevalidate

		storageExpiration := expiration
		if expiresParseError || storageExpiration < cfg.Expiration {
			storageExpiration = cfg.Expiration
		}

		if expiration <= 0 && !expiresParseError {
			c.Set(cfg.CacheHeader, cacheUnreachable)
			return nil
		}

		ts = atomic.LoadUint64(&timestamp)
		responseTS := max(ts, nowUnix)

		maxAgeSeconds := uint64(time.Duration(math.MaxInt64) / time.Second)
		var ageDuration time.Duration
		apparentAge := e.age
		if e.date > 0 && responseTS > e.date {
			dateAge := responseTS - e.date
			if dateAge > apparentAge {
				apparentAge = dateAge
			}
		}
		if expirationSource != expirationSourceExpires {
			if apparentAge > maxAgeSeconds {
				ageDuration = expiration + time.Second
			} else {
				ageDuration = time.Duration(apparentAge) * time.Second
			}
		}
		remainingExpiration := expiration - ageDuration
		if remainingExpiration <= 0 {
			if expirationSource != expirationSourceExpires {
				c.Set(cfg.CacheHeader, cacheUnreachable)
				return nil
			}
			remainingExpiration = 0
		}

		if !cfg.DisableVaryHeaders && shouldStoreVaryManifest {
			if err := storeVaryManifest(reqCtx, manager, manifestKey, varyNames, storageExpiration); err != nil {
				return err
			}
		}

		e.exp = responseTS + uint64(remainingExpiration.Seconds())
		e.ttl = uint64(expiration.Seconds())
		if expiresParseError {
			e.exp = ts + 1
		}

		// Store entry in heap (space already reserved in eviction phase)
		var heapIdx int
		if cfg.MaxBytes > 0 {
			mux.Lock()
			heapIdx = heap.put(key, e.exp, bodySize)
			e.heapidx = heapIdx
			// Note: storedBytes was incremented during reservation, and evictions
			// have already been accounted for, so no additional increment is needed
			spaceReserved = false // Clear flag to prevent defer from unreserving
			mux.Unlock()
		}

		cleanupOnStoreError := func(ctx context.Context, releaseEntry, rawStored bool) error {
			var cleanupErr error
			if cfg.MaxBytes > 0 {
				mux.Lock()
				_, size := heap.remove(heapIdx)
				storedBytes -= size
				mux.Unlock()
			}
			if releaseEntry {
				manager.release(e)
			}
			if rawStored {
				rawKey := key + "_body"
				if err := manager.del(ctx, rawKey); err != nil {
					cleanupErr = errors.Join(cleanupErr, fmt.Errorf("cache: failed to delete raw key %q after store error: %w", maskKey(rawKey), err))
				}
			}
			return cleanupErr
		}

		// For external Storage we store raw body separated
		if cfg.Storage != nil {
			if err := manager.setRaw(reqCtx, key+"_body", e.body, storageExpiration); err != nil {
				if cleanupErr := cleanupOnStoreError(reqCtx, true, false); cleanupErr != nil {
					err = errors.Join(err, cleanupErr)
				}
				return err
			}
			// avoid body msgp encoding
			e.body = nil
			if err := manager.set(reqCtx, key, e, storageExpiration); err != nil {
				if cleanupErr := cleanupOnStoreError(reqCtx, false, true); cleanupErr != nil {
					err = errors.Join(err, cleanupErr)
				}
				return err
			}
		} else {
			// Store entry in memory
			if err := manager.set(reqCtx, key, e, storageExpiration); err != nil {
				if cleanupErr := cleanupOnStoreError(reqCtx, true, false); cleanupErr != nil {
					err = errors.Join(err, cleanupErr)
				}
				return err
			}
		}

		// If revalidating, remove old heap entry now that replacement is successfully stored
		if cfg.MaxBytes > 0 && revalidate && oldHeapIdx >= 0 {
			mux.Lock()
			removeHeapEntry(key, oldHeapIdx)
			mux.Unlock()
		}

		c.Set(cfg.CacheHeader, cacheMiss)

		// Finish response
		return nil
	}
}

// hasDirective checks if a cache directive header value contains a directive (case-insensitive).
// A directive is considered matched when followed by end-of-string, ',', ' ', '\t', or '='
// per RFC 9111 §5.2.
func hasDirective(cc, directive string) bool {
	ccLen := len(cc)
	dirLen := len(directive)
	for i := 0; i <= ccLen-dirLen; i++ {
		if !utils.EqualFold(cc[i:i+dirLen], directive) {
			continue
		}
		if i > 0 {
			prev := cc[i-1]
			if prev != ' ' && prev != ',' && prev != '\t' {
				continue
			}
		}
		if i+dirLen == ccLen {
			return true
		}
		next := cc[i+dirLen]
		if next == ',' || next == ' ' || next == '\t' || next == '=' {
			return true
		}
	}

	return false
}

func cacheBodyFetchError(mask func(string) string, key string, err error) error {
	if errors.Is(err, errCacheMiss) {
		return fmt.Errorf("cache: no cached body for key %q: %w", mask(key), err)
	}
	return err
}

func parseUintDirective(val []byte) (uint64, bool) {
	if len(val) == 0 {
		return 0, false
	}
	parsed, err := fasthttp.ParseUint(val)
	if err != nil || parsed < 0 {
		return 0, false
	}
	return uint64(parsed), true
}

func parseCacheControlDirectives(cc []byte, fn func(key, value []byte)) {
	for i := 0; i < len(cc); {
		// skip leading separators and OWS (space/tab per RFC 9110 §5.6.3)
		for i < len(cc) && (cc[i] == ' ' || cc[i] == '\t' || cc[i] == ',') {
			i++
		}
		if i >= len(cc) {
			break
		}

		start := i
		for i < len(cc) && cc[i] != ',' {
			i++
		}
		partEnd := i
		for partEnd > start && (cc[partEnd-1] == ' ' || cc[partEnd-1] == '\t') {
			partEnd--
		}

		keyStart := start
		for keyStart < partEnd && (cc[keyStart] == ' ' || cc[keyStart] == '\t') {
			keyStart++
		}
		if keyStart >= partEnd {
			continue
		}

		keyEnd := keyStart
		for keyEnd < partEnd && cc[keyEnd] != '=' {
			keyEnd++
		}
		// Trim trailing OWS from key
		keyEndTrimmed := keyEnd
		for keyEndTrimmed > keyStart && (cc[keyEndTrimmed-1] == ' ' || cc[keyEndTrimmed-1] == '\t') {
			keyEndTrimmed--
		}
		key := cc[keyStart:keyEndTrimmed]

		var value []byte
		if keyEnd < partEnd && cc[keyEnd] == '=' {
			valueStart := keyEnd + 1
			for valueStart < partEnd && (cc[valueStart] == ' ' || cc[valueStart] == '\t') {
				valueStart++
			}
			valueEnd := partEnd
			for valueEnd > valueStart && (cc[valueEnd-1] == ' ' || cc[valueEnd-1] == '\t') {
				valueEnd--
			}
			if valueStart <= valueEnd {
				value = cc[valueStart:valueEnd]
				// Handle quoted-string values per RFC 9111 Section 5.2
				if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
					value = unquoteCacheDirective(value)
				}
			}
		}

		fn(key, value)
		i++ // skip comma
	}
}

// unquoteCacheDirective removes quotes and handles escaped characters in quoted-string values.
// Per RFC 9111 Section 5.2, quoted-string values follow RFC 9110 Section 5.6.4.
func unquoteCacheDirective(quoted []byte) []byte {
	if len(quoted) < 2 {
		return quoted
	}

	// Remove surrounding quotes
	inner := quoted[1 : len(quoted)-1]

	// Check if there are any escaped characters (backslash followed by another character)
	hasEscapes := false
	for i := 0; i < len(inner)-1; i++ {
		if inner[i] == '\\' {
			hasEscapes = true
			break
		}
	}

	// If no escapes, return the inner content directly
	if !hasEscapes {
		return inner
	}

	// Process escaped characters
	result := make([]byte, 0, len(inner))
	for i := 0; i < len(inner); i++ {
		if inner[i] == '\\' && i+1 < len(inner) {
			// Skip the backslash and take the next character
			i++
			result = append(result, inner[i])
		} else {
			result = append(result, inner[i])
		}
	}

	return result
}

type responseCacheControl struct {
	maxAge          uint64
	sMaxAge         uint64
	maxAgeSet       bool
	sMaxAgeSet      bool
	hasNoCache      bool
	hasNoStore      bool
	hasPrivate      bool
	hasPublic       bool
	mustRevalidate  bool
	proxyRevalidate bool
}

func parseResponseCacheControl(cc []byte) responseCacheControl {
	parsed := responseCacheControl{}
	parseCacheControlDirectives(cc, func(key, value []byte) {
		switch {
		case utils.EqualFold(utils.UnsafeString(key), noStore):
			parsed.hasNoStore = true
		case utils.EqualFold(utils.UnsafeString(key), noCache):
			parsed.hasNoCache = true
		case utils.EqualFold(utils.UnsafeString(key), privateDirective):
			parsed.hasPrivate = true
		case utils.EqualFold(utils.UnsafeString(key), "public"):
			parsed.hasPublic = true
		case utils.EqualFold(utils.UnsafeString(key), "max-age"):
			if v, ok := parseUintDirective(value); ok {
				parsed.maxAgeSet = true
				parsed.maxAge = v
			}
		case utils.EqualFold(utils.UnsafeString(key), "s-maxage"):
			if v, ok := parseUintDirective(value); ok {
				parsed.sMaxAgeSet = true
				parsed.sMaxAge = v
			}
		case utils.EqualFold(utils.UnsafeString(key), "must-revalidate"):
			parsed.mustRevalidate = true
		case utils.EqualFold(utils.UnsafeString(key), "proxy-revalidate"):
			parsed.proxyRevalidate = true
		default:
			// ignore unknown directives
		}
	})
	return parsed
}

// parseMaxAge extracts the max-age directive from a Cache-Control header.
func parseMaxAge(cc string) (time.Duration, bool) {
	parsed := parseResponseCacheControl(utils.UnsafeBytes(cc))
	if !parsed.maxAgeSet {
		return 0, false
	}
	return secondsToDuration(parsed.maxAge), true
}

func parseRequestCacheControl(cc []byte) requestCacheDirectives {
	directives := requestCacheDirectives{}
	parseCacheControlDirectives(cc, func(key, value []byte) {
		switch {
		case utils.EqualFold(utils.UnsafeString(key), noStore):
			directives.noStore = true
		case utils.EqualFold(utils.UnsafeString(key), noCache):
			directives.noCache = true
		case utils.EqualFold(utils.UnsafeString(key), "only-if-cached"):
			directives.onlyIfCached = true
		case utils.EqualFold(utils.UnsafeString(key), "max-age"):
			if sec, ok := parseUintDirective(value); ok {
				directives.maxAgeSet = true
				directives.maxAge = sec
			}
		case utils.EqualFold(utils.UnsafeString(key), "max-stale"):
			directives.maxStaleSet = true
			directives.maxStaleAny = len(value) == 0
			if !directives.maxStaleAny {
				if sec, ok := parseUintDirective(value); ok {
					directives.maxStale = sec
				}
			}
		case utils.EqualFold(utils.UnsafeString(key), "min-fresh"):
			if sec, ok := parseUintDirective(value); ok {
				directives.minFreshSet = true
				directives.minFresh = sec
			}
		default:
			// ignore unknown directives
		}
	})
	return directives
}

func parseRequestCacheControlString(cc string) requestCacheDirectives {
	return parseRequestCacheControl(utils.UnsafeBytes(cc))
}

func cachedResponseAge(e *item, now uint64) uint64 {
	clampedDate := clampDateSeconds(e.date, now)

	resident := uint64(0)
	if e.exp != 0 {
		if e.exp <= now {
			resident = e.ttl + (now - e.exp)
		} else {
			resident = e.ttl - (e.exp - now)
		}
	}

	dateAge := uint64(0)
	if clampedDate != 0 && now > clampedDate {
		dateAge = now - clampedDate
	}

	currentAge := max(dateAge, max(resident, e.age))
	return currentAge
}

func appendWarningHeaders(h *fasthttp.ResponseHeader, servedStale, heuristicFreshness bool) { //nolint:revive // flags are intentional to represent Warning variants
	if servedStale {
		h.Add(fiber.HeaderWarning, `110 - "Response is stale"`)
	}
	if heuristicFreshness {
		h.Add(fiber.HeaderWarning, `113 - "Heuristic expiration"`)
	}
}

func remainingFreshness(e *item, now uint64) uint64 {
	if e == nil || e.exp == 0 || now >= e.exp {
		return 0
	}

	return e.exp - now
}

func isHeuristicFreshness(e *item, cfg *Config, entryAge uint64) bool {
	const heuristicAgeThresholdSeconds = uint64(24 * time.Hour / time.Second)
	if entryAge <= heuristicAgeThresholdSeconds {
		return false
	}

	if len(e.expires) > 0 {
		return false
	}

	cacheControl := utils.UnsafeString(e.cacheControl)
	if parsedCC := parseResponseCacheControl(utils.UnsafeBytes(cacheControl)); parsedCC.maxAgeSet || parsedCC.sMaxAgeSet {
		return false
	}

	return cfg.Expiration > 0
}

func lookupCachedHeader(headers []cachedHeader, name string) ([]byte, bool) {
	for i := range headers {
		if utils.EqualFold(utils.UnsafeString(headers[i].key), name) {
			return headers[i].value, true
		}
	}
	return nil, false
}

func parseHTTPDate(dateBytes []byte) (uint64, bool) {
	if len(dateBytes) == 0 {
		return 0, false
	}
	parsedDate, err := fasthttp.ParseHTTPDate(dateBytes)
	if err != nil {
		return 0, false
	}

	return safeUnixSeconds(parsedDate), true
}

func clampDateSeconds(dateSeconds, fallback uint64) uint64 {
	const maxUnixSeconds = uint64(math.MaxInt64)
	if dateSeconds == 0 || dateSeconds > maxUnixSeconds || dateSeconds > fallback {
		return fallback
	}

	return dateSeconds
}

func safeUnixSeconds(t time.Time) uint64 {
	sec := t.Unix()
	if sec < 0 {
		return 0
	}

	return uint64(sec)
}

func secondsToTime(sec uint64) time.Time {
	var clamped int64
	if sec > uint64(math.MaxInt64) {
		clamped = math.MaxInt64
	} else {
		clamped = int64(sec)
	}

	return time.Unix(clamped, 0).UTC()
}

func secondsToDuration(sec uint64) time.Duration {
	const maxSeconds = uint64(math.MaxInt64) / uint64(time.Second)
	if sec > maxSeconds {
		return time.Duration(math.MaxInt64)
	}
	return time.Duration(sec) * time.Second
}

func defaultKeyGenerator(c fiber.Ctx, cfg *Config) string {
	v := keyBufferPool.Get()
	bufPtr, ok := v.(*[]byte)
	if !ok || bufPtr == nil {
		b := make([]byte, 0, defaultKeyBufferCap)
		bufPtr = &b
	}

	buf := (*bufPtr)[:0]
	// Escape delimiters in path to prevent crafted paths from injecting key structure
	buf = append(buf, boundKeySegment(escapeKeyDelimiters(c.Path()))...)

	if !cfg.DisableQueryKeys {
		buf = append(buf, '|', 'q', '=')
		buf = append(buf, canonicalQueryString(c.Request().URI())...)
	}

	if len(cfg.KeyHeaders) > 0 {
		buf = append(buf, '|', 'h', '=')
		buf = append(buf, canonicalHeaderSubset(&c.Request().Header, cfg.KeyHeaders)...)
	}

	if len(cfg.KeyCookies) > 0 {
		buf = append(buf, '|', 'c', '=')
		buf = append(buf, canonicalCookieSubset(c, cfg.KeyCookies)...)
	}

	result := string(buf)

	// Reset buffer and return to pool, but discard if it grew too large
	// to prevent pool from retaining oversized buffers
	if cap(buf) <= defaultKeyBufferCap*4 {
		*bufPtr = buf
		keyBufferPool.Put(bufPtr)
	}

	return result
}

func canonicalQueryString(uri *fasthttp.URI) string {
	raw := uri.QueryString()
	if len(raw) == 0 {
		return ""
	}

	query := utils.CopyString(utils.UnsafeString(raw))

	// Pre-scan query string to detect excessive parameters before expensive parsing.
	// This prevents DoS via url.ParseQuery allocating large maps/slices.
	if len(query) > maxQueryBufferSize {
		return boundKeySegment(escapeKeyDelimiters(query))
	}

	// Fast path: single key=value pair needs no parsing or sorting
	if strings.IndexByte(query, '&') < 0 {
		return boundKeySegment(escapeKeyDelimiters(query))
	}

	// Quick count of potential parameters (ampersands + 1)
	paramCount := 1
	for i := 0; i < len(query); i++ {
		if query[i] == '&' {
			paramCount++
			if paramCount > maxQueryParams {
				// Too many parameters detected, hash without parsing
				return boundKeySegment(escapeKeyDelimiters(query))
			}
		}
	}

	parsed, err := url.ParseQuery(query)
	if err != nil {
		return boundKeySegment(escapeKeyDelimiters(query))
	}

	// Double-check actual parameter count after parsing
	actualCount := 0
	for _, values := range parsed {
		actualCount += len(values)
		if actualCount > maxQueryParams {
			return boundKeySegment(escapeKeyDelimiters(query))
		}
	}

	keys := make([]string, 0, len(parsed))
	for key := range parsed {
		keys = append(keys, key)
	}
	sort.Strings(keys)

	// Use pooled buffer to prevent excessive memory allocation during URL escaping.
	// URL escaping can expand strings up to 3x (each byte -> %XX).
	v := keyBufferPool.Get()
	bufPtr, ok := v.(*[]byte)
	if !ok || bufPtr == nil {
		b := make([]byte, 0, defaultKeyBufferCap)
		bufPtr = &b
	}
	buf := (*bufPtr)[:0]

	for _, key := range keys {
		values := parsed[key]
		sort.Strings(values)
		for _, value := range values {
			if len(buf) > 0 {
				buf = append(buf, '&')
			}

			escapedKey := url.QueryEscape(key)
			escapedValue := url.QueryEscape(value)

			// Check buffer size before appending to prevent unbounded growth
			if len(buf)+len(escapedKey)+len(escapedValue)+2 > maxQueryBufferSize {
				if cap(buf) <= defaultKeyBufferCap*4 {
					*bufPtr = buf
					keyBufferPool.Put(bufPtr)
				}
				return boundKeySegment(escapeKeyDelimiters(query))
			}

			buf = append(buf, escapedKey...)
			buf = append(buf, '=')
			buf = append(buf, escapedValue...)
		}
	}

	result := boundKeySegment(string(buf))

	// Return buffer to pool if not oversized
	if cap(buf) <= defaultKeyBufferCap*4 {
		*bufPtr = buf
		keyBufferPool.Put(bufPtr)
	}

	return result
}

func canonicalHeaderSubset(header *fasthttp.RequestHeader, names []string) string {
	if len(names) == 0 {
		return ""
	}

	buf := make([]byte, 0, len(names)*16)
	for idx, name := range names {
		if idx > 0 {
			buf = append(buf, '|')
		}
		// Escape name (though names are normalized and trusted)
		buf = append(buf, escapeKeyDelimiters(name)...)
		buf = append(buf, ':')
		headerValue := header.Peek(name)
		// Escape value to prevent delimiter injection
		escapedValue := escapeKeyDelimiters(utils.UnsafeString(headerValue))
		buf = append(buf, boundKeySegment(escapedValue)...)
	}

	return string(buf)
}

func canonicalCookieSubset(c fiber.Ctx, names []string) string {
	if len(names) == 0 {
		return ""
	}

	buf := make([]byte, 0, len(names)*16)
	for idx, name := range names {
		if idx > 0 {
			buf = append(buf, '|')
		}
		// Escape name (though names are normalized and trusted)
		buf = append(buf, escapeKeyDelimiters(name)...)
		buf = append(buf, ':')
		cookieValue := c.Cookies(name)
		// Escape value to prevent delimiter injection
		escapedValue := escapeKeyDelimiters(cookieValue)
		buf = append(buf, boundKeySegment(escapedValue)...)
	}

	return string(buf)
}

// escapeKeyDelimiters escapes pipe, colon, and backslash characters used as delimiters in cache keys
// to prevent injection attacks where crafted values could collide with different inputs
func escapeKeyDelimiters(s string) string {
	// Fast path: no characters to escape
	if !strings.ContainsAny(s, "|:\\") {
		return s
	}

	// Escape | as \p and : as \c, and \ as \\ (backslash must be escaped first)
	result := strings.ReplaceAll(s, "\\", "\\\\")
	result = strings.ReplaceAll(result, "|", "\\p")
	result = strings.ReplaceAll(result, ":", "\\c")
	return result
}

func boundKeySegment(segment string) string {
	if len(segment) <= maxKeyDimensionSegmentLength {
		return segment
	}
	hash := sha256.Sum256(utils.UnsafeBytes(segment))
	return "sha256:" + hex.EncodeToString(hash[:])
}

func parseVary(vary string) ([]string, bool) {
	names := make([]string, 0, 8)
	count := 0
	for part := range strings.SplitSeq(vary, ",") {
		name := utils.TrimSpace(utilsstrings.ToLower(part))
		if name == "" {
			continue
		}
		if name == "*" {
			return nil, true
		}

		// Protect against DoS via excessive Vary headers
		count++
		if count > maxVaryHeaders {
			// Too many Vary headers, treat as uncacheable (same as Vary: *)
			return nil, true
		}

		names = append(names, name)
	}

	if len(names) == 0 {
		return nil, false
	}

	sort.Strings(names)
	return names, false
}

func makeBuildVaryKeyFunc(hexBufPool *sync.Pool) func([]string, *fasthttp.RequestHeader) string {
	return func(names []string, hdr *fasthttp.RequestHeader) string {
		sum := sha256.New()
		for _, name := range names {
			_, _ = sum.Write(utils.UnsafeBytes(name)) //nolint:errcheck // hash.Hash.Write for std hashes never errors
			_, _ = sum.Write([]byte{0})               //nolint:errcheck // hash.Hash.Write for std hashes never errors
			_, _ = sum.Write(hdr.Peek(name))          //nolint:errcheck // hash.Hash.Write for std hashes never errors
			_, _ = sum.Write([]byte{0})               //nolint:errcheck // hash.Hash.Write for std hashes never errors
		}

		var hashBytes [sha256.Size]byte
		sum.Sum(hashBytes[:0])

		v := hexBufPool.Get()
		bufPtr, ok := v.(*[]byte)
		if !ok || bufPtr == nil {
			b := make([]byte, hexLen)
			bufPtr = &b
		}

		buf := *bufPtr
		// Defensive in case someone changed Pool.New or Put a different sized buffer.
		if cap(buf) < hexLen {
			buf = make([]byte, hexLen)
		} else {
			buf = buf[:hexLen]
		}
		*bufPtr = buf

		hex.Encode(buf, hashBytes[:])
		result := "|vary|" + string(buf)

		hexBufPool.Put(bufPtr)
		return result
	}
}

func storeVaryManifest(ctx context.Context, manager *manager, manifestKey string, names []string, exp time.Duration) error {
	if len(names) == 0 {
		return nil
	}
	data := strings.Join(names, ",")
	return manager.setRaw(ctx, manifestKey, utils.UnsafeBytes(data), exp)
}

//nolint:gocritic // returning explicit values keeps the signature concise while avoiding unnecessary named results
func loadVaryManifest(ctx context.Context, manager *manager, manifestKey string) ([]string, bool, error) {
	raw, err := manager.getRaw(ctx, manifestKey)
	if err != nil {
		if errors.Is(err, errCacheMiss) {
			return nil, false, nil
		}
		return nil, false, err
	}
	manifest := utils.UnsafeString(raw)
	names, hasStar := parseVary(manifest)
	if hasStar {
		return nil, false, nil
	}
	return names, len(names) > 0, nil
}

func allowsSharedCacheDirectives(cc responseCacheControl) bool {
	if cc.hasPrivate {
		return false
	}
	if cc.hasPublic || cc.sMaxAgeSet || cc.mustRevalidate || cc.proxyRevalidate {
		return true
	}

	// RFC 9111 §4.2.2 permits Expires as an absolute expiry for cacheable responses, but for
	// authenticated requests §3.6 requires an explicit shared-cache directive. Therefore,
	// an Expires header alone MUST NOT allow sharing when Authorization is present.
	return false
}

func allowsSharedCache(cc string) bool {
	return allowsSharedCacheDirectives(parseResponseCacheControl(utils.UnsafeBytes(cc)))
}

func makeHashAuthFunc(hexBufPool *sync.Pool) func([]byte) string {
	return func(authHeader []byte) string {
		sum := sha256.Sum256(authHeader)

		v := hexBufPool.Get()
		bufPtr, ok := v.(*[]byte)
		if !ok || bufPtr == nil {
			b := make([]byte, hexLen)
			bufPtr = &b
		}

		buf := *bufPtr
		if cap(buf) < hexLen {
			buf = make([]byte, hexLen)
		} else {
			buf = buf[:hexLen]
		}
		*bufPtr = buf

		hex.Encode(buf, sum[:])
		result := string(buf)

		hexBufPool.Put(bufPtr)
		return result
	}
}
