package native

import (
	"encoding/binary"
	"errors"
	"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/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"
)

// Palam represents the programmed transparency native contract.
// Palam (Latin for "openly/publicly") provides role-based encrypted
// transaction flows with judicial declassification.
type Palam struct {
	interop.ContractMD
	Tutus ITutus
	Vita         IVita
	RoleRegistry IRoleRegistry
	Lex          ILex
}

// PalamCache represents the cached state for Palam contract.
type PalamCache struct {
	flowCount       uint64
	requestCount    uint64
	logCount        uint64
	attachmentCount uint64
}

// Storage key prefixes for Palam.
const (
	palamPrefixFlow               byte = 0x01 // flowID -> Flow
	palamPrefixFlowByBucket       byte = 0x02 // bucket + flowID -> exists
	palamPrefixFlowByParticipant  byte = 0x03 // participant + flowID -> exists
	palamPrefixFlowByTag          byte = 0x04 // tag + flowID -> exists
	palamPrefixRequest            byte = 0x10 // requestID -> DeclassifyRequest
	palamPrefixRequestByFlow      byte = 0x11 // flowID + requestID -> exists
	palamPrefixRequestByRequester byte = 0x12 // requester + requestID -> exists
	palamPrefixPendingRequest     byte = 0x13 // flowID -> pending requestID
	palamPrefixAccessLog          byte = 0x20 // logID -> AccessLog
	palamPrefixLogByFlow          byte = 0x21 // flowID + logID -> exists
	palamPrefixLogByAccessor      byte = 0x22 // accessor + logID -> exists
	palamPrefixAttachment         byte = 0x30 // attachmentID -> FlowAttachment
	palamPrefixAttachmentByFlow   byte = 0x31 // flowID + attachmentID -> exists
	palamPrefixFlowCounter        byte = 0xF0 // -> uint64
	palamPrefixRequestCounter     byte = 0xF1 // -> next request ID
	palamPrefixLogCounter         byte = 0xF2 // -> next log ID
	palamPrefixAttachmentCounter  byte = 0xF3 // -> next attachment ID
	palamPrefixConfig             byte = 0xFF // -> PalamConfig
)

// Event names for Palam.
const (
	FlowRecordedEvent        = "FlowRecorded"
	FlowAttachmentEvent      = "FlowAttachment"
	FlowAccessedEvent        = "FlowAccessed"
	DeclassifyRequestedEvent = "DeclassifyRequested"
	DeclassifyApprovalEvent  = "DeclassifyApproval"
	DeclassifyGrantedEvent   = "DeclassifyGranted"
	DeclassifyDeniedEvent    = "DeclassifyDenied"
	DeclassifyExpiredEvent   = "DeclassifyExpired"
)

// Role constants for Palam (from RoleRegistry).
const (
	RolePalamAuditor uint64 = 25 // Can request declassification
	RolePalamJudge   uint64 = 26 // Can approve declassification
)

// Various errors for Palam.
var (
	ErrPalamFlowNotFound        = errors.New("flow not found")
	ErrPalamFlowExists          = errors.New("flow already exists")
	ErrPalamRequestNotFound     = errors.New("declassify request not found")
	ErrPalamRequestExists       = errors.New("declassify request already exists")
	ErrPalamNotParticipant      = errors.New("caller is not a participant")
	ErrPalamNotAuditor          = errors.New("caller is not an auditor")
	ErrPalamNotJudge            = errors.New("caller is not a judge")
	ErrPalamAlreadyApproved     = errors.New("already approved this request")
	ErrPalamRequestExpired      = errors.New("request has expired")
	ErrPalamRequestNotPending   = errors.New("request is not pending")
	ErrPalamRequestDenied       = errors.New("request was denied")
	ErrPalamNoVita              = errors.New("caller must have an active Vita")
	ErrPalamInvalidTag          = errors.New("invalid tag")
	ErrPalamInvalidBucket       = errors.New("invalid bucket")
	ErrPalamInvalidCaseID       = errors.New("invalid case ID")
	ErrPalamInvalidReason       = errors.New("invalid reason (minimum 50 characters)")
	ErrPalamCannotSelfApprove   = errors.New("cannot approve own request")
	ErrPalamNoPermission        = errors.New("no permission for this action")
	ErrPalamInvalidAttachment   = errors.New("invalid attachment type")
	ErrPalamAttachmentNotFound  = errors.New("attachment not found")
	ErrPalamNotCommittee        = errors.New("invalid committee signature")
)

