package native

import (
	"encoding/binary"
	"errors"
	"fmt"
	"math/big"

	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/dao"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop/runtime"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/storage"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/hash"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

// Vita represents a soul-bound identity native contract.
type Vita struct {
	interop.ContractMD
	Tutus        ITutus
	RoleRegistry IRoleRegistry
	Lex          ILex
	Annos        IAnnos
}

// VitaCache represents the cached state for Vita contract.
type VitaCache struct {
	tokenCount uint64
}

// Storage key prefixes for Vita.
const (
	prefixTokenByOwner    = 0x01 // owner (Uint160) -> tokenID (uint64)
	prefixTokenByID       = 0x02 // tokenID (uint64) -> Vita token
	prefixPersonHash      = 0x03 // personHash -> tokenID (uniqueness)
	prefixAttribute       = 0x04 // tokenID + attrKey -> Attribute
	prefixChallenge       = 0x05 // challengeID -> AuthChallenge
	prefixRecovery        = 0x06 // requestID -> RecoveryRequest
	prefixActiveRecovery  = 0x07 // tokenID + "recovery" -> requestID
	prefixPresence        = 0x08 // tokenID -> PresenceRecord (VPP)
	prefixPresenceConfig  = 0x09 // -> PresenceConfig
	prefixTokenCounter    = 0x10 // -> uint64
	prefixConfig          = 0x11 // -> VitaConfig
)

// Event names for Vita.
const (
	VitaCreatedEvent           = "VitaCreated"
	VitaSuspendedEvent         = "VitaSuspended"
	VitaReinstatedEvent        = "VitaReinstated"
	VitaRevokedEvent           = "VitaRevoked"
	AttributeSetEvent          = "AttributeSet"
	AttributeRevokedEvent      = "AttributeRevoked"
	AuthenticationSuccessEvent = "AuthenticationSuccess"
	AuthChallengeCreatedEvent  = "AuthChallengeCreated"
	RecoveryInitiatedEvent     = "RecoveryInitiated"
	RecoveryApprovalEvent      = "RecoveryApproval"
	RecoveryExecutedEvent      = "RecoveryExecuted"
	RecoveryCancelledEvent     = "RecoveryCancelled"
	VestingUpdatedEvent        = "VestingUpdated"
	PresenceRecordedEvent      = "PresenceRecorded"
	PresenceVisibilityEvent    = "PresenceVisibilityChanged"
)

// Default vesting period in blocks (approximately 30 days at 1 second per block).
// New Vita tokens must vest before having full rights (Sybil resistance).
const defaultVestingPeriod uint32 = 2592000 // 30 days * 24 hours * 60 minutes * 60 seconds

// Various errors.
var (
	ErrTokenAlreadyExists        = errors.New("token already exists for this owner")
	ErrPersonHashExists          = errors.New("person hash already linked to another token")
	ErrTokenNotFound             = errors.New("token not found")
	ErrTokenSuspended            = errors.New("token is suspended")
	ErrTokenRevoked              = errors.New("token is revoked")
	ErrTokenNotSuspended         = errors.New("token is not suspended")
	ErrInvalidOwner       = errors.New("invalid owner")
	ErrInvalidPersonHash  = errors.New("invalid person hash")
	ErrNotCommittee       = errors.New("invalid committee signature")
	ErrVitaInvalidWitness = errors.New("invalid witness")
	ErrVitaNotFullyVested = errors.New("vita is not fully vested")
	ErrAttributeNotFound         = errors.New("attribute not found")
	ErrAttributeRevoked          = errors.New("attribute is already revoked")
	ErrAttributeExpired          = errors.New("attribute has expired")
	ErrInvalidAttributeKey       = errors.New("invalid attribute key")
	ErrInvalidValueHash          = errors.New("invalid value hash")
	ErrInvalidDisclosureLevel    = errors.New("invalid disclosure level")
	ErrTokenNotActive            = errors.New("token is not active")
	ErrChallengeNotFound         = errors.New("challenge not found")
	ErrChallengeExpired          = errors.New("challenge has expired")
	ErrChallengeAlreadyFulfilled = errors.New("challenge already fulfilled")
	ErrInvalidSignature          = errors.New("invalid signature")
	ErrInvalidPurpose            = errors.New("invalid purpose")
	ErrNoRecentAuth              = errors.New("no recent authentication found")
	ErrRecoveryNotFound          = errors.New("recovery request not found")
	ErrRecoveryAlreadyActive     = errors.New("recovery already active for this token")
	ErrRecoveryNotPending        = errors.New("recovery is not in pending state")
	ErrRecoveryDelayNotPassed    = errors.New("recovery delay period has not passed")
	ErrRecoveryExpired           = errors.New("recovery request has expired")
	ErrAlreadyApproved           = errors.New("already approved this recovery")
	ErrInvalidNewOwner           = errors.New("invalid new owner")
	ErrTokenInRecovery           = errors.New("token is already in recovery")
	ErrNotRecoveryRequester      = errors.New("not the recovery requester")
	ErrCallerHasNoVita        = errors.New("caller does not have a Vita")
	ErrRoleNotAssigned        = errors.New("required role not assigned")
	ErrPermissionDenied       = errors.New("permission denied")
	ErrVitaInvalidRole        = errors.New("invalid core role")
	ErrVitaInvalidResource    = errors.New("invalid resource")
	ErrVitaInvalidAction      = errors.New("invalid action")
	// Presence (VPP) errors
	ErrPresenceRateLimited    = errors.New("heartbeat rate limit exceeded")
	ErrPresenceInvalidSession = errors.New("invalid session ID: must be 32 bytes")
	ErrPresenceInvalidDevice  = errors.New("invalid device hash: must be 32 bytes")
	ErrPresenceNotFound       = errors.New("no presence record found")
)

// CoreRole represents built-in roles for cross-contract access control.
type CoreRole uint8

// Core roles for Vita holders.
const (
	CoreRoleNone       CoreRole = 0
	CoreRoleUser       CoreRole = 1 // Basic authenticated user
	CoreRoleVerified   CoreRole = 2 // User with verified attributes
	CoreRoleCommittee  CoreRole = 3 // Committee member
	CoreRoleAttestor   CoreRole = 4 // Can attest attributes for others
	CoreRoleRecovery   CoreRole = 5 // Can participate in recovery
)

var (
	_ interop.Contract        = (*Vita)(nil)
	_ dao.NativeContractCache = (*VitaCache)(nil)
)

// Copy implements NativeContractCache interface.
func (c *VitaCache) Copy() dao.NativeContractCache {
	return &VitaCache{
		tokenCount: c.tokenCount,
	}
}

// checkCommittee checks if the caller has committee authority.
// Uses RoleRegistry if available, falls back to NEO.CheckCommittee().
func (v *Vita) checkCommittee(ic *interop.Context) bool {
	if v.RoleRegistry != nil {
		return v.RoleRegistry.CheckCommittee(ic)
	}
	// Fallback to Tutus for backwards compatibility
	return v.Tutus.CheckCommittee(ic)
}

// newVita creates a new Vita native contract.
func newVita() *Vita {
	v := &Vita{
		ContractMD: *interop.NewContractMD(nativenames.Vita, nativeids.Vita),
	}
	defer v.BuildHFSpecificMD(v.ActiveIn())

	// Register method
	desc := NewDescriptor("register", smartcontract.ByteArrayType,
		manifest.NewParameter("owner", smartcontract.Hash160Type),
		manifest.NewParameter("personHash", smartcontract.ByteArrayType),
		manifest.NewParameter("isEntity", smartcontract.BoolType),
		manifest.NewParameter("recoveryHash", smartcontract.ByteArrayType),
		manifest.NewParameter("birthTimestamp", smartcontract.IntegerType))
	md := NewMethodAndPrice(v.register, 1<<17, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// GetToken method
	desc = NewDescriptor("getToken", smartcontract.ArrayType,
		manifest.NewParameter("owner", smartcontract.Hash160Type))
	md = NewMethodAndPrice(v.getToken, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// GetTokenByID method
	desc = NewDescriptor("getTokenByID", smartcontract.ArrayType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType))
	md = NewMethodAndPrice(v.getTokenByID, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// Exists method
	desc = NewDescriptor("exists", smartcontract.BoolType,
		manifest.NewParameter("owner", smartcontract.Hash160Type))
	md = NewMethodAndPrice(v.exists, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// TotalSupply method
	desc = NewDescriptor("totalSupply", smartcontract.IntegerType)
	md = NewMethodAndPrice(v.totalSupply, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// Suspend method
	desc = NewDescriptor("suspend", smartcontract.BoolType,
		manifest.NewParameter("owner", smartcontract.Hash160Type),
		manifest.NewParameter("reason", smartcontract.StringType))
	md = NewMethodAndPrice(v.suspend, 1<<16, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// Reinstate method
	desc = NewDescriptor("reinstate", smartcontract.BoolType,
		manifest.NewParameter("owner", smartcontract.Hash160Type))
	md = NewMethodAndPrice(v.reinstate, 1<<16, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// Revoke method
	desc = NewDescriptor("revoke", smartcontract.BoolType,
		manifest.NewParameter("owner", smartcontract.Hash160Type),
		manifest.NewParameter("reason", smartcontract.StringType))
	md = NewMethodAndPrice(v.revoke, 1<<17, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// SetAttribute method
	desc = NewDescriptor("setAttribute", smartcontract.BoolType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("key", smartcontract.StringType),
		manifest.NewParameter("valueHash", smartcontract.ByteArrayType),
		manifest.NewParameter("valueEnc", smartcontract.ByteArrayType),
		manifest.NewParameter("expiresAt", smartcontract.IntegerType),
		manifest.NewParameter("disclosureLevel", smartcontract.IntegerType))
	md = NewMethodAndPrice(v.setAttribute, 1<<17, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// GetAttribute method
	desc = NewDescriptor("getAttribute", smartcontract.ArrayType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("key", smartcontract.StringType))
	md = NewMethodAndPrice(v.getAttribute, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// RevokeAttribute method
	desc = NewDescriptor("revokeAttribute", smartcontract.BoolType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("key", smartcontract.StringType),
		manifest.NewParameter("reason", smartcontract.StringType))
	md = NewMethodAndPrice(v.revokeAttribute, 1<<16, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// VerifyAttribute method
	desc = NewDescriptor("verifyAttribute", smartcontract.BoolType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("key", smartcontract.StringType),
		manifest.NewParameter("expectedHash", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(v.verifyAttribute, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// CreateChallenge method
	desc = NewDescriptor("createChallenge", smartcontract.ArrayType,
		manifest.NewParameter("owner", smartcontract.Hash160Type),
		manifest.NewParameter("purpose", smartcontract.StringType))
	md = NewMethodAndPrice(v.createChallenge, 1<<16, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// GetChallenge method
	desc = NewDescriptor("getChallenge", smartcontract.ArrayType,
		manifest.NewParameter("challengeId", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(v.getChallenge, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// FulfillChallenge method
	desc = NewDescriptor("fulfillChallenge", smartcontract.BoolType,
		manifest.NewParameter("challengeId", smartcontract.ByteArrayType),
		manifest.NewParameter("signature", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(v.fulfillChallenge, 1<<17, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// VerifyAuth method
	desc = NewDescriptor("verifyAuth", smartcontract.BoolType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("purpose", smartcontract.StringType),
		manifest.NewParameter("maxAge", smartcontract.IntegerType))
	md = NewMethodAndPrice(v.verifyAuth, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// InitiateRecovery method
	desc = NewDescriptor("initiateRecovery", smartcontract.ByteArrayType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("newOwner", smartcontract.Hash160Type),
		manifest.NewParameter("evidence", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(v.initiateRecovery, 1<<17, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// ApproveRecovery method
	desc = NewDescriptor("approveRecovery", smartcontract.BoolType,
		manifest.NewParameter("requestId", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(v.approveRecovery, 1<<16, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// ExecuteRecovery method
	desc = NewDescriptor("executeRecovery", smartcontract.BoolType,
		manifest.NewParameter("requestId", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(v.executeRecovery, 1<<17, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// CancelRecovery method
	desc = NewDescriptor("cancelRecovery", smartcontract.BoolType,
		manifest.NewParameter("requestId", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(v.cancelRecovery, 1<<16, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// GetRecoveryRequest method
	desc = NewDescriptor("getRecoveryRequest", smartcontract.ArrayType,
		manifest.NewParameter("requestId", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(v.getRecoveryRequest, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// ValidateCaller method
	desc = NewDescriptor("validateCaller", smartcontract.ArrayType)
	md = NewMethodAndPrice(v.validateCaller, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// RequireRole method
	desc = NewDescriptor("requireRole", smartcontract.IntegerType,
		manifest.NewParameter("roleId", smartcontract.IntegerType))
	md = NewMethodAndPrice(v.requireRole, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// RequireCoreRole method
	desc = NewDescriptor("requireCoreRole", smartcontract.IntegerType,
		manifest.NewParameter("coreRole", smartcontract.IntegerType))
	md = NewMethodAndPrice(v.requireCoreRole, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// RequirePermission method
	desc = NewDescriptor("requirePermission", smartcontract.IntegerType,
		manifest.NewParameter("resource", smartcontract.StringType),
		manifest.NewParameter("action", smartcontract.StringType),
		manifest.NewParameter("scope", smartcontract.StringType))
	md = NewMethodAndPrice(v.requirePermission, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// SetVesting method (committee only)
	desc = NewDescriptor("setVesting", smartcontract.BoolType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("vestedUntil", smartcontract.IntegerType))
	md = NewMethodAndPrice(v.setVesting, 1<<16, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// IsFullyVested method
	desc = NewDescriptor("isFullyVested", smartcontract.BoolType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType))
	md = NewMethodAndPrice(v.isFullyVested, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// GetVestingInfo method
	desc = NewDescriptor("getVestingInfo", smartcontract.ArrayType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType))
	md = NewMethodAndPrice(v.getVestingInfo, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// Events
	eDesc := NewEventDescriptor(VitaCreatedEvent,
		manifest.NewParameter("tokenId", smartcontract.ByteArrayType),
		manifest.NewParameter("owner", smartcontract.Hash160Type),
		manifest.NewParameter("createdAt", smartcontract.IntegerType))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(VitaSuspendedEvent,
		manifest.NewParameter("tokenId", smartcontract.ByteArrayType),
		manifest.NewParameter("reason", smartcontract.StringType),
		manifest.NewParameter("suspendedBy", smartcontract.Hash160Type))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(VitaReinstatedEvent,
		manifest.NewParameter("tokenId", smartcontract.ByteArrayType),
		manifest.NewParameter("reinstatedBy", smartcontract.Hash160Type))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(VitaRevokedEvent,
		manifest.NewParameter("tokenId", smartcontract.ByteArrayType),
		manifest.NewParameter("reason", smartcontract.StringType),
		manifest.NewParameter("revokedBy", smartcontract.Hash160Type))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(AttributeSetEvent,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("key", smartcontract.StringType),
		manifest.NewParameter("attestor", smartcontract.Hash160Type),
		manifest.NewParameter("expiresAt", smartcontract.IntegerType))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(AttributeRevokedEvent,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("key", smartcontract.StringType),
		manifest.NewParameter("revokedBy", smartcontract.Hash160Type),
		manifest.NewParameter("reason", smartcontract.StringType))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(AuthChallengeCreatedEvent,
		manifest.NewParameter("challengeId", smartcontract.ByteArrayType),
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("purpose", smartcontract.StringType),
		manifest.NewParameter("expiresAt", smartcontract.IntegerType))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(AuthenticationSuccessEvent,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("purpose", smartcontract.StringType),
		manifest.NewParameter("timestamp", smartcontract.IntegerType))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(RecoveryInitiatedEvent,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("requestId", smartcontract.ByteArrayType),
		manifest.NewParameter("delayUntil", smartcontract.IntegerType))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(RecoveryApprovalEvent,
		manifest.NewParameter("requestId", smartcontract.ByteArrayType),
		manifest.NewParameter("approver", smartcontract.Hash160Type),
		manifest.NewParameter("approvalCount", smartcontract.IntegerType),
		manifest.NewParameter("required", smartcontract.IntegerType))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(RecoveryExecutedEvent,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("oldOwner", smartcontract.Hash160Type),
		manifest.NewParameter("newOwner", smartcontract.Hash160Type))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(RecoveryCancelledEvent,
		manifest.NewParameter("requestId", smartcontract.ByteArrayType),
		manifest.NewParameter("cancelledBy", smartcontract.Hash160Type))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(VestingUpdatedEvent,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("vestedUntil", smartcontract.IntegerType),
		manifest.NewParameter("updatedBy", smartcontract.Hash160Type))
	v.AddEvent(NewEvent(eDesc))

	// VPP (Vita Presence Protocol) methods

	// RecordPresence method - record a heartbeat for VPP
	desc = NewDescriptor("recordPresence", smartcontract.BoolType,
		manifest.NewParameter("sessionId", smartcontract.ByteArrayType),
		manifest.NewParameter("deviceHash", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(v.recordPresence, 1<<15, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// GetPresence method - get presence record for a token
	desc = NewDescriptor("getPresence", smartcontract.ArrayType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType))
	md = NewMethodAndPrice(v.getPresence, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// GetPresenceStatus method - get computed presence status
	desc = NewDescriptor("getPresenceStatus", smartcontract.IntegerType,
		manifest.NewParameter("tokenId", smartcontract.IntegerType))
	md = NewMethodAndPrice(v.getPresenceStatus, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// SetPresenceVisibility method - set visibility (invisible mode)
	desc = NewDescriptor("setPresenceVisibility", smartcontract.BoolType,
		manifest.NewParameter("invisible", smartcontract.BoolType))
	md = NewMethodAndPrice(v.setPresenceVisibility, 1<<15, callflag.States|callflag.AllowNotify)
	v.AddMethod(md, desc)

	// GetPresenceConfig method - get VPP configuration
	desc = NewDescriptor("getPresenceConfig", smartcontract.ArrayType)
	md = NewMethodAndPrice(v.getPresenceConfig, 1<<15, callflag.ReadStates)
	v.AddMethod(md, desc)

	// VPP Events
	eDesc = NewEventDescriptor(PresenceRecordedEvent,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("blockHeight", smartcontract.IntegerType),
		manifest.NewParameter("sessionId", smartcontract.ByteArrayType))
	v.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(PresenceVisibilityEvent,
		manifest.NewParameter("tokenId", smartcontract.IntegerType),
		manifest.NewParameter("invisible", smartcontract.BoolType))
	v.AddEvent(NewEvent(eDesc))

	return v
}

// Metadata returns contract metadata.
func (v *Vita) Metadata() *interop.ContractMD {
	return &v.ContractMD
}

// Initialize initializes Vita contract at the specified hardfork.
func (v *Vita) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
	if hf != v.ActiveIn() {
		return nil
	}

	cache := &VitaCache{
		tokenCount: 0,
	}
	ic.DAO.SetCache(v.ID, cache)

	// Initialize token counter to 0
	v.setTokenCounter(ic.DAO, 0)

	return nil
}

// InitializeCache fills native Vita cache from DAO on node restart.
func (v *Vita) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
	cache := &VitaCache{}

	// Load token counter from storage
	counter := v.getTokenCounter(d)
	cache.tokenCount = counter

	d.SetCache(v.ID, cache)
	return nil
}

// OnPersist implements the Contract interface.
func (v *Vita) OnPersist(ic *interop.Context) error {
	return nil
}

// PostPersist implements the Contract interface.
func (v *Vita) PostPersist(ic *interop.Context) error {
	return nil
}

// ActiveIn returns the hardfork this contract activates in.
// Vita is always active (returns nil).
func (v *Vita) ActiveIn() *config.Hardfork {
	return nil
}

// Storage key helpers

func makeTokenByOwnerKey(owner util.Uint160) []byte {
	key := make([]byte, 1+util.Uint160Size)
	key[0] = prefixTokenByOwner
	copy(key[1:], owner.BytesBE())
	return key
}

func makeTokenByIDKey(tokenID uint64) []byte {
	key := make([]byte, 9)
	key[0] = prefixTokenByID
	binary.BigEndian.PutUint64(key[1:], tokenID)
	return key
}

func makePersonHashKey(personHash []byte) []byte {
	key := make([]byte, 1+len(personHash))
	key[0] = prefixPersonHash
	copy(key[1:], personHash)
	return key
}

func makeAttributeKey(tokenID uint64, attrKey string) []byte {
	keyBytes := []byte(attrKey)
	key := make([]byte, 9+len(keyBytes))
	key[0] = prefixAttribute
	binary.BigEndian.PutUint64(key[1:9], tokenID)
	copy(key[9:], keyBytes)
	return key
}

func makeChallengeKey(challengeID util.Uint256) []byte {
	key := make([]byte, 1+util.Uint256Size)
	key[0] = prefixChallenge
	copy(key[1:], challengeID.BytesBE())
	return key
}

func makeLastAuthKey(tokenID uint64, purpose string) []byte {
	purposeBytes := []byte(purpose)
	key := make([]byte, 9+len(purposeBytes))
	key[0] = prefixConfig // Using config prefix with tokenID+purpose for last auth tracking
	binary.BigEndian.PutUint64(key[1:9], tokenID)
	copy(key[9:], purposeBytes)
	return key
}

func makeRecoveryKey(requestID util.Uint256) []byte {
	key := make([]byte, 1+util.Uint256Size)
	key[0] = prefixRecovery
	copy(key[1:], requestID.BytesBE())
	return key
}

func makeActiveRecoveryKey(tokenID uint64) []byte {
	key := make([]byte, 9)
	key[0] = prefixActiveRecovery
	binary.BigEndian.PutUint64(key[1:], tokenID)
	return key
}

// Token counter methods

func (v *Vita) getTokenCounter(d *dao.Simple) uint64 {
	si := d.GetStorageItem(v.ID, []byte{prefixTokenCounter})
	if si == nil {
		return 0
	}
	return binary.BigEndian.Uint64(si)
}

func (v *Vita) setTokenCounter(d *dao.Simple, count uint64) {
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, count)
	d.PutStorageItem(v.ID, []byte{prefixTokenCounter}, buf)
}

func (v *Vita) getAndIncrementTokenCounter(d *dao.Simple) uint64 {
	cache := d.GetRWCache(v.ID).(*VitaCache)
	tokenID := cache.tokenCount
	cache.tokenCount++
	v.setTokenCounter(d, cache.tokenCount)
	return tokenID
}

// Token storage methods

func (v *Vita) putToken(d *dao.Simple, token *state.Vita) error {
	item, err := token.ToStackItem()
	if err != nil {
		return err
	}
	data, err := stackitem.Serialize(item)
	if err != nil {
		return err
	}
	// Store by ID
	d.PutStorageItem(v.ID, makeTokenByIDKey(token.TokenID), data)
	// Store owner -> tokenID mapping
	tokenIDBytes := make([]byte, 8)
	binary.BigEndian.PutUint64(tokenIDBytes, token.TokenID)
	d.PutStorageItem(v.ID, makeTokenByOwnerKey(token.Owner), tokenIDBytes)
	// Store personHash -> tokenID mapping for uniqueness
	if len(token.PersonHash) > 0 {
		d.PutStorageItem(v.ID, makePersonHashKey(token.PersonHash), tokenIDBytes)
	}
	return nil
}

func (v *Vita) getTokenByOwnerInternal(d *dao.Simple, owner util.Uint160) (*state.Vita, error) {
	si := d.GetStorageItem(v.ID, makeTokenByOwnerKey(owner))
	if si == nil {
		return nil, nil
	}
	tokenID := binary.BigEndian.Uint64(si)
	return v.getTokenByIDInternal(d, tokenID)
}

func (v *Vita) getTokenByIDInternal(d *dao.Simple, tokenID uint64) (*state.Vita, error) {
	si := d.GetStorageItem(v.ID, makeTokenByIDKey(tokenID))
	if si == nil {
		return nil, nil
	}
	item, err := stackitem.Deserialize(si)
	if err != nil {
		return nil, err
	}
	token := new(state.Vita)
	if err := token.FromStackItem(item); err != nil {
		return nil, err
	}
	return token, nil
}

func (v *Vita) tokenExistsForOwner(d *dao.Simple, owner util.Uint160) bool {
	si := d.GetStorageItem(v.ID, makeTokenByOwnerKey(owner))
	return si != nil
}

func (v *Vita) personHashExists(d *dao.Simple, personHash []byte) bool {
	if len(personHash) == 0 {
		return false
	}
	si := d.GetStorageItem(v.ID, makePersonHashKey(personHash))
	return si != nil
}

// Contract methods

// register creates a new Vita.
func (v *Vita) register(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])
	personHash := toBytes(args[1])
	isEntity, err := args[2].TryBool()
	if err != nil {
		panic(fmt.Errorf("invalid isEntity: %w", err))
	}
	recoveryHash := toBytes(args[3])
	birthTimestamp := toUint64(args[4])

	// Validate owner
	if owner.Equals(util.Uint160{}) {
		panic(ErrInvalidOwner)
	}

	// Check witness for the owner
	ok, err := runtime.CheckHashedWitness(ic, owner)
	if err != nil || !ok {
		panic(ErrVitaInvalidWitness)
	}

	// Check if token already exists for this owner
	if v.tokenExistsForOwner(ic.DAO, owner) {
		panic(ErrTokenAlreadyExists)
	}

	// Check if person hash is already linked to another token
	if v.personHashExists(ic.DAO, personHash) {
		panic(ErrPersonHashExists)
	}

	// Get next token ID
	tokenID := v.getAndIncrementTokenCounter(ic.DAO)

	// Create token with vesting period for Sybil resistance
	// New tokens must vest before having full rights
	vestedUntil := ic.Block.Index + defaultVestingPeriod

	token := &state.Vita{
		TokenID:      tokenID,
		Owner:        owner,
		PersonHash:   personHash,
		IsEntity:     isEntity,
		CreatedAt:    ic.Block.Index,
		UpdatedAt:    ic.Block.Index,
		Status:       state.TokenStatusActive,
		StatusReason: "",
		RecoveryHash: recoveryHash,
		VestedUntil:  vestedUntil,
	}

	// Store token
	if err := v.putToken(ic.DAO, token); err != nil {
		panic(err)
	}

	// Register birth in Annos contract for lifespan tracking
	// birthTimestamp is the actual birth date (Unix timestamp in seconds)
	// This allows existing adults to register with their real birth date
	// For newborns being registered at birth, use current block timestamp
	if v.Annos != nil {
		if err := v.Annos.RegisterBirthInternal(ic.DAO, ic, tokenID, owner, birthTimestamp); err != nil {
			panic(fmt.Errorf("failed to register birth in Annos: %w", err))
		}
	}

	// Generate token ID bytes for return and event
	tokenIDBytes := hash.Sha256(append(owner.BytesBE(), personHash...)).BytesBE()

	// Emit event
	err = ic.AddNotification(v.Hash, VitaCreatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(tokenIDBytes),
		stackitem.NewByteArray(owner.BytesBE()),
		stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewByteArray(tokenIDBytes)
}

// getToken returns the token for the given owner.
func (v *Vita) getToken(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])

	token, err := v.getTokenByOwnerInternal(ic.DAO, owner)
	if err != nil {
		panic(err)
	}
	if token == nil {
		return stackitem.Null{}
	}

	item, err := token.ToStackItem()
	if err != nil {
		panic(err)
	}
	return item
}

// getTokenByID returns the token for the given token ID.
func (v *Vita) getTokenByID(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID := toBigInt(args[0]).Uint64()

	token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
	if err != nil {
		panic(err)
	}
	if token == nil {
		return stackitem.Null{}
	}

	item, err := token.ToStackItem()
	if err != nil {
		panic(err)
	}
	return item
}

// exists checks if a token exists for the given owner.
func (v *Vita) exists(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])
	return stackitem.NewBool(v.tokenExistsForOwner(ic.DAO, owner))
}

// totalSupply returns the total number of tokens.
func (v *Vita) totalSupply(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	cache := ic.DAO.GetROCache(v.ID).(*VitaCache)
	return stackitem.NewBigInteger(big.NewInt(int64(cache.tokenCount)))
}

// suspend temporarily suspends a token (committee only).
func (v *Vita) suspend(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])
	reason := toString(args[1])

	// Check committee
	if !v.checkCommittee(ic) {
		panic(ErrNotCommittee)
	}

	// Require liberty restriction order from Lex (due process protection)
	// Suspending a Vita is a restriction of liberty, which requires judicial authority
	if v.Lex != nil && !v.Lex.IsRestrictedInternal(ic.DAO, owner, state.RightLiberty, ic.Block.Index) {
		panic("liberty restriction order required via Lex (due process)")
	}

	// Get token
	token, err := v.getTokenByOwnerInternal(ic.DAO, owner)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Check if already revoked
	if token.Status == state.TokenStatusRevoked {
		panic(ErrTokenRevoked)
	}

	// Update status
	token.Status = state.TokenStatusSuspended
	token.StatusReason = reason
	token.UpdatedAt = ic.Block.Index

	// Store updated token
	if err := v.putToken(ic.DAO, token); err != nil {
		panic(err)
	}

	// Get caller for event
	caller := ic.VM.GetCallingScriptHash()

	// Generate token ID bytes
	tokenIDBytes := make([]byte, 8)
	binary.BigEndian.PutUint64(tokenIDBytes, token.TokenID)

	// Emit event
	err = ic.AddNotification(v.Hash, VitaSuspendedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(tokenIDBytes),
		stackitem.NewByteArray([]byte(reason)),
		stackitem.NewByteArray(caller.BytesBE()),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewBool(true)
}

// reinstate reinstates a suspended token (committee only).
func (v *Vita) reinstate(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])

	// Check committee
	if !v.checkCommittee(ic) {
		panic(ErrNotCommittee)
	}

	// Get token
	token, err := v.getTokenByOwnerInternal(ic.DAO, owner)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Check if suspended
	if token.Status != state.TokenStatusSuspended {
		panic(ErrTokenNotSuspended)
	}

	// Update status
	token.Status = state.TokenStatusActive
	token.StatusReason = ""
	token.UpdatedAt = ic.Block.Index

	// Store updated token
	if err := v.putToken(ic.DAO, token); err != nil {
		panic(err)
	}

	// Get caller for event
	caller := ic.VM.GetCallingScriptHash()

	// Generate token ID bytes
	tokenIDBytes := make([]byte, 8)
	binary.BigEndian.PutUint64(tokenIDBytes, token.TokenID)

	// Emit event
	err = ic.AddNotification(v.Hash, VitaReinstatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(tokenIDBytes),
		stackitem.NewByteArray(caller.BytesBE()),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewBool(true)
}

// revoke permanently revokes a token (committee only).
func (v *Vita) revoke(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])
	reason := toString(args[1])

	// Check committee
	if !v.checkCommittee(ic) {
		panic(ErrNotCommittee)
	}

	// Require liberty restriction order from Lex (due process protection)
	// Revoking a Vita is a permanent restriction of liberty, requiring judicial authority
	if v.Lex != nil && !v.Lex.IsRestrictedInternal(ic.DAO, owner, state.RightLiberty, ic.Block.Index) {
		panic("liberty restriction order required via Lex (due process)")
	}

	// Get token
	token, err := v.getTokenByOwnerInternal(ic.DAO, owner)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Check if already revoked
	if token.Status == state.TokenStatusRevoked {
		panic(ErrTokenRevoked)
	}

	// Update status
	token.Status = state.TokenStatusRevoked
	token.StatusReason = reason
	token.UpdatedAt = ic.Block.Index

	// Store updated token
	if err := v.putToken(ic.DAO, token); err != nil {
		panic(err)
	}

	// Get caller for event
	caller := ic.VM.GetCallingScriptHash()

	// Generate token ID bytes
	tokenIDBytes := make([]byte, 8)
	binary.BigEndian.PutUint64(tokenIDBytes, token.TokenID)

	// Emit event
	err = ic.AddNotification(v.Hash, VitaRevokedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(tokenIDBytes),
		stackitem.NewByteArray([]byte(reason)),
		stackitem.NewByteArray(caller.BytesBE()),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewBool(true)
}

// Public methods for cross-native access

// GetTokenByOwner returns the token for the given owner (for cross-native access).
func (v *Vita) GetTokenByOwner(d *dao.Simple, owner util.Uint160) (*state.Vita, error) {
	return v.getTokenByOwnerInternal(d, owner)
}

// GetTokenByIDPublic returns the token for the given ID (for cross-native access).
func (v *Vita) GetTokenByIDPublic(d *dao.Simple, tokenID uint64) (*state.Vita, error) {
	return v.getTokenByIDInternal(d, tokenID)
}

// TokenExists returns true if a token exists for the given owner.
func (v *Vita) TokenExists(d *dao.Simple, owner util.Uint160) bool {
	return v.tokenExistsForOwner(d, owner)
}

// GetTotalTokenCount returns the total number of tokens issued (for quorum calculations).
func (v *Vita) GetTotalTokenCount(d *dao.Simple) uint64 {
	return v.getTokenCounter(d)
}

// ExistsInternal checks if a Vita token with the given ID exists.
func (v *Vita) ExistsInternal(d *dao.Simple, vitaID uint64) bool {
	token, err := v.getTokenByIDInternal(d, vitaID)
	return err == nil && token != nil
}

// OwnerOfInternal returns the owner of a Vita token by ID.
// Returns empty Uint160 if token doesn't exist.
func (v *Vita) OwnerOfInternal(d *dao.Simple, vitaID uint64) util.Uint160 {
	token, err := v.getTokenByIDInternal(d, vitaID)
	if err != nil || token == nil {
		return util.Uint160{}
	}
	return token.Owner
}

// IsAdultVerified checks if the owner has a verified "age_verified" attribute
// indicating they are 18+ years old. Used for age-restricted purchases.
// The attribute must be non-revoked and not expired.
func (v *Vita) IsAdultVerified(d *dao.Simple, owner util.Uint160) bool {
	token, err := v.getTokenByOwnerInternal(d, owner)
	if err != nil || token == nil {
		return false
	}

	// Check for "age_verified" attribute
	attr, err := v.getAttributeInternal(d, token.TokenID, "age_verified")
	if err != nil || attr == nil {
		return false
	}

	// Check attribute is not revoked
	if attr.Revoked {
		return false
	}

	// Note: Expiration check would require current block height
	// For now, we check if ExpiresAt is set and > 0 means it could expire
	// In production, pass block height and compare
	return true
}

// Attribute storage methods

func (v *Vita) putAttribute(d *dao.Simple, tokenID uint64, attr *state.Attribute) error {
	item, err := attr.ToStackItem()
	if err != nil {
		return err
	}
	data, err := stackitem.Serialize(item)
	if err != nil {
		return err
	}
	d.PutStorageItem(v.ID, makeAttributeKey(tokenID, attr.Key), data)
	return nil
}

func (v *Vita) getAttributeInternal(d *dao.Simple, tokenID uint64, key string) (*state.Attribute, error) {
	si := d.GetStorageItem(v.ID, makeAttributeKey(tokenID, key))
	if si == nil {
		return nil, nil
	}
	item, err := stackitem.Deserialize(si)
	if err != nil {
		return nil, err
	}
	attr := new(state.Attribute)
	if err := attr.FromStackItem(item); err != nil {
		return nil, err
	}
	return attr, nil
}

// setAttribute sets or updates an attribute on a token.
// The caller must be either the token owner (self-attestation) or any other account (third-party attestation).
func (v *Vita) setAttribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID := toBigInt(args[0]).Uint64()
	key := toString(args[1])
	valueHash := toBytes(args[2])
	valueEnc := toBytes(args[3])
	expiresAt := uint32(toBigInt(args[4]).Int64())
	disclosureLevel := state.DisclosureLevel(toBigInt(args[5]).Int64())

	// Validate inputs
	if len(key) == 0 || len(key) > 64 {
		panic(ErrInvalidAttributeKey)
	}
	if len(valueHash) == 0 || len(valueHash) > 64 {
		panic(ErrInvalidValueHash)
	}
	if disclosureLevel > state.DisclosurePublic {
		panic(ErrInvalidDisclosureLevel)
	}

	// Get token
	token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Check token is active
	if token.Status != state.TokenStatusActive {
		panic(ErrTokenNotActive)
	}

	// Get the attestor (caller)
	attestor := ic.VM.GetCallingScriptHash()

	// If attestor is the owner, check witness
	// Otherwise, any contract/account can attest (third-party attestation)
	if attestor.Equals(token.Owner) {
		ok, err := runtime.CheckHashedWitness(ic, token.Owner)
		if err != nil || !ok {
			panic(ErrVitaInvalidWitness)
		}
	}

	// Create attribute
	attr := &state.Attribute{
		Key:             key,
		ValueHash:       valueHash,
		ValueEnc:        valueEnc,
		Attestor:        attestor,
		AttestedAt:      ic.Block.Index,
		ExpiresAt:       expiresAt,
		Revoked:         false,
		RevokedAt:       0,
		DisclosureLevel: disclosureLevel,
	}

	// Store attribute
	if err := v.putAttribute(ic.DAO, tokenID, attr); err != nil {
		panic(err)
	}

	// Emit event
	err = ic.AddNotification(v.Hash, AttributeSetEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(tokenID))),
		stackitem.NewByteArray([]byte(key)),
		stackitem.NewByteArray(attestor.BytesBE()),
		stackitem.NewBigInteger(big.NewInt(int64(expiresAt))),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewBool(true)
}

// getAttribute returns an attribute for a token.
func (v *Vita) getAttribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID := toBigInt(args[0]).Uint64()
	key := toString(args[1])

	attr, err := v.getAttributeInternal(ic.DAO, tokenID, key)
	if err != nil {
		panic(err)
	}
	if attr == nil {
		return stackitem.Null{}
	}

	item, err := attr.ToStackItem()
	if err != nil {
		panic(err)
	}
	return item
}

// revokeAttribute revokes an attribute.
// Can be called by the token owner, the original attestor, or committee.
func (v *Vita) revokeAttribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID := toBigInt(args[0]).Uint64()
	key := toString(args[1])
	reason := toString(args[2])

	// Get token
	token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Get attribute
	attr, err := v.getAttributeInternal(ic.DAO, tokenID, key)
	if err != nil {
		panic(err)
	}
	if attr == nil {
		panic(ErrAttributeNotFound)
	}

	// Check if already revoked
	if attr.Revoked {
		panic(ErrAttributeRevoked)
	}

	// Check authorization: owner, attestor, or committee
	caller := ic.VM.GetCallingScriptHash()
	isOwner := caller.Equals(token.Owner)
	isAttestor := caller.Equals(attr.Attestor)
	isCommittee := v.checkCommittee(ic)

	if isOwner {
		ok, err := runtime.CheckHashedWitness(ic, token.Owner)
		if err != nil || !ok {
			panic(ErrVitaInvalidWitness)
		}
	} else if !isAttestor && !isCommittee {
		panic(ErrVitaInvalidWitness)
	}

	// Revoke attribute
	attr.Revoked = true
	attr.RevokedAt = ic.Block.Index

	// Store updated attribute
	if err := v.putAttribute(ic.DAO, tokenID, attr); err != nil {
		panic(err)
	}

	// Emit event
	err = ic.AddNotification(v.Hash, AttributeRevokedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(tokenID))),
		stackitem.NewByteArray([]byte(key)),
		stackitem.NewByteArray(caller.BytesBE()),
		stackitem.NewByteArray([]byte(reason)),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewBool(true)
}

// verifyAttribute verifies that an attribute exists and its hash matches.
// Returns true if the attribute exists, is not revoked, not expired, and hash matches.
func (v *Vita) verifyAttribute(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID := toBigInt(args[0]).Uint64()
	key := toString(args[1])
	expectedHash := toBytes(args[2])

	// Get attribute
	attr, err := v.getAttributeInternal(ic.DAO, tokenID, key)
	if err != nil {
		panic(err)
	}
	if attr == nil {
		return stackitem.NewBool(false)
	}

	// Check if revoked
	if attr.Revoked {
		return stackitem.NewBool(false)
	}

	// Check if expired (0 means never expires)
	if attr.ExpiresAt > 0 && ic.Block.Index > attr.ExpiresAt {
		return stackitem.NewBool(false)
	}

	// Compare hashes
	if len(attr.ValueHash) != len(expectedHash) {
		return stackitem.NewBool(false)
	}
	for i := range attr.ValueHash {
		if attr.ValueHash[i] != expectedHash[i] {
			return stackitem.NewBool(false)
		}
	}

	return stackitem.NewBool(true)
}

// Public methods for cross-native attribute access

// GetAttribute returns an attribute for the given token and key (for cross-native access).
func (v *Vita) GetAttribute(d *dao.Simple, tokenID uint64, key string) (*state.Attribute, error) {
	return v.getAttributeInternal(d, tokenID, key)
}

// Challenge storage methods

func (v *Vita) putChallenge(d *dao.Simple, challenge *state.AuthChallenge) error {
	item, err := challenge.ToStackItem()
	if err != nil {
		return err
	}
	data, err := stackitem.Serialize(item)
	if err != nil {
		return err
	}
	d.PutStorageItem(v.ID, makeChallengeKey(challenge.ChallengeID), data)
	return nil
}

func (v *Vita) getChallengeInternal(d *dao.Simple, challengeID util.Uint256) (*state.AuthChallenge, error) {
	si := d.GetStorageItem(v.ID, makeChallengeKey(challengeID))
	if si == nil {
		return nil, nil
	}
	item, err := stackitem.Deserialize(si)
	if err != nil {
		return nil, err
	}
	challenge := new(state.AuthChallenge)
	if err := challenge.FromStackItem(item); err != nil {
		return nil, err
	}
	return challenge, nil
}

func (v *Vita) setLastAuth(d *dao.Simple, tokenID uint64, purpose string, blockHeight uint32) {
	buf := make([]byte, 4)
	binary.BigEndian.PutUint32(buf, blockHeight)
	d.PutStorageItem(v.ID, makeLastAuthKey(tokenID, purpose), buf)
}

func (v *Vita) getLastAuth(d *dao.Simple, tokenID uint64, purpose string) uint32 {
	si := d.GetStorageItem(v.ID, makeLastAuthKey(tokenID, purpose))
	if si == nil {
		return 0
	}
	return binary.BigEndian.Uint32(si)
}

// Default challenge expiry in blocks (approximately 5 minutes at 15 seconds per block)
const defaultChallengeExpiry uint32 = 20

// createChallenge creates a new authentication challenge for a token owner.
// Anyone can create a challenge for any token owner.
func (v *Vita) createChallenge(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])
	purpose := toString(args[1])

	// Validate purpose
	if len(purpose) == 0 || len(purpose) > 32 {
		panic(ErrInvalidPurpose)
	}

	// Get token
	token, err := v.getTokenByOwnerInternal(ic.DAO, owner)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Check token is active
	if token.Status != state.TokenStatusActive {
		panic(ErrTokenNotActive)
	}

	// Generate nonce (use transaction hash + block index + token ID for uniqueness)
	nonceData := append(ic.Tx.Hash().BytesBE(), make([]byte, 8)...)
	binary.BigEndian.PutUint64(nonceData[util.Uint256Size:], token.TokenID)
	nonce := hash.Sha256(nonceData).BytesBE()

	// Generate challenge ID
	challengeIDData := append(nonce, owner.BytesBE()...)
	challengeIDData = append(challengeIDData, []byte(purpose)...)
	challengeID := hash.Sha256(challengeIDData)

	// Create challenge
	challenge := &state.AuthChallenge{
		ChallengeID: challengeID,
		TokenID:     token.TokenID,
		Nonce:       nonce,
		CreatedAt:   ic.Block.Index,
		ExpiresAt:   ic.Block.Index + defaultChallengeExpiry,
		Purpose:     purpose,
		Fulfilled:   false,
		FulfilledAt: 0,
	}

	// Store challenge
	if err := v.putChallenge(ic.DAO, challenge); err != nil {
		panic(err)
	}

	// Emit event
	err = ic.AddNotification(v.Hash, AuthChallengeCreatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(challengeID.BytesBE()),
		stackitem.NewBigInteger(big.NewInt(int64(token.TokenID))),
		stackitem.NewByteArray([]byte(purpose)),
		stackitem.NewBigInteger(big.NewInt(int64(challenge.ExpiresAt))),
	}))
	if err != nil {
		panic(err)
	}

	// Return challenge as struct
	item, err := challenge.ToStackItem()
	if err != nil {
		panic(err)
	}
	return item
}

// getChallenge returns a challenge by ID.
func (v *Vita) getChallenge(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	challengeIDBytes := toBytes(args[0])
	if len(challengeIDBytes) != util.Uint256Size {
		panic("invalid challenge ID length")
	}
	challengeID, err := util.Uint256DecodeBytesBE(challengeIDBytes)
	if err != nil {
		panic(err)
	}

	challenge, err := v.getChallengeInternal(ic.DAO, challengeID)
	if err != nil {
		panic(err)
	}
	if challenge == nil {
		return stackitem.Null{}
	}

	item, err := challenge.ToStackItem()
	if err != nil {
		panic(err)
	}
	return item
}

// fulfillChallenge fulfills an authentication challenge by providing a valid signature.
// The signature must be from the token owner over the challenge nonce.
func (v *Vita) fulfillChallenge(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	challengeIDBytes := toBytes(args[0])
	if len(challengeIDBytes) != util.Uint256Size {
		panic("invalid challenge ID length")
	}
	challengeID, err := util.Uint256DecodeBytesBE(challengeIDBytes)
	if err != nil {
		panic(err)
	}
	_ = toBytes(args[1]) // signature - validation happens via witness

	// Get challenge
	challenge, err := v.getChallengeInternal(ic.DAO, challengeID)
	if err != nil {
		panic(err)
	}
	if challenge == nil {
		panic(ErrChallengeNotFound)
	}

	// Check if already fulfilled
	if challenge.Fulfilled {
		panic(ErrChallengeAlreadyFulfilled)
	}

	// Check if expired
	if ic.Block.Index > challenge.ExpiresAt {
		panic(ErrChallengeExpired)
	}

	// Get token
	token, err := v.getTokenByIDInternal(ic.DAO, challenge.TokenID)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Verify the caller has witness for the token owner
	// This ensures the owner is actually signing this transaction
	ok, err := runtime.CheckHashedWitness(ic, token.Owner)
	if err != nil || !ok {
		panic(ErrInvalidSignature)
	}

	// Mark challenge as fulfilled
	challenge.Fulfilled = true
	challenge.FulfilledAt = ic.Block.Index

	// Store updated challenge
	if err := v.putChallenge(ic.DAO, challenge); err != nil {
		panic(err)
	}

	// Record last auth time for this token and purpose
	v.setLastAuth(ic.DAO, challenge.TokenID, challenge.Purpose, ic.Block.Index)

	// Emit success event
	err = ic.AddNotification(v.Hash, AuthenticationSuccessEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(challenge.TokenID))),
		stackitem.NewByteArray([]byte(challenge.Purpose)),
		stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewBool(true)
}

// verifyAuth checks if a token has authenticated recently for a given purpose.
// Returns true if the last successful authentication for the purpose was within maxAge blocks.
func (v *Vita) verifyAuth(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID := toBigInt(args[0]).Uint64()
	purpose := toString(args[1])
	maxAge := uint32(toBigInt(args[2]).Int64())

	// Get last auth time
	lastAuth := v.getLastAuth(ic.DAO, tokenID, purpose)
	if lastAuth == 0 {
		return stackitem.NewBool(false)
	}

	// Check if within maxAge
	if ic.Block.Index > lastAuth+maxAge {
		return stackitem.NewBool(false)
	}

	return stackitem.NewBool(true)
}

// Public methods for cross-native authentication access

// GetLastAuth returns the last authentication block height for a token and purpose.
func (v *Vita) GetLastAuth(d *dao.Simple, tokenID uint64, purpose string) uint32 {
	return v.getLastAuth(d, tokenID, purpose)
}

// Recovery configuration constants
const (
	// defaultRecoveryDelay is the number of blocks before recovery can be executed (approximately 24 hours at 15s blocks)
	defaultRecoveryDelay uint32 = 5760
	// defaultRecoveryExpiry is the number of blocks until a recovery request expires (approximately 7 days)
	defaultRecoveryExpiry uint32 = 40320
	// defaultRequiredApprovals is the number of committee approvals needed for recovery
	defaultRequiredApprovals int = 2
)

// Recovery storage methods

func (v *Vita) putRecoveryRequest(d *dao.Simple, req *state.RecoveryRequest) error {
	item, err := req.ToStackItem()
	if err != nil {
		return err
	}
	data, err := stackitem.Serialize(item)
	if err != nil {
		return err
	}
	d.PutStorageItem(v.ID, makeRecoveryKey(req.RequestID), data)
	return nil
}

func (v *Vita) getRecoveryRequestInternal(d *dao.Simple, requestID util.Uint256) (*state.RecoveryRequest, error) {
	si := d.GetStorageItem(v.ID, makeRecoveryKey(requestID))
	if si == nil {
		return nil, nil
	}
	item, err := stackitem.Deserialize(si)
	if err != nil {
		return nil, err
	}
	req := new(state.RecoveryRequest)
	if err := req.FromStackItem(item); err != nil {
		return nil, err
	}
	return req, nil
}

func (v *Vita) setActiveRecovery(d *dao.Simple, tokenID uint64, requestID util.Uint256) {
	d.PutStorageItem(v.ID, makeActiveRecoveryKey(tokenID), requestID.BytesBE())
}

func (v *Vita) getActiveRecovery(d *dao.Simple, tokenID uint64) *util.Uint256 {
	si := d.GetStorageItem(v.ID, makeActiveRecoveryKey(tokenID))
	if si == nil {
		return nil
	}
	requestID, err := util.Uint256DecodeBytesBE(si)
	if err != nil {
		return nil
	}
	return &requestID
}

func (v *Vita) clearActiveRecovery(d *dao.Simple, tokenID uint64) {
	d.DeleteStorageItem(v.ID, makeActiveRecoveryKey(tokenID))
}

// initiateRecovery starts a key recovery process for a token.
// Anyone can initiate recovery for any token.
func (v *Vita) initiateRecovery(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID := toBigInt(args[0]).Uint64()
	newOwner := toUint160(args[1])
	evidence := toBytes(args[2])

	// Validate new owner
	if newOwner.Equals(util.Uint160{}) {
		panic(ErrInvalidNewOwner)
	}

	// Get token
	token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Check token is not revoked
	if token.Status == state.TokenStatusRevoked {
		panic(ErrTokenRevoked)
	}

	// Check token is not already in recovery
	if token.Status == state.TokenStatusRecovering {
		panic(ErrTokenInRecovery)
	}

	// Check no active recovery exists
	if v.getActiveRecovery(ic.DAO, tokenID) != nil {
		panic(ErrRecoveryAlreadyActive)
	}

	// Get requester (caller)
	requester := ic.VM.GetCallingScriptHash()

	// Generate request ID
	requestIDData := append(ic.Tx.Hash().BytesBE(), make([]byte, 8)...)
	binary.BigEndian.PutUint64(requestIDData[util.Uint256Size:], tokenID)
	requestIDData = append(requestIDData, newOwner.BytesBE()...)
	requestID := hash.Sha256(requestIDData)

	// Create recovery request
	req := &state.RecoveryRequest{
		RequestID:         requestID,
		TokenID:           tokenID,
		NewOwner:          newOwner,
		Requester:         requester,
		Evidence:          evidence,
		Approvals:         []util.Uint160{},
		RequiredApprovals: defaultRequiredApprovals,
		CreatedAt:         ic.Block.Index,
		DelayUntil:        ic.Block.Index + defaultRecoveryDelay,
		ExpiresAt:         ic.Block.Index + defaultRecoveryExpiry,
		Status:            state.RecoveryStatusPending,
	}

	// Store recovery request
	if err := v.putRecoveryRequest(ic.DAO, req); err != nil {
		panic(err)
	}

	// Mark active recovery for token
	v.setActiveRecovery(ic.DAO, tokenID, requestID)

	// Update token status to recovering
	token.Status = state.TokenStatusRecovering
	token.StatusReason = "recovery initiated"
	token.UpdatedAt = ic.Block.Index
	if err := v.putToken(ic.DAO, token); err != nil {
		panic(err)
	}

	// Emit event
	err = ic.AddNotification(v.Hash, RecoveryInitiatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(tokenID))),
		stackitem.NewByteArray(requestID.BytesBE()),
		stackitem.NewBigInteger(big.NewInt(int64(req.DelayUntil))),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewByteArray(requestID.BytesBE())
}

// approveRecovery approves a recovery request (committee member only).
func (v *Vita) approveRecovery(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	requestIDBytes := toBytes(args[0])
	if len(requestIDBytes) != util.Uint256Size {
		panic("invalid request ID length")
	}
	requestID, err := util.Uint256DecodeBytesBE(requestIDBytes)
	if err != nil {
		panic(err)
	}

	// Check committee
	if !v.checkCommittee(ic) {
		panic(ErrNotCommittee)
	}

	// Get recovery request
	req, err := v.getRecoveryRequestInternal(ic.DAO, requestID)
	if err != nil {
		panic(err)
	}
	if req == nil {
		panic(ErrRecoveryNotFound)
	}

	// Check status is pending
	if req.Status != state.RecoveryStatusPending {
		panic(ErrRecoveryNotPending)
	}

	// Check not expired
	if ic.Block.Index > req.ExpiresAt {
		panic(ErrRecoveryExpired)
	}

	// Get approver (caller)
	approver := ic.VM.GetCallingScriptHash()

	// Check not already approved by this approver
	for _, a := range req.Approvals {
		if a.Equals(approver) {
			panic(ErrAlreadyApproved)
		}
	}

	// Add approval
	req.Approvals = append(req.Approvals, approver)

	// Check if sufficient approvals
	if len(req.Approvals) >= req.RequiredApprovals {
		req.Status = state.RecoveryStatusApproved
	}

	// Store updated request
	if err := v.putRecoveryRequest(ic.DAO, req); err != nil {
		panic(err)
	}

	// Emit event
	err = ic.AddNotification(v.Hash, RecoveryApprovalEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(requestID.BytesBE()),
		stackitem.NewByteArray(approver.BytesBE()),
		stackitem.NewBigInteger(big.NewInt(int64(len(req.Approvals)))),
		stackitem.NewBigInteger(big.NewInt(int64(req.RequiredApprovals))),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewBool(true)
}

// executeRecovery executes an approved recovery request after the delay period.
func (v *Vita) executeRecovery(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	requestIDBytes := toBytes(args[0])
	if len(requestIDBytes) != util.Uint256Size {
		panic("invalid request ID length")
	}
	requestID, err := util.Uint256DecodeBytesBE(requestIDBytes)
	if err != nil {
		panic(err)
	}

	// Get recovery request
	req, err := v.getRecoveryRequestInternal(ic.DAO, requestID)
	if err != nil {
		panic(err)
	}
	if req == nil {
		panic(ErrRecoveryNotFound)
	}

	// Check status is approved
	if req.Status != state.RecoveryStatusApproved {
		panic(ErrRecoveryNotPending)
	}

	// Check delay period has passed
	if ic.Block.Index < req.DelayUntil {
		panic(ErrRecoveryDelayNotPassed)
	}

	// Check not expired
	if ic.Block.Index > req.ExpiresAt {
		panic(ErrRecoveryExpired)
	}

	// Get token
	token, err := v.getTokenByIDInternal(ic.DAO, req.TokenID)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Store old owner for event
	oldOwner := token.Owner

	// Delete old owner -> tokenID mapping
	ic.DAO.DeleteStorageItem(v.ID, makeTokenByOwnerKey(oldOwner))

	// Update token owner
	token.Owner = req.NewOwner
	token.Status = state.TokenStatusActive
	token.StatusReason = ""
	token.UpdatedAt = ic.Block.Index

	// Store updated token (this will create new owner -> tokenID mapping)
	if err := v.putToken(ic.DAO, token); err != nil {
		panic(err)
	}

	// Update request status
	req.Status = state.RecoveryStatusExecuted
	if err := v.putRecoveryRequest(ic.DAO, req); err != nil {
		panic(err)
	}

	// Clear active recovery
	v.clearActiveRecovery(ic.DAO, req.TokenID)

	// Emit event
	err = ic.AddNotification(v.Hash, RecoveryExecutedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(req.TokenID))),
		stackitem.NewByteArray(oldOwner.BytesBE()),
		stackitem.NewByteArray(req.NewOwner.BytesBE()),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewBool(true)
}

// cancelRecovery cancels an active recovery request.
// Can be called by the original token owner or the requester.
func (v *Vita) cancelRecovery(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	requestIDBytes := toBytes(args[0])
	if len(requestIDBytes) != util.Uint256Size {
		panic("invalid request ID length")
	}
	requestID, err := util.Uint256DecodeBytesBE(requestIDBytes)
	if err != nil {
		panic(err)
	}

	// Get recovery request
	req, err := v.getRecoveryRequestInternal(ic.DAO, requestID)
	if err != nil {
		panic(err)
	}
	if req == nil {
		panic(ErrRecoveryNotFound)
	}

	// Check status is pending or approved (not executed or denied)
	if req.Status == state.RecoveryStatusExecuted || req.Status == state.RecoveryStatusDenied {
		panic(ErrRecoveryNotPending)
	}

	// Get token
	token, err := v.getTokenByIDInternal(ic.DAO, req.TokenID)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Check authorization: owner, requester, or committee
	caller := ic.VM.GetCallingScriptHash()
	isOwner := caller.Equals(token.Owner)
	isRequester := caller.Equals(req.Requester)
	isCommittee := v.checkCommittee(ic)

	if isOwner {
		ok, err := runtime.CheckHashedWitness(ic, token.Owner)
		if err != nil || !ok {
			panic(ErrVitaInvalidWitness)
		}
	} else if !isRequester && !isCommittee {
		panic(ErrVitaInvalidWitness)
	}

	// Update request status
	req.Status = state.RecoveryStatusDenied
	if err := v.putRecoveryRequest(ic.DAO, req); err != nil {
		panic(err)
	}

	// Restore token status
	token.Status = state.TokenStatusActive
	token.StatusReason = ""
	token.UpdatedAt = ic.Block.Index
	if err := v.putToken(ic.DAO, token); err != nil {
		panic(err)
	}

	// Clear active recovery
	v.clearActiveRecovery(ic.DAO, req.TokenID)

	// Emit event
	err = ic.AddNotification(v.Hash, RecoveryCancelledEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(requestID.BytesBE()),
		stackitem.NewByteArray(caller.BytesBE()),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewBool(true)
}

// getRecoveryRequest returns a recovery request by ID.
func (v *Vita) getRecoveryRequest(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	requestIDBytes := toBytes(args[0])
	if len(requestIDBytes) != util.Uint256Size {
		panic("invalid request ID length")
	}
	requestID, err := util.Uint256DecodeBytesBE(requestIDBytes)
	if err != nil {
		panic(err)
	}

	req, err := v.getRecoveryRequestInternal(ic.DAO, requestID)
	if err != nil {
		panic(err)
	}
	if req == nil {
		return stackitem.Null{}
	}

	item, err := req.ToStackItem()
	if err != nil {
		panic(err)
	}
	return item
}

// Public methods for cross-native recovery access

// GetRecoveryRequest returns a recovery request by ID (for cross-native access).
func (v *Vita) GetRecoveryRequest(d *dao.Simple, requestID util.Uint256) (*state.RecoveryRequest, error) {
	return v.getRecoveryRequestInternal(d, requestID)
}

// GetActiveRecoveryForToken returns the active recovery request ID for a token (for cross-native access).
func (v *Vita) GetActiveRecoveryForToken(d *dao.Simple, tokenID uint64) *util.Uint256 {
	return v.getActiveRecovery(d, tokenID)
}

// Cross-contract integration methods

// validateCaller validates that the calling contract's owner has a valid Vita.
// Returns the tokenID and core roles for the caller.
func (v *Vita) validateCaller(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	caller := ic.VM.GetCallingScriptHash()

	// Get token by caller address
	token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrCallerHasNoVita)
	}

	// Check token is active
	if token.Status != state.TokenStatusActive {
		if token.Status == state.TokenStatusSuspended {
			panic(ErrTokenSuspended)
		}
		if token.Status == state.TokenStatusRevoked {
			panic(ErrTokenRevoked)
		}
		if token.Status == state.TokenStatusRecovering {
			panic(ErrTokenInRecovery)
		}
		panic(ErrTokenNotActive)
	}

	// Determine core roles
	roles := v.getCoreRoles(ic, token)

	// Return struct with tokenID and roles
	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(token.TokenID))),
		stackitem.NewBigInteger(big.NewInt(int64(roles))),
	})
}

// getCoreRoles determines the core roles for a token holder.
func (v *Vita) getCoreRoles(ic *interop.Context, token *state.Vita) uint64 {
	var roles uint64

	// All active token holders have User role
	roles |= 1 << uint64(CoreRoleUser)

	// Check if user is verified (has non-expired, non-revoked attributes)
	if v.hasVerifiedAttributes(ic.DAO, token.TokenID) {
		roles |= 1 << uint64(CoreRoleVerified)
	}

	// Check if user is a committee member
	if v.checkCommittee(ic) {
		roles |= 1 << uint64(CoreRoleCommittee)
		roles |= 1 << uint64(CoreRoleAttestor)  // Committee members can attest
		roles |= 1 << uint64(CoreRoleRecovery)  // Committee members participate in recovery
	}

	return roles
}

// hasVerifiedAttributes checks if a token has any valid (non-expired, non-revoked) attributes.
func (v *Vita) hasVerifiedAttributes(d *dao.Simple, tokenID uint64) bool {
	// For now, just check if any attribute exists
	// In a full implementation, this would scan attributes and check expiry/revocation
	prefix := make([]byte, 9)
	prefix[0] = prefixAttribute
	binary.BigEndian.PutUint64(prefix[1:], tokenID)

	// Check if any attribute storage items exist for this token
	var found bool
	d.Seek(v.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		found = true
		return false // Stop after finding first
	})
	return found
}

// requireRole checks if the caller has a specific role ID (for RoleRegistry integration).
// This is a stub that will be extended when RoleRegistry is implemented.
func (v *Vita) requireRole(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	roleID := toBigInt(args[0]).Uint64()

	caller := ic.VM.GetCallingScriptHash()

	// Get token by caller address
	token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrCallerHasNoVita)
	}

	// Check token is active
	if token.Status != state.TokenStatusActive {
		panic(ErrTokenNotActive)
	}

	// Stub: In the future, this will check against RoleRegistry
	// For now, just validate the token exists and is active
	// Role 0 means "any authenticated user"
	if roleID == 0 {
		return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
	}

	// For non-zero roles, check if it maps to a core role
	if roleID <= uint64(CoreRoleRecovery) {
		roles := v.getCoreRoles(ic, token)
		if roles&(1<<roleID) != 0 {
			return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
		}
	}

	panic(ErrRoleNotAssigned)
}

// requireCoreRole checks if the caller has a specific core role.
func (v *Vita) requireCoreRole(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	coreRole := CoreRole(toBigInt(args[0]).Uint64())

	if coreRole > CoreRoleRecovery {
		panic(ErrVitaInvalidRole)
	}

	caller := ic.VM.GetCallingScriptHash()

	// Get token by caller address
	token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrCallerHasNoVita)
	}

	// Check token is active
	if token.Status != state.TokenStatusActive {
		panic(ErrTokenNotActive)
	}

	// CoreRoleNone means any authenticated user
	if coreRole == CoreRoleNone {
		return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
	}

	// Check if user has the required core role
	roles := v.getCoreRoles(ic, token)
	if roles&(1<<uint64(coreRole)) != 0 {
		return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
	}

	panic(ErrRoleNotAssigned)
}

// requirePermission checks if the caller has permission for a resource/action.
// This is a stub for future RoleRegistry integration with RBAC.
func (v *Vita) requirePermission(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	resource := toString(args[0])
	action := toString(args[1])
	scope := toString(args[2])

	if len(resource) == 0 {
		panic(ErrVitaInvalidResource)
	}
	if len(action) == 0 {
		panic(ErrVitaInvalidAction)
	}

	caller := ic.VM.GetCallingScriptHash()

	// Get token by caller address
	token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrCallerHasNoVita)
	}

	// Check token is active
	if token.Status != state.TokenStatusActive {
		panic(ErrTokenNotActive)
	}

	// Stub: In future, this will check against RoleRegistry RBAC
	// For now, committee members have all permissions
	if v.checkCommittee(ic) {
		return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
	}

	// For now, allow any authenticated user for basic resources
	// This is a permissive stub - real implementation will check RBAC
	_ = scope // scope will be used in future implementation
	return stackitem.NewBigInteger(big.NewInt(int64(token.TokenID)))
}

// Public methods for cross-native access control

// ValidateCaller validates a caller has a Vita and returns their roles.
func (v *Vita) ValidateCaller(ic *interop.Context, caller util.Uint160) (*state.Vita, uint64, error) {
	token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
	if err != nil {
		return nil, 0, err
	}
	if token == nil {
		return nil, 0, ErrCallerHasNoVita
	}
	if token.Status != state.TokenStatusActive {
		return nil, 0, ErrTokenNotActive
	}
	roles := v.getCoreRoles(ic, token)
	return token, roles, nil
}

// HasCoreRole checks if a token holder has a specific core role.
func (v *Vita) HasCoreRole(ic *interop.Context, tokenID uint64, role CoreRole) bool {
	token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
	if err != nil || token == nil {
		return false
	}
	if token.Status != state.TokenStatusActive {
		return false
	}
	roles := v.getCoreRoles(ic, token)
	return roles&(1<<uint64(role)) != 0
}

// Vesting methods for Sybil resistance

// setVesting sets or updates the vesting period for a Vita token (committee only).
// This can be used to accelerate vesting for verified identities or extend it for suspicious ones.
func (v *Vita) setVesting(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID := toBigInt(args[0]).Uint64()
	vestedUntil := uint32(toBigInt(args[1]).Int64())

	// Check committee
	if !v.checkCommittee(ic) {
		panic(ErrNotCommittee)
	}

	// Get token
	token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Check token is not revoked
	if token.Status == state.TokenStatusRevoked {
		panic(ErrTokenRevoked)
	}

	// Update vesting
	token.VestedUntil = vestedUntil
	token.UpdatedAt = ic.Block.Index

	// Store updated token
	if err := v.putToken(ic.DAO, token); err != nil {
		panic(err)
	}

	// Get caller for event
	caller := ic.VM.GetCallingScriptHash()

	// Emit event
	err = ic.AddNotification(v.Hash, VestingUpdatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(tokenID))),
		stackitem.NewBigInteger(big.NewInt(int64(vestedUntil))),
		stackitem.NewByteArray(caller.BytesBE()),
	}))
	if err != nil {
		panic(err)
	}

	return stackitem.NewBool(true)
}

// isFullyVested checks if a Vita token has completed its vesting period.
// Returns true if the current block height is >= vestedUntil.
func (v *Vita) isFullyVested(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID := toBigInt(args[0]).Uint64()

	token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
	if err != nil {
		panic(err)
	}
	if token == nil {
		panic(ErrTokenNotFound)
	}

	// Token is fully vested if current block >= vestedUntil
	// VestedUntil of 0 means immediately vested (legacy tokens)
	isVested := token.VestedUntil == 0 || ic.Block.Index >= token.VestedUntil
	return stackitem.NewBool(isVested)
}

// getVestingInfo returns vesting information for a Vita token.
// Returns [vestedUntil, isFullyVested, remainingBlocks] or null if token not found.
func (v *Vita) getVestingInfo(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID := toBigInt(args[0]).Uint64()

	token, err := v.getTokenByIDInternal(ic.DAO, tokenID)
	if err != nil {
		panic(err)
	}
	if token == nil {
		return stackitem.Null{}
	}

	isVested := token.VestedUntil == 0 || ic.Block.Index >= token.VestedUntil

	var remainingBlocks uint32
	if !isVested && token.VestedUntil > ic.Block.Index {
		remainingBlocks = token.VestedUntil - ic.Block.Index
	}

	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(token.VestedUntil))),
		stackitem.NewBool(isVested),
		stackitem.NewBigInteger(big.NewInt(int64(remainingBlocks))),
	})
}

// Public methods for cross-native vesting access

// IsFullyVestedInternal checks if a Vita token has completed its vesting period.
// For cross-contract use by other native contracts like Collocatio and Eligere.
func (v *Vita) IsFullyVestedInternal(d *dao.Simple, tokenID uint64, currentBlock uint32) bool {
	token, err := v.getTokenByIDInternal(d, tokenID)
	if err != nil || token == nil {
		return false
	}
	// VestedUntil of 0 means immediately vested (legacy tokens)
	return token.VestedUntil == 0 || currentBlock >= token.VestedUntil
}

// GetVestedUntil returns the vesting block height for a Vita token.
// Returns 0 if token not found.
func (v *Vita) GetVestedUntil(d *dao.Simple, tokenID uint64) uint32 {
	token, err := v.getTokenByIDInternal(d, tokenID)
	if err != nil || token == nil {
		return 0
	}
	return token.VestedUntil
}

// ============================================================================
// VPP (Vita Presence Protocol) Implementation
// ============================================================================

// presenceKey generates the storage key for a presence record.
func (v *Vita) presenceKey(tokenID uint64) []byte {
	key := make([]byte, 9)
	key[0] = prefixPresence
	binary.BigEndian.PutUint64(key[1:], tokenID)
	return key
}

// putPresence stores a presence record.
func (v *Vita) putPresence(d *dao.Simple, record *state.PresenceRecord) error {
	key := v.presenceKey(record.TokenID)
	item, err := record.ToStackItem()
	if err != nil {
		return err
	}
	data, err := stackitem.Serialize(item)
	if err != nil {
		return err
	}
	d.PutStorageItem(v.ID, key, data)
	return nil
}

// getPresenceInternal retrieves a presence record.
func (v *Vita) getPresenceInternal(d *dao.Simple, tokenID uint64) (*state.PresenceRecord, error) {
	key := v.presenceKey(tokenID)
	si := d.GetStorageItem(v.ID, key)
	if si == nil {
		return nil, ErrPresenceNotFound
	}
	item, err := stackitem.Deserialize(si)
	if err != nil {
		return nil, err
	}
	record := &state.PresenceRecord{}
	if err := record.FromStackItem(item); err != nil {
		return nil, err
	}
	return record, nil
}

// getPresenceConfigInternal retrieves or creates default presence config.
func (v *Vita) getPresenceConfigInternal(d *dao.Simple) state.PresenceConfig {
	key := []byte{prefixPresenceConfig}
	si := d.GetStorageItem(v.ID, key)
	if si == nil {
		return state.DefaultPresenceConfig()
	}
	item, err := stackitem.Deserialize(si)
	if err != nil {
		return state.DefaultPresenceConfig()
	}
	config := state.PresenceConfig{}
	if err := config.FromStackItem(item); err != nil {
		return state.DefaultPresenceConfig()
	}
	return config
}

// computePresenceStatus computes the current presence status based on last heartbeat.
func (v *Vita) computePresenceStatus(d *dao.Simple, tokenID uint64, currentBlock uint32) state.PresenceStatus {
	record, err := v.getPresenceInternal(d, tokenID)
	if err != nil {
		return state.PresenceStatusOffline
	}

	// If invisible mode, return invisible
	if record.Invisible {
		return state.PresenceStatusInvisible
	}

	config := v.getPresenceConfigInternal(d)
	blocksSinceHeartbeat := currentBlock - record.LastHeartbeat

	if blocksSinceHeartbeat <= config.PresenceWindow {
		return state.PresenceStatusOnline
	} else if blocksSinceHeartbeat <= config.AwayWindow {
		return state.PresenceStatusAway
	}
	return state.PresenceStatusOffline
}

// recordPresence records a heartbeat for the caller's Vita.
// This is the core VPP method called by the Symbey app.
func (v *Vita) recordPresence(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	sessionID, err := args[0].TryBytes()
	if err != nil {
		panic(fmt.Errorf("invalid sessionId: %w", err))
	}
	if len(sessionID) != 32 {
		panic(ErrPresenceInvalidSession)
	}

	deviceHash, err := args[1].TryBytes()
	if err != nil {
		panic(fmt.Errorf("invalid deviceHash: %w", err))
	}
	if len(deviceHash) != 32 {
		panic(ErrPresenceInvalidDevice)
	}

	// Get caller's Vita
	caller := ic.VM.GetCallingScriptHash()
	token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
	if err != nil || token == nil {
		panic(ErrCallerHasNoVita)
	}
	if token.Status != state.TokenStatusActive {
		panic(ErrTokenNotActive)
	}

	// Check rate limiting
	config := v.getPresenceConfigInternal(ic.DAO)
	existing, _ := v.getPresenceInternal(ic.DAO, token.TokenID)
	if existing != nil {
		blocksSinceLastHeartbeat := ic.Block.Index - existing.LastHeartbeat
		if blocksSinceLastHeartbeat < config.MinHeartbeatInterval {
			panic(ErrPresenceRateLimited)
		}
	}

	// Create or update presence record
	record := &state.PresenceRecord{
		TokenID:       token.TokenID,
		LastHeartbeat: ic.Block.Index,
		SessionID:     sessionID,
		DeviceHash:    deviceHash,
		Status:        state.PresenceStatusOnline,
		Invisible:     existing != nil && existing.Invisible,
	}

	if err := v.putPresence(ic.DAO, record); err != nil {
		panic(err)
	}

	// Emit event
	ic.AddNotification(v.Hash, PresenceRecordedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(token.TokenID))),
		stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))),
		stackitem.NewByteArray(sessionID),
	}))

	return stackitem.NewBool(true)
}

