package native

import (
	"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"
)

// Ancora represents the Ancora native contract for anchoring Merkle roots of off-chain data.
// Latin: "ancora" = anchor - anchors off-chain data to on-chain verification.
type Ancora struct {
	interop.ContractMD
	Vita   IVita
	Tutus  ITutus
	Audit  *AuditLogger // ARCH-005: Comprehensive audit logging
}

// AncoraCache holds cached configuration for the Ancora contract.
type AncoraCache struct {
	config state.StateAnchorsConfig
}

// Storage prefixes for Ancora contract.
const (
	ancoraConfigPrefix        = 0x01
	ancoraRootPrefix          = 0x10 // vitaID + dataType -> RootInfo
	ancoraHistoryPrefix       = 0x11 // vitaID + dataType + version -> RootInfo
	ancoraProviderPrefix      = 0x20 // dataType + provider -> ProviderConfig
	ancoraErasurePrefix       = 0x30 // vitaID + dataType -> ErasureInfo
	ancoraUpdateCountPrefix   = 0x40 // blockHeight + provider -> count
	ancoraLastUpdatePrefix    = 0x41 // vitaID + dataType + provider -> blockHeight
	ancoraAttestationPrefix   = 0x50 // attestationHash -> AttestationInfo
)

// Errors for Ancora contract.
var (
	ErrAncoraInvalidDataType      = errors.New("invalid data type")
	ErrAncoraInvalidRoot          = errors.New("invalid Merkle root: must be 32 bytes")
	ErrAncoraProviderNotFound     = errors.New("provider not found")
	ErrAncoraProviderInactive     = errors.New("provider is inactive")
	ErrAncoraProviderExists       = errors.New("provider already registered")
	ErrAncoraUnauthorized         = errors.New("unauthorized: caller is not authorized provider")
	ErrAncoraRateLimited          = errors.New("rate limit exceeded")
	ErrAncoraUpdateCooldown       = errors.New("update cooldown not elapsed")
	ErrAncoraNoRoot               = errors.New("no root found for vitaID and dataType")
	ErrAncoraInvalidProof         = errors.New("invalid Merkle proof")
	ErrAncoraProofTooDeep         = errors.New("proof exceeds maximum depth")
	ErrAncoraVersionNotFound      = errors.New("version not found in history")
	ErrAncoraErasurePending       = errors.New("erasure already pending")
	ErrAncoraErasureNotPending    = errors.New("no pending erasure request")
	ErrAncoraErasureGracePeriod   = errors.New("erasure grace period not elapsed")
	ErrAncoraInvalidAttestation   = errors.New("invalid attestation")
	ErrAncoraAttestationExpired   = errors.New("attestation has expired")
	ErrAncoraVitaNotFound         = errors.New("vita not found")
)

var (
	_ interop.Contract        = (*Ancora)(nil)
	_ dao.NativeContractCache = (*AncoraCache)(nil)
)

// Copy implements NativeContractCache interface.
func (c *AncoraCache) Copy() dao.NativeContractCache {
	cp := &AncoraCache{config: c.config}
	return cp
}