// NewPalam creates a new Palam native contract.
func NewPalam() *Palam {
	p := &Palam{}

	p.ContractMD = *interop.NewContractMD(nativenames.Palam, nativeids.Palam)
	defer p.BuildHFSpecificMD(p.ActiveIn())

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

	// ===== Flow Recording =====

	desc = NewDescriptor("recordFlow", smartcontract.Hash256Type,
		manifest.NewParameter("tag", smartcontract.StringType),
		manifest.NewParameter("amount", smartcontract.IntegerType),
		manifest.NewParameter("bucket", smartcontract.StringType),
		manifest.NewParameter("participants", smartcontract.ArrayType),
		manifest.NewParameter("consumerData", smartcontract.ByteArrayType),
		manifest.NewParameter("merchantData", smartcontract.ByteArrayType),
		manifest.NewParameter("distributorData", smartcontract.ByteArrayType),
		manifest.NewParameter("producerData", smartcontract.ByteArrayType),
		manifest.NewParameter("ngoData", smartcontract.ByteArrayType),
		manifest.NewParameter("auditorData", smartcontract.ByteArrayType),
		manifest.NewParameter("previousFlowID", smartcontract.Hash256Type))
	md = NewMethodAndPrice(p.recordFlow, 1<<17, callflag.States|callflag.AllowNotify)
	p.AddMethod(md, desc)

	desc = NewDescriptor("attachToFlow", smartcontract.IntegerType,
		manifest.NewParameter("flowID", smartcontract.Hash256Type),
		manifest.NewParameter("attachmentType", smartcontract.StringType),
		manifest.NewParameter("encryptedData", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(p.attachToFlow, 1<<16, callflag.States|callflag.AllowNotify)
	p.AddMethod(md, desc)

	// ===== Flow Queries =====

	desc = NewDescriptor("getFlow", smartcontract.ArrayType,
		manifest.NewParameter("flowID", smartcontract.Hash256Type))
	md = NewMethodAndPrice(p.getFlow, 1<<15, callflag.ReadStates|callflag.AllowNotify)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getTotalFlows", smartcontract.IntegerType)
	md = NewMethodAndPrice(p.getTotalFlows, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getAttachment", smartcontract.ArrayType,
		manifest.NewParameter("attachmentID", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.getAttachment, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getTotalAttachments", smartcontract.IntegerType)
	md = NewMethodAndPrice(p.getTotalAttachments, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	// ===== Declassification =====

	desc = NewDescriptor("requestDeclassify", smartcontract.IntegerType,
		manifest.NewParameter("flowID", smartcontract.Hash256Type),
		manifest.NewParameter("caseID", smartcontract.StringType),
		manifest.NewParameter("reason", smartcontract.StringType),
		manifest.NewParameter("expirationBlocks", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.requestDeclassify, 1<<17, callflag.States|callflag.AllowNotify)
	p.AddMethod(md, desc)

	desc = NewDescriptor("approveDeclassify", smartcontract.BoolType,
		manifest.NewParameter("requestID", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.approveDeclassify, 1<<16, callflag.States|callflag.AllowNotify)
	p.AddMethod(md, desc)

	desc = NewDescriptor("denyDeclassify", smartcontract.BoolType,
		manifest.NewParameter("requestID", smartcontract.IntegerType),
		manifest.NewParameter("reason", smartcontract.StringType))
	md = NewMethodAndPrice(p.denyDeclassify, 1<<16, callflag.States|callflag.AllowNotify)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getDeclassifyRequest", smartcontract.ArrayType,
		manifest.NewParameter("requestID", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.getDeclassifyRequest, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getTotalRequests", smartcontract.IntegerType)
	md = NewMethodAndPrice(p.getTotalRequests, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	desc = NewDescriptor("hasDeclassifyGrant", smartcontract.BoolType,
		manifest.NewParameter("flowID", smartcontract.Hash256Type),
		manifest.NewParameter("requester", smartcontract.Hash160Type))
	md = NewMethodAndPrice(p.hasDeclassifyGrant, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	// ===== Access Logging =====

	desc = NewDescriptor("getAccessLog", smartcontract.ArrayType,
		manifest.NewParameter("logID", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.getAccessLog, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	desc = NewDescriptor("getTotalLogs", smartcontract.IntegerType)
	md = NewMethodAndPrice(p.getTotalLogs, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	// ===== Role & Permissions =====

	desc = NewDescriptor("getRolePermissions", smartcontract.ArrayType,
		manifest.NewParameter("role", smartcontract.IntegerType))
	md = NewMethodAndPrice(p.getRolePermissions, 1<<15, callflag.ReadStates)
	p.AddMethod(md, desc)

	return p
}

// Metadata returns the metadata of the contract.
func (p *Palam) Metadata() *interop.ContractMD {
	return &p.ContractMD
}

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

	// Initialize counters
	p.setFlowCounter(ic.DAO, 0)
	p.setRequestCounter(ic.DAO, 0)
	p.setLogCounter(ic.DAO, 0)
	p.setAttachmentCounter(ic.DAO, 0)

	// Set default config
	cfg := state.DefaultPalamConfig()
	cfgItem := cfg.ToStackItem()
	cfgBytes, err := ic.DAO.GetItemCtx().Serialize(cfgItem, false)
	if err != nil {
		return err
	}
	ic.DAO.PutStorageItem(p.ID, []byte{palamPrefixConfig}, cfgBytes)

	return nil
}

// InitializeCache initializes the contract cache.
func (p *Palam) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
	return nil // No cache needed - use storage directly
}

// ActiveIn returns the hardfork at which the contract becomes active.
func (p *Palam) ActiveIn() *config.Hardfork {
	return nil // Always active
}

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

// GetFlowInternal returns a flow by ID (for cross-contract use).
func (p *Palam) GetFlowInternal(d *dao.Simple, flowID util.Uint256) *state.Flow {
	return p.getFlowInternal(d, flowID)
}

// HasDeclassifyGrant checks if requester has declassify grant for a flow (for cross-contract use).
func (p *Palam) HasDeclassifyGrant(d *dao.Simple, flowID util.Uint256, requester util.Uint160) bool {
	return p.hasGrantInternal(d, flowID, requester)
}

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

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

// ===== Configuration =====

func (p *Palam) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	cfg := p.getConfigInternal(ic.DAO)
	return cfg.ToStackItem()
}

func (p *Palam) getConfigInternal(d *dao.Simple) *state.PalamConfig {
	si := d.GetStorageItem(p.ID, []byte{palamPrefixConfig})
	if si == nil {
		return state.DefaultPalamConfig()
	}

	item, err := stackitem.Deserialize(si)
	if err != nil {
		return state.DefaultPalamConfig()
	}

	cfg := &state.PalamConfig{}
	if err := cfg.FromStackItem(item); err != nil {
		return state.DefaultPalamConfig()
	}
	return cfg
}

// ===== Counter Helpers =====

func (p *Palam) getFlowCounter(d *dao.Simple) uint64 {
	si := d.GetStorageItem(p.ID, []byte{palamPrefixFlowCounter})
	if si == nil || len(si) == 0 {
		return 0
	}
	return binary.LittleEndian.Uint64(si)
}

func (p *Palam) setFlowCounter(d *dao.Simple, count uint64) {
	data := make([]byte, 8)
	binary.LittleEndian.PutUint64(data, count)
	d.PutStorageItem(p.ID, []byte{palamPrefixFlowCounter}, data)
}

func (p *Palam) getRequestCounter(d *dao.Simple) uint64 {
	si := d.GetStorageItem(p.ID, []byte{palamPrefixRequestCounter})
	if si == nil || len(si) == 0 {
		return 0
	}
	return binary.LittleEndian.Uint64(si)
}

func (p *Palam) setRequestCounter(d *dao.Simple, count uint64) {
	data := make([]byte, 8)
	binary.LittleEndian.PutUint64(data, count)
	d.PutStorageItem(p.ID, []byte{palamPrefixRequestCounter}, data)
}

func (p *Palam) getNextRequestID(d *dao.Simple) uint64 {
	id := p.getRequestCounter(d) + 1
	p.setRequestCounter(d, id)
	return id
}

func (p *Palam) getLogCounter(d *dao.Simple) uint64 {
	si := d.GetStorageItem(p.ID, []byte{palamPrefixLogCounter})
	if si == nil || len(si) == 0 {
		return 0
	}
	return binary.LittleEndian.Uint64(si)
}

func (p *Palam) setLogCounter(d *dao.Simple, count uint64) {
	data := make([]byte, 8)
	binary.LittleEndian.PutUint64(data, count)
	d.PutStorageItem(p.ID, []byte{palamPrefixLogCounter}, data)
}

func (p *Palam) getNextLogID(d *dao.Simple) uint64 {
	id := p.getLogCounter(d) + 1
	p.setLogCounter(d, id)
	return id
}

func (p *Palam) getAttachmentCounter(d *dao.Simple) uint64 {
	si := d.GetStorageItem(p.ID, []byte{palamPrefixAttachmentCounter})
	if si == nil || len(si) == 0 {
		return 0
	}
	return binary.LittleEndian.Uint64(si)
}

func (p *Palam) setAttachmentCounter(d *dao.Simple, count uint64) {
	data := make([]byte, 8)
	binary.LittleEndian.PutUint64(data, count)
	d.PutStorageItem(p.ID, []byte{palamPrefixAttachmentCounter}, data)
}

func (p *Palam) getNextAttachmentID(d *dao.Simple) uint64 {
	id := p.getAttachmentCounter(d) + 1
	p.setAttachmentCounter(d, id)
	return id
}

// ===== Flow Recording =====

func (p *Palam) recordFlow(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	tag, err := stackitem.ToString(args[0])
	if err != nil || len(tag) == 0 || len(tag) > 64 {
		panic(ErrPalamInvalidTag)
	}

	amountBI, err := args[1].TryInteger()
	if err != nil {
		panic(err)
	}
	amount := amountBI.Uint64()

	bucket, err := stackitem.ToString(args[2])
	if err != nil || len(bucket) == 0 || len(bucket) > 32 {
		panic(ErrPalamInvalidBucket)
	}

	participantsArr, ok := args[3].Value().([]stackitem.Item)
	if !ok {
		panic("invalid participants array")
	}
	participants := make([]util.Uint160, len(participantsArr))
	for i, p := range participantsArr {
		pBytes, err := p.TryBytes()
		if err != nil {
			panic(err)
		}
		participants[i], err = util.Uint160DecodeBytesBE(pBytes)
		if err != nil {
			panic(err)
		}
	}

	consumerData, err := args[4].TryBytes()
	if err != nil {
		panic(err)
	}
	merchantData, err := args[5].TryBytes()
	if err != nil {
		panic(err)
	}
	distributorData, err := args[6].TryBytes()
	if err != nil {
		panic(err)
	}
	producerData, err := args[7].TryBytes()
	if err != nil {
		panic(err)
	}
	ngoData, err := args[8].TryBytes()
	if err != nil {
		panic(err)
	}
	auditorData, err := args[9].TryBytes()
	if err != nil {
		panic(err)
	}

	prevFlowBytes, err := args[10].TryBytes()
	if err != nil {
		panic(err)
	}
	var previousFlowID util.Uint256
	if len(prevFlowBytes) == 32 {
		previousFlowID, err = util.Uint256DecodeBytesBE(prevFlowBytes)
		if err != nil {
			panic(err)
		}
	}

	caller := ic.VM.GetCallingScriptHash()

	// Verify caller has Vita
	if p.Vita != nil && !p.Vita.TokenExists(ic.DAO, caller) {
		panic(ErrPalamNoVita)
	}

	// Generate flow ID from content hash
	flowData := append([]byte(tag), []byte(bucket)...)
	flowData = append(flowData, caller.BytesBE()...)
	flowData = append(flowData, consumerData...)
	flowData = append(flowData, merchantData...)
	blockBytes := make([]byte, 4)
	binary.LittleEndian.PutUint32(blockBytes, ic.Block.Index)
	flowData = append(flowData, blockBytes...)
	flowID := hash.Sha256(flowData)

	// Check if flow already exists
	if p.getFlowInternal(ic.DAO, flowID) != nil {
		panic(ErrPalamFlowExists)
	}

	flow := &state.Flow{
		FlowID:          flowID,
		Bucket:          bucket,
		Tag:             tag,
		Amount:          amount,
		Timestamp:       ic.Block.Index,
		Creator:         caller,
		ConsumerData:    consumerData,
		MerchantData:    merchantData,
		DistributorData: distributorData,
		ProducerData:    producerData,
		NGOData:         ngoData,
		AuditorData:     auditorData,
		Participants:    participants,
		PreviousFlowID:  previousFlowID,
		Status:          state.FlowStatusActive,
	}

	// Store flow
	p.putFlow(ic.DAO, flow)

	// Update counter
	count := p.getFlowCounter(ic.DAO) + 1
	p.setFlowCounter(ic.DAO, count)

	// Emit event
	ic.AddNotification(p.Hash, FlowRecordedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(flowID.BytesBE()),
		stackitem.NewByteArray([]byte(tag)),
		stackitem.NewByteArray([]byte(bucket)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ic.Block.Index))),
	}))

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

func (p *Palam) putFlow(d *dao.Simple, flow *state.Flow) {
	flowItem := flow.ToStackItem()
	flowBytes, err := d.GetItemCtx().Serialize(flowItem, false)
	if err != nil {
		panic(err)
	}

	// Store main record
	key := append([]byte{palamPrefixFlow}, flow.FlowID.BytesBE()...)
	d.PutStorageItem(p.ID, key, flowBytes)

	// Index by bucket
	bucketKey := append([]byte{palamPrefixFlowByBucket}, []byte(flow.Bucket)...)
	bucketKey = append(bucketKey, flow.FlowID.BytesBE()...)
	d.PutStorageItem(p.ID, bucketKey, []byte{1})

	// Index by tag
	tagKey := append([]byte{palamPrefixFlowByTag}, []byte(flow.Tag)...)
	tagKey = append(tagKey, flow.FlowID.BytesBE()...)
	d.PutStorageItem(p.ID, tagKey, []byte{1})

	// Index by participants
	for _, participant := range flow.Participants {
		partKey := append([]byte{palamPrefixFlowByParticipant}, participant.BytesBE()...)
		partKey = append(partKey, flow.FlowID.BytesBE()...)
		d.PutStorageItem(p.ID, partKey, []byte{1})
	}
}

func (p *Palam) getFlowInternal(d *dao.Simple, flowID util.Uint256) *state.Flow {
	key := append([]byte{palamPrefixFlow}, flowID.BytesBE()...)
	si := d.GetStorageItem(p.ID, key)
	if si == nil {
		return nil
	}

	item, err := stackitem.Deserialize(si)
	if err != nil {
		return nil
	}

	flow := &state.Flow{}
	if err := flow.FromStackItem(item); err != nil {
		return nil
	}
	return flow
}

func (p *Palam) getFlow(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	flowIDBytes, err := args[0].TryBytes()
	if err != nil {
		panic(err)
	}
	flowID, err := util.Uint256DecodeBytesBE(flowIDBytes)
	if err != nil {
		panic(err)
	}

	flow := p.getFlowInternal(ic.DAO, flowID)
	if flow == nil {
		return stackitem.Null{}
	}

	// Log access if configured
	cfg := p.getConfigInternal(ic.DAO)
	if cfg.LogAllAccess {
		caller := ic.VM.GetCallingScriptHash()
		p.logAccess(ic, flowID, caller, state.AccessTypeView, "getFlow")
	}

	return flow.ToStackItem()
}

func (p *Palam) getTotalFlows(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getFlowCounter(ic.DAO)))
}

// ===== Attachments =====

func (p *Palam) attachToFlow(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	flowIDBytes, err := args[0].TryBytes()
	if err != nil {
		panic(err)
	}
	flowID, err := util.Uint256DecodeBytesBE(flowIDBytes)
	if err != nil {
		panic(err)
	}

	attachmentType, err := stackitem.ToString(args[1])
	if err != nil || len(attachmentType) == 0 || len(attachmentType) > 64 {
		panic(ErrPalamInvalidAttachment)
	}

	encryptedData, err := args[2].TryBytes()
	if err != nil {
		panic(err)
	}

	caller := ic.VM.GetCallingScriptHash()

	// Verify flow exists
	flow := p.getFlowInternal(ic.DAO, flowID)
	if flow == nil {
		panic(ErrPalamFlowNotFound)
	}

	// Verify caller is a participant
	isParticipant := false
	for _, p := range flow.Participants {
		if p.Equals(caller) {
			isParticipant = true
			break
		}
	}
	if !isParticipant && !flow.Creator.Equals(caller) {
		panic(ErrPalamNotParticipant)
	}

	attachmentID := p.getNextAttachmentID(ic.DAO)

	attachment := &state.FlowAttachment{
		AttachmentID:   attachmentID,
		FlowID:         flowID,
		AttachmentType: attachmentType,
		EncryptedData:  encryptedData,
		Attacher:       caller,
		AttachedAt:     ic.Block.Index,
	}

	p.putAttachment(ic.DAO, attachment)

	// Emit event
	ic.AddNotification(p.Hash, FlowAttachmentEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(flowID.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(attachmentID)),
		stackitem.NewByteArray([]byte(attachmentType)),
	}))

	return stackitem.NewBigInteger(new(big.Int).SetUint64(attachmentID))
}

func (p *Palam) putAttachment(d *dao.Simple, att *state.FlowAttachment) {
	attItem := att.ToStackItem()
	attBytes, err := d.GetItemCtx().Serialize(attItem, false)
	if err != nil {
		panic(err)
	}

	idBytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(idBytes, att.AttachmentID)

	// Store main record
	key := append([]byte{palamPrefixAttachment}, idBytes...)
	d.PutStorageItem(p.ID, key, attBytes)

	// Index by flow
	flowKey := append([]byte{palamPrefixAttachmentByFlow}, att.FlowID.BytesBE()...)
	flowKey = append(flowKey, idBytes...)
	d.PutStorageItem(p.ID, flowKey, []byte{1})
}

func (p *Palam) getAttachmentInternal(d *dao.Simple, attachmentID uint64) *state.FlowAttachment {
	idBytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(idBytes, attachmentID)

	key := append([]byte{palamPrefixAttachment}, idBytes...)
	si := d.GetStorageItem(p.ID, key)
	if si == nil {
		return nil
	}

	item, err := stackitem.Deserialize(si)
	if err != nil {
		return nil
	}

	att := &state.FlowAttachment{}
	if err := att.FromStackItem(item); err != nil {
		return nil
	}
	return att
}

func (p *Palam) getAttachment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	attachmentID, err := args[0].TryInteger()
	if err != nil {
		panic(err)
	}

	att := p.getAttachmentInternal(ic.DAO, attachmentID.Uint64())
	if att == nil {
		return stackitem.Null{}
	}

	return att.ToStackItem()
}

func (p *Palam) getTotalAttachments(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getAttachmentCounter(ic.DAO)))
}

