package permissionauthoringv1

import (
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"errors"
	"strconv"
	"strings"
)

const authoringTokenPrefix = "pa:v1:"

type parsedAuthoringToken struct {
	revision     uint64
	revisionText string
	digest       []byte
}

// NewAuthoringToken returns the one compare token for a canonical
// PermissionSet identity and authoring revision. Opaque is an API-usage rule:
// callers must preserve rather than interpret the token. The token is not a
// credential, authorization capability, secret, or proof that a caller read a
// snapshot. Runtime authenticates and authorizes independently; this
// deterministic content binding prevents equal revisions on two different
// PermissionSets from becoming interchangeable after a Profile rebind.
func NewAuthoringToken(canonicalPermissionSetID string, revision uint64) (string, error) {
	if !validOpaque(canonicalPermissionSetID, MaxTargetIdentifierBytes) || revision == 0 {
		return "", errors.New("permission authoring token identity is invalid")
	}
	revisionText := strconv.FormatUint(revision, 10)
	digest := authoringTokenDigest(canonicalPermissionSetID, revisionText)
	return authoringTokenPrefix + revisionText + ":" + hex.EncodeToString(digest[:]), nil
}

// AuthoringTokenMatches reports whether token is the canonical binding for the
// supplied PermissionSet identity and revision. Comparison is constant-time so
// this helper remains safe if a future transport treats the token as sensitive.
func AuthoringTokenMatches(token, canonicalPermissionSetID string, revision uint64) bool {
	expected, err := NewAuthoringToken(canonicalPermissionSetID, revision)
	if err != nil || len(token) != len(expected) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
}

func validBoundAuthoringToken(token, canonicalPermissionSetID string) bool {
	_, ok := boundAuthoringTokenRevision(token, canonicalPermissionSetID)
	return ok
}

func boundAuthoringTokenRevision(token, canonicalPermissionSetID string) (uint64, bool) {
	parsed, ok := parseCanonicalAuthoringToken(token)
	if !ok {
		return 0, false
	}
	expected := authoringTokenDigest(canonicalPermissionSetID, parsed.revisionText)
	if subtle.ConstantTimeCompare(parsed.digest, expected[:]) != 1 {
		return 0, false
	}
	return parsed.revision, true
}

func validUnboundAuthoringToken(token string) bool {
	_, ok := parseCanonicalAuthoringToken(token)
	return ok
}

func parseCanonicalAuthoringToken(token string) (parsedAuthoringToken, bool) {
	if len(token) > MaxAuthoringTokenBytes || !strings.HasPrefix(token, authoringTokenPrefix) {
		return parsedAuthoringToken{}, false
	}
	remainder := strings.TrimPrefix(token, authoringTokenPrefix)
	revisionText, digestText, found := strings.Cut(remainder, ":")
	if !found || revisionText == "" || digestText == "" ||
		len(digestText) != sha256.Size*2 || revisionText[0] < '1' || revisionText[0] > '9' {
		return parsedAuthoringToken{}, false
	}
	for index := 1; index < len(revisionText); index++ {
		if revisionText[index] < '0' || revisionText[index] > '9' {
			return parsedAuthoringToken{}, false
		}
	}
	revision, err := strconv.ParseUint(revisionText, 10, 64)
	if err != nil || revision == 0 {
		return parsedAuthoringToken{}, false
	}
	digest, err := hex.DecodeString(digestText)
	if err != nil || digestText != strings.ToLower(digestText) {
		return parsedAuthoringToken{}, false
	}
	return parsedAuthoringToken{revision: revision, revisionText: revisionText, digest: digest}, true
}

func authoringTokenDigest(canonicalPermissionSetID, revisionText string) [sha256.Size]byte {
	return sha256.Sum256([]byte("permission-authoring-token-v1\x00" + canonicalPermissionSetID + "\x00" + revisionText))
}
