// Package authprojection owns the Edge-local immutable principal projection
// cache. The cache is deliberately transport-neutral: callers install a
// snapshot only after the surrounding transport has authenticated its source.
package authprojection

import (
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"sync/atomic"
	"time"

	"google.golang.org/protobuf/proto"

	iop "iop/proto/gen/iop"
)

var (
	ErrInvalidProjection   = errors.New("authprojection: invalid projection")
	ErrProjectionTooLarge  = errors.New("authprojection: projection exceeds configured bounds")
	ErrGenerationNotHigher = errors.New("authprojection: generation is not higher than the installed generation")
	ErrGenerationConflict  = errors.New("authprojection: equal generation changes immutable content")
	ErrProjectionExpired   = errors.New("authprojection: projection is expired")
	ErrBindingStale        = errors.New("authprojection: credential binding is stale")
)

// State distinguishes the compatibility default from managed authorization.
// Once any verified snapshot is installed, expiry never returns the cache to
// unmanaged mode; managed requests remain fail-closed.
type State uint8

const (
	StateUnmanaged State = iota
	StateFresh
	StateExpired
)

func (s State) String() string {
	switch s {
	case StateUnmanaged:
		return "unmanaged"
	case StateFresh:
		return "fresh"
	case StateExpired:
		return "expired"
	default:
		return "unknown"
	}
}

// ValidateBinding is the immediate pre-lease and pre-send revocation fence.
// It checks one atomic snapshot and requires an exact generation and route.
func (c *Cache) ValidateBinding(binding *iop.CredentialLeaseBinding) error {
	if c == nil || binding == nil {
		return ErrBindingStale
	}
	installed := c.current.Load()
	if c.stateFor(installed) != StateFresh || installed.generation != binding.GetProjectionGeneration() {
		return ErrBindingStale
	}
	for _, route := range installed.routes {
		if route.PrincipalRef == binding.GetPrincipalRef() && route.CredentialSlotRef == binding.GetCredentialSlotRef() &&
			route.RouteID == binding.GetRouteId() && route.ProfileID == binding.GetProfileId() &&
			route.UpstreamModel == binding.GetUpstreamTarget() && route.RouteRevision == binding.GetRouteRevision() &&
			route.CredentialRevision == binding.GetCredentialRevision() {
			return nil
		}
	}
	return ErrBindingStale
}

// Limits are hard in-memory and encoded-size bounds for one snapshot.
type Limits struct {
	MaxTokens int
	MaxRoutes int
	MaxBytes  int
}

func DefaultLimits() Limits {
	return Limits{
		MaxTokens: 4096,
		MaxRoutes: 16384,
		MaxBytes:  4 << 20,
	}
}

func normalizeLimits(limits Limits) Limits {
	defaults := DefaultLimits()
	if limits.MaxTokens <= 0 {
		limits.MaxTokens = defaults.MaxTokens
	}
	if limits.MaxRoutes <= 0 {
		limits.MaxRoutes = defaults.MaxRoutes
	}
	if limits.MaxBytes <= 0 {
		limits.MaxBytes = defaults.MaxBytes
	}
	return limits
}

// Principal is safe authenticated caller metadata. It contains no raw token.
type Principal struct {
	PrincipalRef   string
	PrincipalAlias string
	TokenRef       string
	TokenRevision  uint64
}

// Route is a secret-free immutable route binding retained for later secure
// transport activation.
type Route struct {
	RouteID            string
	RouteAlias         string
	PrincipalRef       string
	CredentialSlotRef  string
	ProfileID          string
	UpstreamModel      string
	ResourceSelector   string
	RouteRevision      uint64
	CredentialRevision uint64
}

// AuthenticatedView is the secret-free, request-local result of authenticating
// a principal against one immutable projection snapshot. Routes are copied so
// callers cannot mutate cache-owned state or observe a later generation.
type AuthenticatedView struct {
	Principal  Principal
	Routes     []Route
	Generation uint64
	State      State
}