// ===== Declassification =====

func (p *Palam) requestDeclassify(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	flowIDBytes, err := args[0].TryBytes()
	if err != nil {
		panic(err)
	}
	flowID, err := util.Uint256DecodeBytesBE(flowIDBytes)
	if err != nil {
		panic(err)
	}

	caseID, err := stackitem.ToString(args[1])
	if err != nil || len(caseID) == 0 || len(caseID) > 64 {
		panic(ErrPalamInvalidCaseID)
	}

	reason, err := stackitem.ToString(args[2])
	if err != nil || len(reason) < 50 || len(reason) > 1024 {
		panic(ErrPalamInvalidReason)
	}

	expirationBlocks, err := args[3].TryInteger()
	if err != nil {
		panic(err)
	}
	expBlocks := uint32(expirationBlocks.Uint64())

	caller := ic.VM.GetCallingScriptHash()

	// Verify flow exists
	flow := p.getFlowInternal(ic.DAO, flowID)
	if flow == nil {
		panic(ErrPalamFlowNotFound)
	}

	// Verify caller is an auditor
	if p.RoleRegistry != nil {
		if !p.RoleRegistry.HasRoleInternal(ic.DAO, caller, RolePalamAuditor, ic.Block.Index) {
			panic(ErrPalamNotAuditor)
		}
	}

	// Get config for defaults
	cfg := p.getConfigInternal(ic.DAO)
	if expBlocks == 0 {
		expBlocks = cfg.DefaultExpiration
	}

	requestID := p.getNextRequestID(ic.DAO)

	request := &state.DeclassifyRequest{
		RequestID:         requestID,
		FlowID:            flowID,
		CaseID:            caseID,
		Reason:            reason,
		Requester:         caller,
		RequesterRole:     state.PalamRoleAuditor,
		RequiredApprovals: cfg.MinApprovals,
		Approvals:         []util.Uint160{},
		ApprovalTimes:     []uint32{},
		Status:            state.DeclassifyPending,
		CreatedAt:         ic.Block.Index,
		ExpiresAt:         ic.Block.Index + expBlocks,
		GrantedAt:         0,
	}

	p.putDeclassifyRequest(ic.DAO, request)

	// Emit event
	ic.AddNotification(p.Hash, DeclassifyRequestedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(requestID)),
		stackitem.NewByteArray(flowID.BytesBE()),
		stackitem.NewByteArray([]byte(caseID)),
		stackitem.NewByteArray(caller.BytesBE()),
	}))

	return stackitem.NewBigInteger(new(big.Int).SetUint64(requestID))
}