// newAncora returns a new Ancora native contract.
func newAncora() *Ancora {
	a := &Ancora{
		ContractMD: *interop.NewContractMD(nativenames.Ancora, nativeids.Ancora, nil),
		Audit:      NewAuditLogger(nativeids.Ancora),
	}
	defer a.BuildHFSpecificMD(a.ActiveIn())

	// Configuration methods
	desc := NewDescriptor("getConfig", smartcontract.ArrayType)
	md := NewMethodAndPrice(a.getConfig, 1<<15, callflag.ReadStates)
	a.AddMethod(md, desc)

	desc = NewDescriptor("setConfig", smartcontract.VoidType,
		manifest.NewParameter("config", smartcontract.ArrayType))
	md = NewMethodAndPrice(a.setConfig, 1<<15, callflag.States)
	a.AddMethod(md, desc)

	// Provider management
	desc = NewDescriptor("registerProvider", smartcontract.BoolType,
		manifest.NewParameter("dataType", smartcontract.IntegerType),
		manifest.NewParameter("provider", smartcontract.Hash160Type),
		manifest.NewParameter("description", smartcontract.StringType),
		manifest.NewParameter("maxUpdatesPerBlock", smartcontract.IntegerType),
		manifest.NewParameter("updateCooldown", smartcontract.IntegerType))
	md = NewMethodAndPrice(a.registerProvider, 1<<15, callflag.States)
	a.AddMethod(md, desc)

	desc = NewDescriptor("revokeProvider", smartcontract.BoolType,
		manifest.NewParameter("dataType", smartcontract.IntegerType),
		manifest.NewParameter("provider", smartcontract.Hash160Type))
	md = NewMethodAndPrice(a.revokeProvider, 1<<15, callflag.States)
	a.AddMethod(md, desc)

	desc = NewDescriptor("getProvider", smartcontract.ArrayType,
		manifest.NewParameter("dataType", smartcontract.IntegerType),
		manifest.NewParameter("provider", smartcontract.Hash160Type))
	md = NewMethodAndPrice(a.getProvider, 1<<15, callflag.ReadStates)
	a.AddMethod(md, desc)

	desc = NewDescriptor("isProviderActive", smartcontract.BoolType,
		manifest.NewParameter("dataType", smartcontract.IntegerType),
		manifest.NewParameter("provider", smartcontract.Hash160Type))
	md = NewMethodAndPrice(a.isProviderActive, 1<<15, callflag.ReadStates)
	a.AddMethod(md, desc)

	// Root management
	desc = NewDescriptor("updateDataRoot", smartcontract.BoolType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("dataType", smartcontract.IntegerType),
		manifest.NewParameter("root", smartcontract.ByteArrayType),
		manifest.NewParameter("leafCount", smartcontract.IntegerType),
		manifest.NewParameter("treeAlgorithm", smartcontract.IntegerType),
		manifest.NewParameter("schemaVersion", smartcontract.StringType),
		manifest.NewParameter("contentHash", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(a.updateDataRoot, 1<<16, callflag.States)
	a.AddMethod(md, desc)

	desc = NewDescriptor("getDataRoot", smartcontract.ArrayType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("dataType", smartcontract.IntegerType))
	md = NewMethodAndPrice(a.getDataRoot, 1<<15, callflag.ReadStates)
	a.AddMethod(md, desc)

	desc = NewDescriptor("getDataRootAtVersion", smartcontract.ArrayType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("dataType", smartcontract.IntegerType),
		manifest.NewParameter("version", smartcontract.IntegerType))
	md = NewMethodAndPrice(a.getDataRootAtVersion, 1<<15, callflag.ReadStates)
	a.AddMethod(md, desc)

	// Proof verification
	desc = NewDescriptor("verifyProof", smartcontract.BoolType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("dataType", smartcontract.IntegerType),
		manifest.NewParameter("leaf", smartcontract.ByteArrayType),
		manifest.NewParameter("proof", smartcontract.ArrayType),
		manifest.NewParameter("index", smartcontract.IntegerType))
	md = NewMethodAndPrice(a.verifyProof, 1<<16, callflag.ReadStates)
	a.AddMethod(md, desc)

	desc = NewDescriptor("verifyProofAtVersion", smartcontract.BoolType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("dataType", smartcontract.IntegerType),
		manifest.NewParameter("version", smartcontract.IntegerType),
		manifest.NewParameter("leaf", smartcontract.ByteArrayType),
		manifest.NewParameter("proof", smartcontract.ArrayType),
		manifest.NewParameter("index", smartcontract.IntegerType))
	md = NewMethodAndPrice(a.verifyProofAtVersion, 1<<16, callflag.ReadStates)
	a.AddMethod(md, desc)

	// GDPR erasure
	desc = NewDescriptor("requestErasure", smartcontract.BoolType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("dataType", smartcontract.IntegerType),
		manifest.NewParameter("reason", smartcontract.StringType))
	md = NewMethodAndPrice(a.requestErasure, 1<<15, callflag.States)
	a.AddMethod(md, desc)

	desc = NewDescriptor("confirmErasure", smartcontract.BoolType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("dataType", smartcontract.IntegerType))
	md = NewMethodAndPrice(a.confirmErasure, 1<<15, callflag.States)
	a.AddMethod(md, desc)

	desc = NewDescriptor("denyErasure", smartcontract.BoolType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("dataType", smartcontract.IntegerType),
		manifest.NewParameter("reason", smartcontract.StringType))
	md = NewMethodAndPrice(a.denyErasure, 1<<15, callflag.States)
	a.AddMethod(md, desc)

	desc = NewDescriptor("getErasureInfo", smartcontract.ArrayType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("dataType", smartcontract.IntegerType))
	md = NewMethodAndPrice(a.getErasureInfo, 1<<15, callflag.ReadStates)
	a.AddMethod(md, desc)

	// Data portability
	desc = NewDescriptor("generatePortabilityAttestation", smartcontract.ByteArrayType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("dataTypes", smartcontract.ArrayType))
	md = NewMethodAndPrice(a.generatePortabilityAttestation, 1<<16, callflag.States)
	a.AddMethod(md, desc)

	desc = NewDescriptor("verifyPortabilityAttestation", smartcontract.BoolType,
		manifest.NewParameter("attestation", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(a.verifyPortabilityAttestation, 1<<15, callflag.ReadStates)
	a.AddMethod(md, desc)

	// Audit log query (ARCH-005)
	desc = NewDescriptor("getAuditLog", smartcontract.ArrayType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("startBlock", smartcontract.IntegerType),
		manifest.NewParameter("endBlock", smartcontract.IntegerType),
		manifest.NewParameter("limit", smartcontract.IntegerType))
	md = NewMethodAndPrice(a.getAuditLog, 1<<16, callflag.ReadStates)
	a.AddMethod(md, desc)

	return a
}

// Metadata implements the Contract interface.
func (a *Ancora) Metadata() *interop.ContractMD {
	return &a.ContractMD
}

// Initialize initializes Ancora native contract and implements the Contract interface.
func (a *Ancora) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
	if hf != a.ActiveIn() {
		return nil
	}

	cfg := state.DefaultStateAnchorsConfig()
	if err := a.putConfig(ic.DAO, &cfg); err != nil {
		return fmt.Errorf("failed to initialize config: %w", err)
	}

	cache := &AncoraCache{config: cfg}
	ic.DAO.SetCache(a.ID, cache)
	return nil
}

// InitializeCache implements the Contract interface.
func (a *Ancora) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
	cfg, err := a.loadConfig(d)
	if err != nil {
		return fmt.Errorf("failed to load config: %w", err)
	}

	cache := &AncoraCache{config: *cfg}
	d.SetCache(a.ID, cache)
	return nil
}

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

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

// ActiveIn implements the Contract interface.
func (a *Ancora) ActiveIn() *config.Hardfork {
	return nil // Active from genesis
}

// Address returns the contract's script hash.
func (a *Ancora) Address() util.Uint160 {
	return a.Hash
}

// ========== Configuration Methods ==========

func (a *Ancora) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	cache := ic.DAO.GetROCache(a.ID).(*AncoraCache)
	cfg := cache.config
	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(cfg.DefaultTreeAlgorithm))),
		stackitem.NewBigInteger(big.NewInt(int64(cfg.MaxProofDepth))),
		stackitem.NewBigInteger(big.NewInt(int64(cfg.DefaultMaxUpdatesPerBlock))),
		stackitem.NewBigInteger(big.NewInt(int64(cfg.DefaultUpdateCooldown))),
		stackitem.NewBigInteger(big.NewInt(int64(cfg.MaxHistoryVersions))),
		stackitem.NewBigInteger(big.NewInt(int64(cfg.ErasureGracePeriod))),
		stackitem.NewBigInteger(big.NewInt(int64(cfg.AttestationValidBlocks))),
	})
}