// Reader is the narrow authentication surface consumed by HTTP ingress.
type Reader interface {
	AuthenticatedView([sha256.Size]byte) (AuthenticatedView, bool)
	LookupDigest([sha256.Size]byte) (Principal, State, bool)
	State() State
	Generation() (uint64, bool)
	RoutesForPrincipal(principalRef string) ([]Route, State)
}

// AuthenticatedView performs the digest scan and principal route collection
// from one atomic snapshot load. Managed HTTP ingress must retain this value
// for the whole request rather than mixing authentication and routing reads
// across generations.
func (c *Cache) AuthenticatedView(digest [sha256.Size]byte) (AuthenticatedView, bool) {
	if c == nil {
		return AuthenticatedView{State: StateUnmanaged}, false
	}
	installed := c.current.Load()
	state := c.stateFor(installed)
	if state != StateFresh {
		return AuthenticatedView{State: state}, false
	}
	matchedIndex := -1
	found := 0
	for index, token := range installed.tokens {
		equal := subtle.ConstantTimeCompare(digest[:], token.digest[:])
		selectCurrent := equal & (found ^ 1)
		matchedIndex = subtle.ConstantTimeSelect(selectCurrent, index, matchedIndex)
		found |= equal
	}
	if found != 1 {
		return AuthenticatedView{State: state, Generation: installed.generation}, false
	}
	principal := installed.tokens[matchedIndex].principal
	view := AuthenticatedView{Principal: principal, Generation: installed.generation, State: state}
	for _, route := range installed.routes {
		if route.PrincipalRef == principal.PrincipalRef {
			view.Routes = append(view.Routes, route)
		}
	}
	return view, true
}

type projectedToken struct {
	digest    [sha256.Size]byte
	principal Principal
}

type snapshot struct {
	generation uint64
	issuedAt   time.Time
	expiresAt  time.Time
	tokens     []projectedToken
	routes     []Route
}

// Cache performs lock-free reads and atomic whole-snapshot swaps.
type Cache struct {
	limits  Limits
	now     func() time.Time
	current atomic.Pointer[snapshot]
}

func NewCache(limits Limits, clock func() time.Time) *Cache {
	if clock == nil {
		clock = time.Now
	}
	return &Cache{limits: normalizeLimits(limits), now: clock}
}