func (p *Palam) putDeclassifyRequest(d *dao.Simple, req *state.DeclassifyRequest) {
	reqItem := req.ToStackItem()
	reqBytes, err := d.GetItemCtx().Serialize(reqItem, false)
	if err != nil {
		panic(err)
	}

	idBytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(idBytes, req.RequestID)

	// Store main record
	key := append([]byte{palamPrefixRequest}, idBytes...)
	d.PutStorageItem(p.ID, key, reqBytes)

	// Index by flow
	flowKey := append([]byte{palamPrefixRequestByFlow}, req.FlowID.BytesBE()...)
	flowKey = append(flowKey, idBytes...)
	d.PutStorageItem(p.ID, flowKey, []byte{1})

	// Index by requester
	reqKey := append([]byte{palamPrefixRequestByRequester}, req.Requester.BytesBE()...)
	reqKey = append(reqKey, idBytes...)
	d.PutStorageItem(p.ID, reqKey, []byte{1})

	// Track pending request for flow
	if req.Status == state.DeclassifyPending {
		pendingKey := append([]byte{palamPrefixPendingRequest}, req.FlowID.BytesBE()...)
		d.PutStorageItem(p.ID, pendingKey, idBytes)
	}
}

func (p *Palam) getRequestInternal(d *dao.Simple, requestID uint64) *state.DeclassifyRequest {
	idBytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(idBytes, requestID)

	key := append([]byte{palamPrefixRequest}, idBytes...)
	si := d.GetStorageItem(p.ID, key)
	if si == nil {
		return nil
	}

	item, err := stackitem.Deserialize(si)
	if err != nil {
		return nil
	}

	req := &state.DeclassifyRequest{}
	if err := req.FromStackItem(item); err != nil {
		return nil
	}
	return req
}