func (a *Ancora) setConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	if !a.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}

	arr, ok := args[0].Value().([]stackitem.Item)
	if !ok || len(arr) != 7 {
		panic("invalid config array")
	}

	cfg := state.StateAnchorsConfig{
		DefaultTreeAlgorithm:      state.TreeAlgorithm(toUint32(arr[0])),
		MaxProofDepth:             toUint32(arr[1]),
		DefaultMaxUpdatesPerBlock: toUint32(arr[2]),
		DefaultUpdateCooldown:     toUint32(arr[3]),
		MaxHistoryVersions:        toUint32(arr[4]),
		ErasureGracePeriod:        toUint32(arr[5]),
		AttestationValidBlocks:    toUint32(arr[6]),
	}

	if err := a.putConfig(ic.DAO, &cfg); err != nil {
		panic(fmt.Errorf("failed to save config: %w", err))
	}

	cache := ic.DAO.GetRWCache(a.ID).(*AncoraCache)
	cache.config = cfg
	return stackitem.Null{}
}

func (a *Ancora) putConfig(d *dao.Simple, cfg *state.StateAnchorsConfig) error {
	key := []byte{ancoraConfigPrefix}
	data := make([]byte, 28) // 7 * 4 bytes
	// Simple serialization
	putUint32(data[0:4], uint32(cfg.DefaultTreeAlgorithm))
	putUint32(data[4:8], cfg.MaxProofDepth)
	putUint32(data[8:12], cfg.DefaultMaxUpdatesPerBlock)
	putUint32(data[12:16], cfg.DefaultUpdateCooldown)
	putUint32(data[16:20], cfg.MaxHistoryVersions)
	putUint32(data[20:24], cfg.ErasureGracePeriod)
	putUint32(data[24:28], cfg.AttestationValidBlocks)
	d.PutStorageItem(a.ID, key, data)
	return nil
}

func (a *Ancora) loadConfig(d *dao.Simple) (*state.StateAnchorsConfig, error) {
	key := []byte{ancoraConfigPrefix}
	data := d.GetStorageItem(a.ID, key)
	if data == nil {
		return nil, errors.New("config not found")
	}
	if len(data) != 28 {
		return nil, errors.New("invalid config data length")
	}
	cfg := &state.StateAnchorsConfig{
		DefaultTreeAlgorithm:      state.TreeAlgorithm(getUint32(data[0:4])),
		MaxProofDepth:             getUint32(data[4:8]),
		DefaultMaxUpdatesPerBlock: getUint32(data[8:12]),
		DefaultUpdateCooldown:     getUint32(data[12:16]),
		MaxHistoryVersions:        getUint32(data[16:20]),
		ErasureGracePeriod:        getUint32(data[20:24]),
		AttestationValidBlocks:    getUint32(data[24:28]),
	}
	return cfg, nil
}

// ========== Provider Management ==========

func (a *Ancora) registerProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	if !a.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}

	dataType := state.DataType(toUint32(args[0]))
	if dataType > state.DataTypeCustom {
		panic(ErrAncoraInvalidDataType)
	}
	// Note: Valid types are 0-5 (Medical, Education, Investment, Documents, Presence, Custom)

	provider := toUint160(args[1])
	description := toString(args[2])
	maxUpdatesPerBlock := toUint32(args[3])
	updateCooldown := toUint32(args[4])

	// Check if provider already exists
	existing := a.getProviderConfig(ic.DAO, dataType, provider)
	if existing != nil {
		panic(ErrAncoraProviderExists)
	}

	cfg := &state.ProviderConfig{
		DataType:           dataType,
		Provider:           provider,
		Description:        description,
		RegisteredAt:       ic.BlockHeight(),
		Active:             true,
		MaxUpdatesPerBlock: maxUpdatesPerBlock,
		UpdateCooldown:     updateCooldown,
	}

	if err := a.putProviderConfig(ic.DAO, cfg); err != nil {
		panic(fmt.Errorf("failed to register provider: %w", err))
	}

	// ARCH-005: Audit log provider registration
	a.Audit.LogGovernance(ic.DAO, ic.VM.GetCallingScriptHash(), "ancora.provider_registered",
		fmt.Sprintf("dataType=%d provider=%s desc=%s", dataType, provider.StringLE(), description),
		AuditOutcomeSuccess, ic.BlockHeight())

	ic.AddNotification(a.Hash, "ProviderRegistered", stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(dataType))),
		stackitem.NewByteArray(provider.BytesBE()),
		stackitem.NewByteArray([]byte(description)),
	}))

	return stackitem.NewBool(true)
}

func (a *Ancora) revokeProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	if !a.Tutus.CheckCommittee(ic) {
		panic("invalid committee signature")
	}

	dataType := state.DataType(toUint32(args[0]))
	provider := toUint160(args[1])

	cfg := a.getProviderConfig(ic.DAO, dataType, provider)
	if cfg == nil {
		panic(ErrAncoraProviderNotFound)
	}

	cfg.Active = false
	if err := a.putProviderConfig(ic.DAO, cfg); err != nil {
		panic(fmt.Errorf("failed to revoke provider: %w", err))
	}

	// ARCH-005: Audit log provider revocation
	a.Audit.LogGovernance(ic.DAO, ic.VM.GetCallingScriptHash(), "ancora.provider_revoked",
		fmt.Sprintf("dataType=%d provider=%s", dataType, provider.StringLE()),
		AuditOutcomeSuccess, ic.BlockHeight())

	ic.AddNotification(a.Hash, "ProviderRevoked", stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(dataType))),
		stackitem.NewByteArray(provider.BytesBE()),
	}))

	return stackitem.NewBool(true)
}

func (a *Ancora) getProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	dataType := state.DataType(toUint32(args[0]))
	provider := toUint160(args[1])

	cfg := a.getProviderConfig(ic.DAO, dataType, provider)
	if cfg == nil {
		return stackitem.Null{}
	}
	item, err := cfg.ToStackItem()
	if err != nil {
		panic(fmt.Errorf("failed to convert provider config to stack item: %w", err))
	}
	return item
}

func (a *Ancora) isProviderActive(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	dataType := state.DataType(toUint32(args[0]))
	provider := toUint160(args[1])

	cfg := a.getProviderConfig(ic.DAO, dataType, provider)
	return stackitem.NewBool(cfg != nil && cfg.Active)
}