// Apply validates and normalizes an immutable candidate. Higher generations
// replace the installed snapshot. An equal generation may only advance the
// freshness window when all authorization content is identical.
func (c *Cache) Apply(projection *iop.PrincipalProjection) error {
	if c == nil || projection == nil {
		return fmt.Errorf("%w: projection is required", ErrInvalidProjection)
	}
	if len(projection.GetTokens()) > c.limits.MaxTokens {
		return fmt.Errorf("%w: tokens=%d max=%d", ErrProjectionTooLarge, len(projection.GetTokens()), c.limits.MaxTokens)
	}
	if len(projection.GetRoutes()) > c.limits.MaxRoutes {
		return fmt.Errorf("%w: routes=%d max=%d", ErrProjectionTooLarge, len(projection.GetRoutes()), c.limits.MaxRoutes)
	}
	if size := proto.Size(projection); size > c.limits.MaxBytes {
		return fmt.Errorf("%w: bytes=%d max=%d", ErrProjectionTooLarge, size, c.limits.MaxBytes)
	}

	issuedAt := time.Unix(0, projection.GetIssuedAtUnixNano()).UTC()
	expiresAt := time.Unix(0, projection.GetExpiresAtUnixNano()).UTC()
	if projection.GetIssuedAtUnixNano() <= 0 || projection.GetExpiresAtUnixNano() <= projection.GetIssuedAtUnixNano() {
		return fmt.Errorf("%w: invalid issued/expiry timestamps", ErrInvalidProjection)
	}
	validationNow := c.now()
	if issuedAt.After(validationNow) {
		return fmt.Errorf("%w: projection is not issued yet", ErrInvalidProjection)
	}
	if !validationNow.Before(expiresAt) {
		return ErrProjectionExpired
	}

	candidate := &snapshot{
		generation: projection.GetGeneration(),
		issuedAt:   issuedAt,
		expiresAt:  expiresAt,
		tokens:     make([]projectedToken, 0, len(projection.GetTokens())),
		routes:     make([]Route, 0, len(projection.GetRoutes())),
	}
	seenDigests := make(map[string]struct{}, len(projection.GetTokens()))
	seenTokenRefs := make(map[string]struct{}, len(projection.GetTokens()))
	for index, token := range projection.GetTokens() {
		if token == nil {
			return fmt.Errorf("%w: token %d is nil", ErrInvalidProjection, index)
		}
		digestText := strings.ToLower(strings.TrimSpace(token.GetTokenDigestSha256()))
		digestBytes, err := hex.DecodeString(digestText)
		if err != nil || len(digestBytes) != sha256.Size {
			return fmt.Errorf("%w: token %d digest must be SHA-256 hex", ErrInvalidProjection, index)
		}
		if strings.TrimSpace(token.GetPrincipalRef()) == "" || strings.TrimSpace(token.GetTokenRef()) == "" {
			return fmt.Errorf("%w: token %d principal_ref and token_ref are required", ErrInvalidProjection, index)
		}
		if _, duplicate := seenDigests[digestText]; duplicate {
			return fmt.Errorf("%w: duplicate token digest", ErrInvalidProjection)
		}
		seenDigests[digestText] = struct{}{}
		tokenRef := strings.TrimSpace(token.GetTokenRef())
		if _, duplicate := seenTokenRefs[tokenRef]; duplicate {
			return fmt.Errorf("%w: duplicate token reference", ErrInvalidProjection)
		}
		seenTokenRefs[tokenRef] = struct{}{}
		var digest [sha256.Size]byte
		copy(digest[:], digestBytes)
		candidate.tokens = append(candidate.tokens, projectedToken{
			digest: digest,
			principal: Principal{
				PrincipalRef:   strings.TrimSpace(token.GetPrincipalRef()),
				PrincipalAlias: strings.TrimSpace(token.GetPrincipalAlias()),
				TokenRef:       tokenRef,
				TokenRevision:  token.GetTokenRevision(),
			},
		})
	}

	seenRouteSelectors := make(map[string]struct{}, 2*len(projection.GetRoutes()))
	for index, route := range projection.GetRoutes() {
		if route == nil {
			return fmt.Errorf("%w: route %d is nil", ErrInvalidProjection, index)
		}
		if strings.TrimSpace(route.GetRouteId()) == "" ||
			strings.TrimSpace(route.GetPrincipalRef()) == "" ||
			strings.TrimSpace(route.GetCredentialSlotRef()) == "" ||
			strings.TrimSpace(route.GetProfileId()) == "" ||
			strings.TrimSpace(route.GetUpstreamModel()) == "" ||
			strings.TrimSpace(route.GetResourceSelector()) == "" {
			return fmt.Errorf("%w: route %d is missing required metadata", ErrInvalidProjection, index)
		}
		principalRef := strings.TrimSpace(route.GetPrincipalRef())
		routeID := strings.TrimSpace(route.GetRouteId())
		routeAlias := strings.TrimSpace(route.GetRouteAlias())
		routeKey := principalRef + "\x00" + routeID
		if _, duplicate := seenRouteSelectors[routeKey]; duplicate {
			return fmt.Errorf("%w: ambiguous principal route selector", ErrInvalidProjection)
		}
		seenRouteSelectors[routeKey] = struct{}{}
		if routeAlias != "" {
			aliasKey := principalRef + "\x00" + routeAlias
			if _, duplicate := seenRouteSelectors[aliasKey]; duplicate {
				return fmt.Errorf("%w: ambiguous principal route selector", ErrInvalidProjection)
			}
			seenRouteSelectors[aliasKey] = struct{}{}
		}
		candidate.routes = append(candidate.routes, Route{
			RouteID:            routeID,
			RouteAlias:         routeAlias,
			PrincipalRef:       principalRef,
			CredentialSlotRef:  strings.TrimSpace(route.GetCredentialSlotRef()),
			ProfileID:          strings.TrimSpace(route.GetProfileId()),
			UpstreamModel:      strings.TrimSpace(route.GetUpstreamModel()),
			ResourceSelector:   strings.TrimSpace(route.GetResourceSelector()),
			RouteRevision:      route.GetRouteRevision(),
			CredentialRevision: route.GetCredentialRevision(),
		})
	}

	for {
		installed := c.current.Load()
		if installed != nil && candidate.generation < installed.generation {
			return fmt.Errorf("%w: candidate=%d installed=%d", ErrGenerationNotHigher, candidate.generation, installed.generation)
		}
		if installed != nil && candidate.generation == installed.generation {
			if !sameImmutableContent(candidate, installed) {
				return fmt.Errorf("%w: generation=%d", ErrGenerationConflict, candidate.generation)
			}
			if !candidate.issuedAt.After(installed.issuedAt) || !candidate.expiresAt.After(installed.expiresAt) {
				return nil
			}
		}
		if !c.now().Before(candidate.expiresAt) {
			return ErrProjectionExpired
		}
		if c.current.CompareAndSwap(installed, candidate) {
			return nil
		}
	}
}