func (p *Palam) approveDeclassify(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	requestID, err := args[0].TryInteger()
	if err != nil {
		panic(err)
	}

	caller := ic.VM.GetCallingScriptHash()

	// Verify caller is a judge
	if p.RoleRegistry != nil {
		if !p.RoleRegistry.HasRoleInternal(ic.DAO, caller, RolePalamJudge, ic.Block.Index) {
			panic(ErrPalamNotJudge)
		}
	}

	req := p.getRequestInternal(ic.DAO, requestID.Uint64())
	if req == nil {
		panic(ErrPalamRequestNotFound)
	}

	// Check request is pending
	if req.Status != state.DeclassifyPending {
		panic(ErrPalamRequestNotPending)
	}

	// Check not expired
	if ic.Block.Index > req.ExpiresAt {
		req.Status = state.DeclassifyExpired
		p.putDeclassifyRequest(ic.DAO, req)
		ic.AddNotification(p.Hash, DeclassifyExpiredEvent, stackitem.NewArray([]stackitem.Item{
			stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)),
		}))
		panic(ErrPalamRequestExpired)
	}

	// Cannot approve own request
	if caller.Equals(req.Requester) {
		panic(ErrPalamCannotSelfApprove)
	}

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

	// Add approval
	req.Approvals = append(req.Approvals, caller)
	req.ApprovalTimes = append(req.ApprovalTimes, ic.Block.Index)

	// Check if enough approvals
	if uint32(len(req.Approvals)) >= req.RequiredApprovals {
		req.Status = state.DeclassifyApproved
		req.GrantedAt = ic.Block.Index

		// Emit granted event
		ic.AddNotification(p.Hash, DeclassifyGrantedEvent, stackitem.NewArray([]stackitem.Item{
			stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)),
			stackitem.NewByteArray(req.FlowID.BytesBE()),
			stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(req.GrantedAt))),
		}))
	}

	p.putDeclassifyRequest(ic.DAO, req)

	// Emit approval event
	ic.AddNotification(p.Hash, DeclassifyApprovalEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)),
		stackitem.NewByteArray(caller.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetInt64(int64(len(req.Approvals)))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(req.RequiredApprovals))),
	}))

	return stackitem.NewBool(true)
}