// getPresence retrieves the presence record for a token.
func (v *Vita) getPresence(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID, err := args[0].TryInteger()
	if err != nil {
		panic(fmt.Errorf("invalid tokenId: %w", err))
	}

	record, err := v.getPresenceInternal(ic.DAO, tokenID.Uint64())
	if err != nil {
		return stackitem.Null{}
	}

	// Compute current status based on time since last heartbeat
	record.Status = v.computePresenceStatus(ic.DAO, record.TokenID, ic.Block.Index)

	item, err := record.ToStackItem()
	if err != nil {
		panic(err)
	}
	return item
}

// getPresenceStatus returns the computed presence status for a token.
func (v *Vita) getPresenceStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tokenID, err := args[0].TryInteger()
	if err != nil {
		panic(fmt.Errorf("invalid tokenId: %w", err))
	}

	status := v.computePresenceStatus(ic.DAO, tokenID.Uint64(), ic.Block.Index)
	return stackitem.NewBigInteger(big.NewInt(int64(status)))
}

// setPresenceVisibility sets the caller's presence visibility (invisible mode).
func (v *Vita) setPresenceVisibility(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	invisible, err := args[0].TryBool()
	if err != nil {
		panic(fmt.Errorf("invalid invisible: %w", err))
	}

	// Get caller's Vita
	caller := ic.VM.GetCallingScriptHash()
	token, err := v.getTokenByOwnerInternal(ic.DAO, caller)
	if err != nil || token == nil {
		panic(ErrCallerHasNoVita)
	}

	// Get or create presence record
	record, _ := v.getPresenceInternal(ic.DAO, token.TokenID)
	if record == nil {
		record = &state.PresenceRecord{
			TokenID:       token.TokenID,
			LastHeartbeat: 0,
			SessionID:     make([]byte, 32),
			DeviceHash:    make([]byte, 32),
			Status:        state.PresenceStatusOffline,
		}
	}

	record.Invisible = invisible
	if invisible {
		record.Status = state.PresenceStatusInvisible
	}

	if err := v.putPresence(ic.DAO, record); err != nil {
		panic(err)
	}

	// Emit event
	ic.AddNotification(v.Hash, PresenceVisibilityEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(token.TokenID))),
		stackitem.NewBool(invisible),
	}))

	return stackitem.NewBool(true)
}

