package delegationv1

import (
	"errors"
	"sort"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"
)

const (
	// MaxIdentifierBytes is the hard v1 wire-grammar ceiling for every opaque
	// identifier except CallerWorkloadID. It is deliberately a protocol safety
	// ceiling, not a claim that every authority accepts 255-byte identifiers.
	MaxIdentifierBytes = 255
	// MaxWorkloadIdentifierBytes leaves room for URI-shaped workload identities
	// without making an unbounded identity string part of the signed envelope.
	MaxWorkloadIdentifierBytes = 1024
	// MaxSigningKeyIDBytes bounds the untrusted key-selector parsed before
	// signature verification.
	MaxSigningKeyIDBytes = 128
	// MaxCapabilities bounds requested and signed-token capability sets, not
	// the independently authoritative grants from which they are intersected.
	MaxCapabilities    = 32
	MaxCapabilityBytes = 128
	// MaxAuthorityCapabilities bounds trusted workload, installation, session,
	// and verifier-registry sets without widening the v1 token grammar.
	MaxAuthorityCapabilities = 1024
	// MaxCompactTokenBytes is the defensive outer bound for the ASCII compact
	// JWS metadata value, before decoding or key lookup.
	MaxCompactTokenBytes = 16 * 1024
	// MaxLifetime is the hard protocol ceiling. Runtime Identity may issue a
	// shorter production lifetime, but never a longer one.
	MaxLifetime = 10 * time.Minute
	// MaxClockSkew is the hard protocol ceiling for a verifier's local skew
	// allowance. Production may configure a smaller value.
	MaxClockSkew = 30 * time.Second
)

// Claims is the complete semantic authorization claim set signed by Runtime
// Identity. SigningKeyID is protected JWS header metadata and therefore is not
// a semantic authorization claim.
//
// IssuedAt and ExpiresAt are whole Unix seconds. MaxLifetime and MaxClockSkew
// are defensive protocol ceilings. P6-D still owns the shorter production
// lifetime, configured skew, and key-overlap duration.
type Claims struct {
	DelegationID      string   `json:"jti"`
	OrgID             string   `json:"org_id"`
	PrincipalID       string   `json:"sub"`
	SessionID         string   `json:"session_id"`
	CallerWorkloadID  string   `json:"caller_workload_id"`
	AppInstallationID string   `json:"app_installation_id"`
	RuntimeAudience   string   `json:"aud"`
	Capabilities      []string `json:"capabilities"`
	AuthEpoch         uint64   `json:"auth_epoch"`
	SecurityEpoch     uint64   `json:"security_epoch"`
	IssuedAt          int64    `json:"iat"`
	ExpiresAt         int64    `json:"exp"`
}

// Validate checks the closed v1 semantic grammar and hard protocol ceilings.
// It does not verify a signature, consult authoritative state, or compare the
// current time. Callers must not treat successful structural validation as
// authentication.
func (c Claims) Validate() error {
	for _, field := range []struct {
		name  string
		value string
		max   int
	}{
		{name: "DelegationID", value: c.DelegationID, max: MaxIdentifierBytes},
		{name: "OrgID", value: c.OrgID, max: MaxIdentifierBytes},
		{name: "PrincipalID", value: c.PrincipalID, max: MaxIdentifierBytes},
		{name: "SessionID", value: c.SessionID, max: MaxIdentifierBytes},
		{name: "CallerWorkloadID", value: c.CallerWorkloadID, max: MaxWorkloadIdentifierBytes},
		{name: "AppInstallationID", value: c.AppInstallationID, max: MaxIdentifierBytes},
		{name: "RuntimeAudience", value: c.RuntimeAudience, max: MaxIdentifierBytes},
	} {
		if !validOpaque(field.value, field.max) {
			return errors.New("runtime delegation: invalid " + field.name)
		}
	}
	if len(c.Capabilities) == 0 {
		return errors.New("runtime delegation: at least one capability is required")
	}
	if len(c.Capabilities) > MaxCapabilities {
		return errors.New("runtime delegation: too many capabilities")
	}
	if !sort.StringsAreSorted(c.Capabilities) {
		return errors.New("runtime delegation: capabilities must be in ascending bytewise order")
	}
	for i, capability := range c.Capabilities {
		if !validCapability(capability) || len(capability) > MaxCapabilityBytes {
			return errors.New("runtime delegation: invalid capability")
		}
		if i > 0 && capability == c.Capabilities[i-1] {
			return errors.New("runtime delegation: duplicate capability")
		}
	}
	if c.AuthEpoch == 0 {
		return errors.New("runtime delegation: AuthEpoch must be positive")
	}
	if c.SecurityEpoch == 0 {
		return errors.New("runtime delegation: SecurityEpoch must be positive")
	}
	if c.IssuedAt <= 0 {
		return errors.New("runtime delegation: IssuedAt must be positive")
	}
	if c.ExpiresAt <= c.IssuedAt {
		return errors.New("runtime delegation: ExpiresAt must be after IssuedAt")
	}
	if c.ExpiresAt-c.IssuedAt > int64(MaxLifetime/time.Second) {
		return errors.New("runtime delegation: lifetime exceeds the v1 protocol maximum")
	}
	return nil
}

func validOpaque(value string, maxBytes int) bool {
	if value == "" || len(value) > maxBytes || !utf8.ValidString(value) || strings.TrimSpace(value) != value {
		return false
	}
	for _, r := range value {
		if unicode.IsControl(r) {
			return false
		}
	}
	return true
}

func validCapability(value string) bool {
	if value == "" || !utf8.ValidString(value) || strings.ToLower(value) != value {
		return false
	}
	parts := strings.Split(value, ".")
	if len(parts) < 2 {
		return false
	}
	for _, part := range parts {
		if part == "" || !lowerAlpha(part[0]) {
			return false
		}
		for i := 1; i < len(part); i++ {
			if !lowerAlpha(part[i]) && !asciiDigit(part[i]) && part[i] != '-' {
				return false
			}
		}
		if part[len(part)-1] == '-' {
			return false
		}
	}
	return true
}

func lowerAlpha(value byte) bool { return value >= 'a' && value <= 'z' }
func asciiDigit(value byte) bool { return value >= '0' && value <= '9' }