func (p *Palam) denyDeclassify(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	requestID, err := args[0].TryInteger()
	if err != nil {
		panic(err)
	}

	reason, err := stackitem.ToString(args[1])
	if err != nil {
		panic(err)
	}

	caller := ic.VM.GetCallingScriptHash()

	// Verify caller is a judge
	if p.RoleRegistry != nil {
		if !p.RoleRegistry.HasRoleInternal(ic.DAO, caller, RolePalamJudge, ic.Block.Index) {
			panic(ErrPalamNotJudge)
		}
	}

	req := p.getRequestInternal(ic.DAO, requestID.Uint64())
	if req == nil {
		panic(ErrPalamRequestNotFound)
	}

	if req.Status != state.DeclassifyPending {
		panic(ErrPalamRequestNotPending)
	}

	req.Status = state.DeclassifyDenied
	p.putDeclassifyRequest(ic.DAO, req)

	// Emit denied event
	ic.AddNotification(p.Hash, DeclassifyDeniedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(req.RequestID)),
		stackitem.NewByteArray(caller.BytesBE()),
		stackitem.NewByteArray([]byte(reason)),
	}))

	return stackitem.NewBool(true)
}

func (p *Palam) getDeclassifyRequest(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	requestID, err := args[0].TryInteger()
	if err != nil {
		panic(err)
	}

	req := p.getRequestInternal(ic.DAO, requestID.Uint64())
	if req == nil {
		return stackitem.Null{}
	}

	return req.ToStackItem()
}