// getPresenceConfig returns the VPP configuration.
func (v *Vita) getPresenceConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	config := v.getPresenceConfigInternal(ic.DAO)
	item, err := config.ToStackItem()
	if err != nil {
		panic(err)
	}
	return item
}

// ============================================================================
// VPP Public Internal Methods (for cross-contract access)
// ============================================================================

// GetPresenceRecord returns the presence record for a token (internal use).
func (v *Vita) GetPresenceRecord(d *dao.Simple, tokenID uint64) (*state.PresenceRecord, error) {
	return v.getPresenceInternal(d, tokenID)
}

// GetPresenceStatusInternal returns the computed presence status (internal use).
func (v *Vita) GetPresenceStatusInternal(d *dao.Simple, tokenID uint64, currentBlock uint32) state.PresenceStatus {
	return v.computePresenceStatus(d, tokenID, currentBlock)
}

// IsOnline checks if a Vita holder is currently online.
func (v *Vita) IsOnline(d *dao.Simple, tokenID uint64, currentBlock uint32) bool {
	status := v.computePresenceStatus(d, tokenID, currentBlock)
	return status == state.PresenceStatusOnline
}

// IsPresent checks if a Vita holder is present (online or away, not offline).
func (v *Vita) IsPresent(d *dao.Simple, tokenID uint64, currentBlock uint32) bool {
	status := v.computePresenceStatus(d, tokenID, currentBlock)
	return status == state.PresenceStatusOnline || status == state.PresenceStatusAway
}