func (a *Ancora) getProviderConfig(d *dao.Simple, dataType state.DataType, provider util.Uint160) *state.ProviderConfig {
	key := append([]byte{ancoraProviderPrefix, byte(dataType)}, provider.BytesBE()...)
	cfg := new(state.ProviderConfig)
	err := getConvertibleFromDAO(a.ID, d, key, cfg)
	if err != nil {
		if errors.Is(err, storage.ErrKeyNotFound) {
			return nil
		}
		panic(fmt.Errorf("failed to get provider config: %w", err))
	}
	return cfg
}

func (a *Ancora) putProviderConfig(d *dao.Simple, cfg *state.ProviderConfig) error {
	key := append([]byte{ancoraProviderPrefix, byte(cfg.DataType)}, cfg.Provider.BytesBE()...)
	return putConvertibleToDAO(a.ID, d, key, cfg)
}

// ========== Root Management ==========

func (a *Ancora) updateDataRoot(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	dataType := state.DataType(toUint32(args[1]))
	root, err := args[2].TryBytes()
	if err != nil {
		panic(ErrAncoraInvalidRoot)
	}
	if len(root) != 32 {
		panic(ErrAncoraInvalidRoot)
	}
	leafCount := toUint64(args[3])
	treeAlgorithm := state.TreeAlgorithm(toUint32(args[4]))
	schemaVersion := toString(args[5])
	contentHash, _ := args[6].TryBytes()

	// Verify Vita exists
	if !a.Vita.ExistsInternal(ic.DAO, vitaID) {
		panic(ErrAncoraVitaNotFound)
	}

	// Verify caller is authorized provider or Vita owner
	caller := ic.VM.GetCallingScriptHash()
	providerCfg := a.getProviderConfig(ic.DAO, dataType, caller)

	if providerCfg == nil || !providerCfg.Active {
		// Check if caller is Vita owner
		owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
		ok, err := runtime.CheckHashedWitness(ic, owner)
		if err != nil || !ok {
			panic(ErrAncoraUnauthorized)
		}
	} else {
		// Rate limiting for providers
		if err := a.checkRateLimit(ic, providerCfg, vitaID, dataType); err != nil {
			panic(err)
		}
	}

	// Get current root to determine version
	currentRoot := a.getRootInfo(ic.DAO, vitaID, dataType)
	var version uint64 = 1
	if currentRoot != nil {
		version = currentRoot.Version + 1
		// Archive current root to history
		if err := a.archiveRoot(ic.DAO, vitaID, dataType, currentRoot); err != nil {
			panic(fmt.Errorf("failed to archive previous root: %w", err))
		}
	}

	newRoot := &state.RootInfo{
		Root:          root,
		LeafCount:     leafCount,
		UpdatedAt:     ic.BlockHeight(),
		UpdatedBy:     caller,
		Version:       version,
		TreeAlgorithm: treeAlgorithm,
		SchemaVersion: schemaVersion,
		ContentHash:   contentHash,
	}

	if err := a.putRootInfo(ic.DAO, vitaID, dataType, newRoot); err != nil {
		panic(fmt.Errorf("failed to update root: %w", err))
	}

	// Update rate limit tracking
	a.recordUpdate(ic.DAO, ic.BlockHeight(), caller, vitaID, dataType)

	// ARCH-005: Audit log data root update
	// For GDPR compliance: the on-chain audit entry serves as immutable proof
	// that an update occurred, even if off-chain data is later deleted
	prevStateHash := util.Uint256{}
	newStateHash := hash.Sha256(root)
	if currentRoot != nil {
		prevStateHash = hash.Sha256(currentRoot.Root)
	}
	resourceID := make([]byte, 9)
	putUint64(resourceID[0:8], vitaID)
	resourceID[8] = byte(dataType)
	a.Audit.LogDataChange(ic.DAO, caller, nativeids.Ancora, "ancora.data_root_updated",
		resourceID, prevStateHash, newStateHash, ic.BlockHeight())

	ic.AddNotification(a.Hash, "DataRootUpdated", stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
		stackitem.NewBigInteger(big.NewInt(int64(dataType))),
		stackitem.NewByteArray(root),
		stackitem.NewBigInteger(big.NewInt(int64(version))),
		stackitem.NewByteArray(caller.BytesBE()),
	}))

	return stackitem.NewBool(true)
}

func (a *Ancora) getDataRoot(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	dataType := state.DataType(toUint32(args[1]))

	root := a.getRootInfo(ic.DAO, vitaID, dataType)
	if root == nil {
		return stackitem.Null{}
	}
	item, err := root.ToStackItem()
	if err != nil {
		panic(fmt.Errorf("failed to convert root to stack item: %w", err))
	}
	return item
}

func (a *Ancora) getDataRootAtVersion(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	dataType := state.DataType(toUint32(args[1]))
	version := toUint64(args[2])

	// First check current version
	current := a.getRootInfo(ic.DAO, vitaID, dataType)
	if current != nil && current.Version == version {
		item, err := current.ToStackItem()
		if err != nil {
			panic(fmt.Errorf("failed to convert root to stack item: %w", err))
		}
		return item
	}

	// Check history
	root := a.getHistoricalRoot(ic.DAO, vitaID, dataType, version)
	if root == nil {
		return stackitem.Null{}
	}
	item, err := root.ToStackItem()
	if err != nil {
		panic(fmt.Errorf("failed to convert root to stack item: %w", err))
	}
	return item
}

func (a *Ancora) getRootInfo(d *dao.Simple, vitaID uint64, dataType state.DataType) *state.RootInfo {
	key := a.makeRootKey(vitaID, dataType)
	root := new(state.RootInfo)
	err := getConvertibleFromDAO(a.ID, d, key, root)
	if err != nil {
		if errors.Is(err, storage.ErrKeyNotFound) {
			return nil
		}
		panic(fmt.Errorf("failed to get root info: %w", err))
	}
	return root
}

func (a *Ancora) putRootInfo(d *dao.Simple, vitaID uint64, dataType state.DataType, root *state.RootInfo) error {
	key := a.makeRootKey(vitaID, dataType)
	return putConvertibleToDAO(a.ID, d, key, root)
}