func (p *Palam) getTotalRequests(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getRequestCounter(ic.DAO)))
}

func (p *Palam) hasDeclassifyGrant(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	flowIDBytes, err := args[0].TryBytes()
	if err != nil {
		panic(err)
	}
	flowID, err := util.Uint256DecodeBytesBE(flowIDBytes)
	if err != nil {
		panic(err)
	}

	requesterBytes, err := args[1].TryBytes()
	if err != nil {
		panic(err)
	}
	requester, err := util.Uint160DecodeBytesBE(requesterBytes)
	if err != nil {
		panic(err)
	}

	// Search for approved request by this requester for this flow
	return stackitem.NewBool(p.hasGrantInternal(ic.DAO, flowID, requester))
}

func (p *Palam) hasGrantInternal(d *dao.Simple, flowID util.Uint256, requester util.Uint160) bool {
	// Iterate through requests for this flow
	prefix := append([]byte{palamPrefixRequestByFlow}, flowID.BytesBE()...)
	d.Seek(p.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) < 8 {
			return true
		}
		requestID := binary.LittleEndian.Uint64(k[len(k)-8:])
		req := p.getRequestInternal(d, requestID)
		if req != nil && req.Status == state.DeclassifyApproved && req.Requester.Equals(requester) {
			return false // Found a grant
		}
		return true
	})
	// Note: This is a simplification - in production would track this more efficiently
	return false
}