func sameImmutableContent(left, right *snapshot) bool {
	if left == nil || right == nil || len(left.tokens) != len(right.tokens) || len(left.routes) != len(right.routes) {
		return false
	}
	tokens := make(map[projectedToken]int, len(left.tokens))
	for _, token := range left.tokens {
		tokens[token]++
	}
	for _, token := range right.tokens {
		if tokens[token] == 0 {
			return false
		}
		tokens[token]--
	}
	routes := make(map[Route]int, len(left.routes))
	for _, route := range left.routes {
		routes[route]++
	}
	for _, route := range right.routes {
		if routes[route] == 0 {
			return false
		}
		routes[route]--
	}
	return true
}

func (c *Cache) stateFor(installed *snapshot) State {
	if installed == nil {
		return StateUnmanaged
	}
	if !c.now().Before(installed.expiresAt) {
		return StateExpired
	}
	return StateFresh
}

func (c *Cache) State() State {
	if c == nil {
		return StateUnmanaged
	}
	return c.stateFor(c.current.Load())
}

func (c *Cache) Generation() (uint64, bool) {
	if c == nil {
		return 0, false
	}
	installed := c.current.Load()
	if installed == nil {
		return 0, false
	}
	return installed.generation, true
}

// LookupDigest compares the supplied verifier digest against every bounded
// candidate using crypto/subtle. The raw caller token never enters this cache.
func (c *Cache) LookupDigest(digest [sha256.Size]byte) (Principal, State, bool) {
	view, ok := c.AuthenticatedView(digest)
	return view.Principal, view.State, ok
}

// RoutesForPrincipal returns a copy of the fresh principal route projection.
// Managed expired state returns no routes and remains fail-closed.
func (c *Cache) RoutesForPrincipal(principalRef string) ([]Route, State) {
	if c == nil {
		return nil, StateUnmanaged
	}
	installed := c.current.Load()
	state := c.stateFor(installed)
	if state != StateFresh {
		return nil, state
	}
	routes := make([]Route, 0)
	for _, route := range installed.routes {
		if route.PrincipalRef == principalRef {
			routes = append(routes, route)
		}
	}
	return routes, state
}

var _ Reader = (*Cache)(nil)