func (a *Ancora) getHistoricalRoot(d *dao.Simple, vitaID uint64, dataType state.DataType, version uint64) *state.RootInfo {
	key := a.makeHistoryKey(vitaID, dataType, version)
	root := new(state.RootInfo)
	err := getConvertibleFromDAO(a.ID, d, key, root)
	if err != nil {
		if errors.Is(err, storage.ErrKeyNotFound) {
			return nil
		}
		panic(fmt.Errorf("failed to get historical root: %w", err))
	}
	return root
}

func (a *Ancora) archiveRoot(d *dao.Simple, vitaID uint64, dataType state.DataType, root *state.RootInfo) error {
	key := a.makeHistoryKey(vitaID, dataType, root.Version)
	return putConvertibleToDAO(a.ID, d, key, root)
}

func (a *Ancora) makeRootKey(vitaID uint64, dataType state.DataType) []byte {
	key := make([]byte, 1+8+1)
	key[0] = ancoraRootPrefix
	putUint64(key[1:9], vitaID)
	key[9] = byte(dataType)
	return key
}

func (a *Ancora) makeHistoryKey(vitaID uint64, dataType state.DataType, version uint64) []byte {
	key := make([]byte, 1+8+1+8)
	key[0] = ancoraHistoryPrefix
	putUint64(key[1:9], vitaID)
	key[9] = byte(dataType)
	putUint64(key[10:18], version)
	return key
}

// ========== Proof Verification ==========

func (a *Ancora) verifyProof(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	dataType := state.DataType(toUint32(args[1]))
	leaf, err := args[2].TryBytes()
	if err != nil {
		panic(ErrAncoraInvalidProof)
	}

	proofArr, ok := args[3].Value().([]stackitem.Item)
	if !ok {
		panic(ErrAncoraInvalidProof)
	}

	proof := make([][]byte, len(proofArr))
	for i, item := range proofArr {
		proof[i], err = item.TryBytes()
		if err != nil {
			panic(ErrAncoraInvalidProof)
		}
	}

	index := toUint64(args[4])

	rootInfo := a.getRootInfo(ic.DAO, vitaID, dataType)
	if rootInfo == nil {
		return stackitem.NewBool(false)
	}

	valid := a.verifyMerkleProofInternal(rootInfo.Root, leaf, proof, index, rootInfo.TreeAlgorithm)

	// ARCH-005: Audit log proof verification (access tracking)
	caller := ic.VM.GetCallingScriptHash()
	owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
	resourceID := make([]byte, 9)
	putUint64(resourceID[0:8], vitaID)
	resourceID[8] = byte(dataType)
	outcome := AuditOutcomeSuccess
	if !valid {
		outcome = AuditOutcomeFailure
	}
	a.Audit.LogAccess(ic.DAO, caller, owner, "ancora.proof_verified", resourceID, outcome, ic.BlockHeight())

	return stackitem.NewBool(valid)
}

func (a *Ancora) verifyProofAtVersion(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	dataType := state.DataType(toUint32(args[1]))
	version := toUint64(args[2])
	leaf, err := args[3].TryBytes()
	if err != nil {
		panic(ErrAncoraInvalidProof)
	}

	proofArr, ok := args[4].Value().([]stackitem.Item)
	if !ok {
		panic(ErrAncoraInvalidProof)
	}

	proof := make([][]byte, len(proofArr))
	for i, item := range proofArr {
		proof[i], err = item.TryBytes()
		if err != nil {
			panic(ErrAncoraInvalidProof)
		}
	}

	index := toUint64(args[5])

	// Check current version first
	rootInfo := a.getRootInfo(ic.DAO, vitaID, dataType)
	if rootInfo != nil && rootInfo.Version == version {
		valid := a.verifyMerkleProofInternal(rootInfo.Root, leaf, proof, index, rootInfo.TreeAlgorithm)
		a.logProofVerification(ic, vitaID, dataType, version, valid)
		return stackitem.NewBool(valid)
	}

	// Check historical version
	rootInfo = a.getHistoricalRoot(ic.DAO, vitaID, dataType, version)
	if rootInfo == nil {
		return stackitem.NewBool(false)
	}

	valid := a.verifyMerkleProofInternal(rootInfo.Root, leaf, proof, index, rootInfo.TreeAlgorithm)
	a.logProofVerification(ic, vitaID, dataType, version, valid)
	return stackitem.NewBool(valid)
}

func (a *Ancora) verifyMerkleProofInternal(root, leaf []byte, proof [][]byte, index uint64, algorithm state.TreeAlgorithm) bool {
	if len(root) != 32 {
		return false
	}

	// Check proof depth
	cache := a.getCache(nil)
	if cache != nil && uint32(len(proof)) > cache.config.MaxProofDepth {
		return false
	}

	// Compute hash based on algorithm
	// Note: Currently only SHA256 is implemented. Keccak256 and Poseidon are TODO.
	var computed []byte
	switch algorithm {
	case state.TreeAlgorithmSHA256:
		computed = hash.Sha256(leaf).BytesBE()
	case state.TreeAlgorithmKeccak256:
		// TODO: Implement Keccak256 when needed for EVM compatibility
		computed = hash.Sha256(leaf).BytesBE()
	default:
		// Poseidon not yet implemented, fallback to SHA256
		computed = hash.Sha256(leaf).BytesBE()
	}

	// Traverse the proof
	for _, sibling := range proof {
		if len(sibling) != 32 {
			return false
		}

		var combined []byte
		if index%2 == 0 {
			combined = append(computed, sibling...)
		} else {
			combined = append(sibling, computed...)
		}

		switch algorithm {
		case state.TreeAlgorithmSHA256:
			computed = hash.Sha256(combined).BytesBE()
		case state.TreeAlgorithmKeccak256:
			// TODO: Implement Keccak256 when needed for EVM compatibility
			computed = hash.Sha256(combined).BytesBE()
		default:
			computed = hash.Sha256(combined).BytesBE()
		}

		index /= 2
	}

	// Compare computed root with stored root
	if len(computed) != len(root) {
		return false
	}
	for i := range computed {
		if computed[i] != root[i] {
			return false
		}
	}
	return true
}