// ===== Access Logging =====

func (p *Palam) logAccess(ic *interop.Context, flowID util.Uint256, accessor util.Uint160, accessType state.AccessType, details string) {
	logID := p.getNextLogID(ic.DAO)

	log := &state.AccessLog{
		LogID:      logID,
		FlowID:     flowID,
		Accessor:   accessor,
		AccessType: accessType,
		Timestamp:  ic.Block.Index,
		Details:    details,
	}

	p.putAccessLog(ic.DAO, log)

	// Emit event
	ic.AddNotification(p.Hash, FlowAccessedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(flowID.BytesBE()),
		stackitem.NewByteArray(accessor.BytesBE()),
		stackitem.NewByteArray([]byte(details)),
	}))
}

func (p *Palam) putAccessLog(d *dao.Simple, log *state.AccessLog) {
	logItem := log.ToStackItem()
	logBytes, err := d.GetItemCtx().Serialize(logItem, false)
	if err != nil {
		panic(err)
	}

	idBytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(idBytes, log.LogID)

	// Store main record
	key := append([]byte{palamPrefixAccessLog}, idBytes...)
	d.PutStorageItem(p.ID, key, logBytes)

	// Index by flow
	flowKey := append([]byte{palamPrefixLogByFlow}, log.FlowID.BytesBE()...)
	flowKey = append(flowKey, idBytes...)
	d.PutStorageItem(p.ID, flowKey, []byte{1})

	// Index by accessor
	accKey := append([]byte{palamPrefixLogByAccessor}, log.Accessor.BytesBE()...)
	accKey = append(accKey, idBytes...)
	d.PutStorageItem(p.ID, accKey, []byte{1})
}

func (p *Palam) getAccessLogInternal(d *dao.Simple, logID uint64) *state.AccessLog {
	idBytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(idBytes, logID)

	key := append([]byte{palamPrefixAccessLog}, idBytes...)
	si := d.GetStorageItem(p.ID, key)
	if si == nil {
		return nil
	}

	item, err := stackitem.Deserialize(si)
	if err != nil {
		return nil
	}

	log := &state.AccessLog{}
	if err := log.FromStackItem(item); err != nil {
		return nil
	}
	return log
}

func (p *Palam) getAccessLog(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	logID, err := args[0].TryInteger()
	if err != nil {
		panic(err)
	}

	log := p.getAccessLogInternal(ic.DAO, logID.Uint64())
	if log == nil {
		return stackitem.Null{}
	}

	return log.ToStackItem()
}

func (p *Palam) getTotalLogs(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	return stackitem.NewBigInteger(new(big.Int).SetUint64(p.getLogCounter(ic.DAO)))
}

// ===== Role Permissions =====

func (p *Palam) getRolePermissions(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	roleInt, err := args[0].TryInteger()
	if err != nil {
		panic(err)
	}
	role := state.PalamRole(roleInt.Uint64())

	permissions := state.DefaultRolePermissions(role)
	return permissions.ToStackItem()
}