// VerifyProofInternal is a cross-contract method for other native contracts.
func (a *Ancora) VerifyProofInternal(d *dao.Simple, vitaID uint64, dataType state.DataType, leaf []byte, proof [][]byte, index uint64) bool {
	rootInfo := a.getRootInfo(d, vitaID, dataType)
	if rootInfo == nil {
		return false
	}
	return a.verifyMerkleProofInternal(rootInfo.Root, leaf, proof, index, rootInfo.TreeAlgorithm)
}

// RequireValidRoot is a cross-contract method that panics if no valid root exists.
func (a *Ancora) RequireValidRoot(d *dao.Simple, vitaID uint64, dataType state.DataType) {
	root := a.getRootInfo(d, vitaID, dataType)
	if root == nil {
		panic(ErrAncoraNoRoot)
	}
}

// logProofVerification logs a proof verification to the audit trail.
func (a *Ancora) logProofVerification(ic *interop.Context, vitaID uint64, dataType state.DataType, version uint64, valid bool) {
	caller := ic.VM.GetCallingScriptHash()
	owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
	resourceID := make([]byte, 17)
	putUint64(resourceID[0:8], vitaID)
	resourceID[8] = byte(dataType)
	putUint64(resourceID[9:17], version)
	outcome := AuditOutcomeSuccess
	if !valid {
		outcome = AuditOutcomeFailure
	}
	a.Audit.LogAccess(ic.DAO, caller, owner, "ancora.proof_verified_at_version", resourceID, outcome, ic.BlockHeight())
}

// ========== GDPR Erasure ==========

func (a *Ancora) requestErasure(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	dataType := state.DataType(toUint32(args[1]))
	reason := toString(args[2])

	// Verify caller is Vita owner
	owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
	ok, err := runtime.CheckHashedWitness(ic, owner)
	if err != nil || !ok {
		panic(ErrAncoraUnauthorized)
	}

	// Check for existing erasure request
	existing := a.getErasureInfoInternal(ic.DAO, vitaID, dataType)
	if existing != nil && existing.Status == state.ErasurePending {
		panic(ErrAncoraErasurePending)
	}

	erasure := &state.ErasureInfo{
		RequestedAt: ic.BlockHeight(),
		RequestedBy: owner,
		Reason:      reason,
		Status:      state.ErasurePending,
	}

	if err := a.putErasureInfo(ic.DAO, vitaID, dataType, erasure); err != nil {
		panic(fmt.Errorf("failed to save erasure request: %w", err))
	}

	// ARCH-005: Audit log erasure request (GDPR Article 17 - Right to be forgotten)
	// This audit entry is CRITICAL for compliance - proves user exercised their right
	resourceID := make([]byte, 9)
	putUint64(resourceID[0:8], vitaID)
	resourceID[8] = byte(dataType)
	a.Audit.LogCompliance(ic.DAO, owner, util.Uint160{}, "ancora.erasure_requested",
		fmt.Sprintf("vitaID=%d dataType=%d reason=%s", vitaID, dataType, reason), ic.BlockHeight())

	ic.AddNotification(a.Hash, "ErasureRequested", stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
		stackitem.NewBigInteger(big.NewInt(int64(dataType))),
		stackitem.NewByteArray([]byte(reason)),
		stackitem.NewBigInteger(big.NewInt(int64(ic.BlockHeight()))),
	}))

	return stackitem.NewBool(true)
}

func (a *Ancora) confirmErasure(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	dataType := state.DataType(toUint32(args[1]))

	// Verify caller is authorized provider
	caller := ic.VM.GetCallingScriptHash()
	providerCfg := a.getProviderConfig(ic.DAO, dataType, caller)
	if providerCfg == nil || !providerCfg.Active {
		panic(ErrAncoraUnauthorized)
	}

	erasure := a.getErasureInfoInternal(ic.DAO, vitaID, dataType)
	if erasure == nil || erasure.Status != state.ErasurePending {
		panic(ErrAncoraErasureNotPending)
	}

	erasure.Status = state.ErasureConfirmed
	erasure.ProcessedAt = ic.BlockHeight()
	erasure.ConfirmedBy = caller

	if err := a.putErasureInfo(ic.DAO, vitaID, dataType, erasure); err != nil {
		panic(fmt.Errorf("failed to confirm erasure: %w", err))
	}

	// Clear the data root
	rootKey := a.makeRootKey(vitaID, dataType)
	ic.DAO.DeleteStorageItem(a.ID, rootKey)

	// ARCH-005: Audit log erasure confirmation (GDPR Article 17 fulfilled)
	// This audit entry proves we complied with the erasure request
	// Note: The on-chain hash of this audit entry remains as proof of compliance
	a.Audit.LogCompliance(ic.DAO, caller, erasure.RequestedBy, "ancora.erasure_confirmed",
		fmt.Sprintf("vitaID=%d dataType=%d requestedAt=%d", vitaID, dataType, erasure.RequestedAt),
		ic.BlockHeight())

	ic.AddNotification(a.Hash, "ErasureConfirmed", stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
		stackitem.NewBigInteger(big.NewInt(int64(dataType))),
		stackitem.NewByteArray(caller.BytesBE()),
	}))

	return stackitem.NewBool(true)
}

func (a *Ancora) denyErasure(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	dataType := state.DataType(toUint32(args[1]))
	reason := toString(args[2])

	// Check grace period
	erasure := a.getErasureInfoInternal(ic.DAO, vitaID, dataType)
	if erasure == nil || erasure.Status != state.ErasurePending {
		panic(ErrAncoraErasureNotPending)
	}

	cache := ic.DAO.GetROCache(a.ID).(*AncoraCache)
	if ic.BlockHeight() < erasure.RequestedAt+cache.config.ErasureGracePeriod {
		panic(ErrAncoraErasureGracePeriod)
	}

	// Verify caller is authorized provider or committee
	caller := ic.VM.GetCallingScriptHash()
	providerCfg := a.getProviderConfig(ic.DAO, dataType, caller)
	if (providerCfg == nil || !providerCfg.Active) && !a.Tutus.CheckCommittee(ic) {
		panic(ErrAncoraUnauthorized)
	}

	erasure.Status = state.ErasureDenied
	erasure.ProcessedAt = ic.BlockHeight()
	erasure.ConfirmedBy = caller
	erasure.DeniedReason = reason

	if err := a.putErasureInfo(ic.DAO, vitaID, dataType, erasure); err != nil {
		panic(fmt.Errorf("failed to deny erasure: %w", err))
	}

	// ARCH-005: Audit log erasure denial (GDPR Article 17 exception invoked)
	// Critical for legal compliance - documents the legal basis for denial
	a.Audit.LogCompliance(ic.DAO, caller, erasure.RequestedBy, "ancora.erasure_denied",
		fmt.Sprintf("vitaID=%d dataType=%d reason=%s", vitaID, dataType, reason),
		ic.BlockHeight())

	ic.AddNotification(a.Hash, "ErasureDenied", stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
		stackitem.NewBigInteger(big.NewInt(int64(dataType))),
		stackitem.NewByteArray([]byte(reason)),
	}))

	return stackitem.NewBool(true)
}

func (a *Ancora) getErasureInfo(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	dataType := state.DataType(toUint32(args[1]))

	erasure := a.getErasureInfoFromDAO(ic.DAO, vitaID, dataType)
	if erasure == nil {
		return stackitem.Null{}
	}
	item, err := erasure.ToStackItem()
	if err != nil {
		panic(fmt.Errorf("failed to convert erasure to stack item: %w", err))
	}
	return item
}

func (a *Ancora) getErasureInfoFromDAO(d *dao.Simple, vitaID uint64, dataType state.DataType) *state.ErasureInfo {
	return a.getErasureInfoInternal(d, vitaID, dataType)
}

func (a *Ancora) getErasureInfoInternal(d *dao.Simple, vitaID uint64, dataType state.DataType) *state.ErasureInfo {
	key := a.makeErasureKey(vitaID, dataType)
	erasure := new(state.ErasureInfo)
	err := getConvertibleFromDAO(a.ID, d, key, erasure)
	if err != nil {
		if errors.Is(err, storage.ErrKeyNotFound) {
			return nil
		}
		panic(fmt.Errorf("failed to get erasure info: %w", err))
	}
	return erasure
}

func (a *Ancora) putErasureInfo(d *dao.Simple, vitaID uint64, dataType state.DataType, erasure *state.ErasureInfo) error {
	key := a.makeErasureKey(vitaID, dataType)
	return putConvertibleToDAO(a.ID, d, key, erasure)
}

func (a *Ancora) makeErasureKey(vitaID uint64, dataType state.DataType) []byte {
	key := make([]byte, 1+8+1)
	key[0] = ancoraErasurePrefix
	putUint64(key[1:9], vitaID)
	key[9] = byte(dataType)
	return key
}

// ========== Data Portability ==========

func (a *Ancora) generatePortabilityAttestation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	dataTypesArr, ok := args[1].Value().([]stackitem.Item)
	if !ok {
		panic("invalid dataTypes array")
	}

	// Verify caller is Vita owner
	owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
	ok2, err := runtime.CheckHashedWitness(ic, owner)
	if err != nil || !ok2 {
		panic(ErrAncoraUnauthorized)
	}

	// Build attestation data
	attestData := make([]byte, 0, 8+4+32*len(dataTypesArr))
	attestData = appendUint64(attestData, vitaID)
	attestData = appendUint32(attestData, ic.BlockHeight())

	for _, item := range dataTypesArr {
		dataType := state.DataType(toUint32(item))
		rootInfo := a.getRootInfo(ic.DAO, vitaID, dataType)
		if rootInfo != nil {
			attestData = append(attestData, byte(dataType))
			attestData = append(attestData, rootInfo.Root...)
		}
	}

	// Hash the attestation
	attestHash := hash.Sha256(attestData).BytesBE()

	// Store attestation with expiry
	cache := ic.DAO.GetROCache(a.ID).(*AncoraCache)
	expiryBlock := ic.BlockHeight() + cache.config.AttestationValidBlocks

	key := append([]byte{ancoraAttestationPrefix}, attestHash...)
	value := make([]byte, 4)
	putUint32(value, expiryBlock)
	ic.DAO.PutStorageItem(a.ID, key, value)

	ic.AddNotification(a.Hash, "PortabilityAttestationGenerated", stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
		stackitem.NewByteArray(attestHash),
		stackitem.NewBigInteger(big.NewInt(int64(expiryBlock))),
	}))

	return stackitem.NewByteArray(attestData)
}

func (a *Ancora) verifyPortabilityAttestation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	attestation, err := args[0].TryBytes()
	if err != nil {
		panic(ErrAncoraInvalidAttestation)
	}

	attestHash := hash.Sha256(attestation).BytesBE()
	key := append([]byte{ancoraAttestationPrefix}, attestHash...)

	value := ic.DAO.GetStorageItem(a.ID, key)
	if value == nil {
		return stackitem.NewBool(false)
	}

	expiryBlock := getUint32(value)
	if ic.BlockHeight() > expiryBlock {
		return stackitem.NewBool(false)
	}

	return stackitem.NewBool(true)
}

// ========== Audit Log Query ==========

// getAuditLog retrieves audit log entries for a specific Vita or globally.
// This provides GDPR-compliant transparency: citizens can see who accessed their data.
func (a *Ancora) getAuditLog(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])
	startBlock := toUint32(args[1])
	endBlock := toUint32(args[2])
	limit := int(toUint32(args[3]))

	if limit <= 0 || limit > 100 {
		limit = 100 // Cap at 100 entries per query
	}

	// If vitaID is specified, verify caller is the owner or committee
	if vitaID > 0 {
		owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
		caller := ic.VM.GetCallingScriptHash()

		// Check if caller is the Vita owner
		ok, err := runtime.CheckHashedWitness(ic, owner)
		if err != nil || !ok {
			// Not the owner - check if committee
			if !a.Tutus.CheckCommittee(ic) {
				// Check if caller is an authorized provider for this data
				isProvider := false
				for dt := state.DataType(0); dt <= state.DataTypeCustom; dt++ {
					if cfg := a.getProviderConfig(ic.DAO, dt, caller); cfg != nil && cfg.Active {
						isProvider = true
						break
					}
				}
				if !isProvider {
					panic(ErrAncoraUnauthorized)
				}
			}
		}
	} else {
		// Global audit query - requires committee
		if !a.Tutus.CheckCommittee(ic) {
			panic(ErrAncoraUnauthorized)
		}
	}

	// Build query
	query := &AuditQuery{
		StartBlock: startBlock,
		EndBlock:   endBlock,
		Limit:      limit,
	}

	// If vitaID specified, query by target (owner of Vita)
	if vitaID > 0 {
		owner := a.Vita.OwnerOfInternal(ic.DAO, vitaID)
		query.Target = &owner
	}

	// Query the audit log
	entries := a.Audit.Query(ic.DAO, query)

	// Convert entries to stack items
	items := make([]stackitem.Item, 0, len(entries))
	for _, entry := range entries {
		items = append(items, stackitem.NewArray([]stackitem.Item{
			stackitem.NewBigInteger(big.NewInt(int64(entry.EntryID))),
			stackitem.NewBigInteger(big.NewInt(int64(entry.Timestamp))),
			stackitem.NewBigInteger(big.NewInt(int64(entry.Category))),
			stackitem.NewBigInteger(big.NewInt(int64(entry.Severity))),
			stackitem.NewBigInteger(big.NewInt(int64(entry.Outcome))),
			stackitem.NewByteArray(entry.Actor.BytesBE()),
			stackitem.NewByteArray(entry.Target.BytesBE()),
			stackitem.NewByteArray([]byte(entry.Action)),
			stackitem.NewByteArray(entry.ResourceID),
			stackitem.NewByteArray([]byte(entry.Details)),
		}))
	}

	return stackitem.NewArray(items)
}

// ========== Rate Limiting Helpers ==========

func (a *Ancora) checkRateLimit(ic *interop.Context, cfg *state.ProviderConfig, vitaID uint64, dataType state.DataType) error {
	// Check per-block rate limit
	blockHeight := ic.BlockHeight()
	updateCount := a.getUpdateCount(ic.DAO, blockHeight, cfg.Provider)
	if updateCount >= cfg.MaxUpdatesPerBlock {
		return ErrAncoraRateLimited
	}

	// Check per-vitaID cooldown
	lastUpdate := a.getLastUpdate(ic.DAO, vitaID, dataType, cfg.Provider)
	if lastUpdate > 0 && blockHeight < lastUpdate+cfg.UpdateCooldown {
		return ErrAncoraUpdateCooldown
	}

	return nil
}

func (a *Ancora) recordUpdate(d *dao.Simple, blockHeight uint32, provider util.Uint160, vitaID uint64, dataType state.DataType) {
	// Increment block update count
	countKey := append([]byte{ancoraUpdateCountPrefix}, provider.BytesBE()...)
	countKey = appendUint32(countKey, blockHeight)

	var count uint32
	data := d.GetStorageItem(a.ID, countKey)
	if data != nil && len(data) >= 4 {
		count = getUint32(data)
	}
	count++
	countData := make([]byte, 4)
	putUint32(countData, count)
	d.PutStorageItem(a.ID, countKey, countData)

	// Record last update for vitaID+dataType+provider
	lastKey := a.makeLastUpdateKey(vitaID, dataType, provider)
	lastData := make([]byte, 4)
	putUint32(lastData, blockHeight)
	d.PutStorageItem(a.ID, lastKey, lastData)
}

func (a *Ancora) getUpdateCount(d *dao.Simple, blockHeight uint32, provider util.Uint160) uint32 {
	countKey := append([]byte{ancoraUpdateCountPrefix}, provider.BytesBE()...)
	countKey = appendUint32(countKey, blockHeight)

	data := d.GetStorageItem(a.ID, countKey)
	if data == nil || len(data) < 4 {
		return 0
	}
	return getUint32(data)
}

func (a *Ancora) getLastUpdate(d *dao.Simple, vitaID uint64, dataType state.DataType, provider util.Uint160) uint32 {
	key := a.makeLastUpdateKey(vitaID, dataType, provider)
	data := d.GetStorageItem(a.ID, key)
	if data == nil || len(data) < 4 {
		return 0
	}
	return getUint32(data)
}

func (a *Ancora) makeLastUpdateKey(vitaID uint64, dataType state.DataType, provider util.Uint160) []byte {
	key := make([]byte, 1+8+1+20)
	key[0] = ancoraLastUpdatePrefix
	putUint64(key[1:9], vitaID)
	key[9] = byte(dataType)
	copy(key[10:30], provider.BytesBE())
	return key
}

// ========== Utility Helpers ==========

func (a *Ancora) getCache(d *dao.Simple) *AncoraCache {
	if d == nil {
		return nil
	}
	cache := d.GetROCache(a.ID)
	if cache == nil {
		return nil
	}
	return cache.(*AncoraCache)
}

func putUint32(b []byte, v uint32) {
	b[0] = byte(v)
	b[1] = byte(v >> 8)
	b[2] = byte(v >> 16)
	b[3] = byte(v >> 24)
}

func getUint32(b []byte) uint32 {
	return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}

func putUint64(b []byte, v uint64) {
	b[0] = byte(v)
	b[1] = byte(v >> 8)
	b[2] = byte(v >> 16)
	b[3] = byte(v >> 24)
	b[4] = byte(v >> 32)
	b[5] = byte(v >> 40)
	b[6] = byte(v >> 48)
	b[7] = byte(v >> 56)
}

func appendUint32(b []byte, v uint32) []byte {
	return append(b, byte(v), byte(v>>8), byte(v>>16), byte(v>>24))
}

func appendUint64(b []byte, v uint64) []byte {
	return append(b, byte(v), byte(v>>8), byte(v>>16), byte(v>>24),
		byte(v>>32), byte(v>>40), byte(v>>48), byte(v>>56))
}
