package native

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

// Collocatio represents the Investment native contract for democratic investment (PIO/EIO/CIO).
// Latin: "collocatio" = placement, arrangement (investment)
type Collocatio struct {
	interop.ContractMD
	Tutus ITutus
	Vita         IVita
	RoleRegistry *RoleRegistry
	VTS          *VTS
	Scire        *Scire
	Eligere      *Eligere
	Tribute      *Tribute
}

// Storage prefixes for Collocatio contract.
const (
	collocatioPrefixConfig               byte = 0x01 // -> CollocatioConfig
	collocatioPrefixOpportunity          byte = 0x10 // opportunityID -> InvestmentOpportunity
	collocatioPrefixOpportunityByType    byte = 0x11 // type + opportunityID -> exists
	collocatioPrefixOpportunityByStatus  byte = 0x12 // status + opportunityID -> exists
	collocatioPrefixOppCounter           byte = 0x1F // -> next opportunity ID
	collocatioPrefixInvestment           byte = 0x20 // investmentID -> Investment
	collocatioPrefixInvestmentByOpp      byte = 0x21 // opportunityID + investmentID -> exists
	collocatioPrefixInvestmentByInvestor byte = 0x22 // vitaID + investmentID -> exists
	collocatioPrefixInvCounter           byte = 0x2F // -> next investment ID
	collocatioPrefixEligibility          byte = 0x30 // vitaID -> InvestorEligibility
	collocatioPrefixEligibilityByOwner   byte = 0x31 // owner -> vitaID
	collocatioPrefixViolation            byte = 0x40 // violationID -> InvestmentViolation
	collocatioPrefixViolationByInvestor  byte = 0x41 // vitaID + violationID -> exists
	collocatioPrefixViolationCounter     byte = 0x4F // -> next violation ID
	collocatioPrefixEmployment           byte = 0x50 // employeeVitaID -> EmploymentVerification
	collocatioPrefixEmploymentByEmployer byte = 0x51 // employerVitaID + employeeVitaID -> exists
	collocatioPrefixContractor           byte = 0x60 // contractorVitaID -> ContractorVerification
	collocatioPrefixContractorByPlatform byte = 0x61 // platformHash + contractorVitaID -> exists
	collocatioPrefixCommitment           byte = 0x70 // commitmentID -> InvestmentCommitment
	collocatioPrefixCommitmentByOpp      byte = 0x71 // opportunityID + commitmentID -> exists
	collocatioPrefixCommitmentByInvestor byte = 0x72 // vitaID + commitmentID -> exists
	collocatioPrefixCommitmentCounter    byte = 0x7F // -> next commitment ID
)

// Collocatio events.
const (
	collocatioOpportunityCreatedEvent   = "OpportunityCreated"
	collocatioOpportunityActivatedEvent = "OpportunityActivated"
	collocatioOpportunityClosedEvent    = "OpportunityClosed"
	collocatioOpportunityFailedEvent    = "OpportunityFailed"
	collocatioOpportunityCancelledEvent = "OpportunityCancelled"
	collocatioInvestmentMadeEvent       = "InvestmentMade"
	collocatioInvestmentWithdrawnEvent  = "InvestmentWithdrawn"
	collocatioReturnsDistributedEvent   = "ReturnsDistributed"
	collocatioEligibilityUpdatedEvent   = "EligibilityUpdated"
	collocatioEducationCompletedEvent   = "EducationCompleted"
	collocatioViolationRecordedEvent    = "ViolationRecorded"
	collocatioViolationResolvedEvent    = "ViolationResolved"
	collocatioEmploymentVerifiedEvent   = "EmploymentVerified"
	collocatioEmploymentRevokedEvent    = "EmploymentRevoked"
	collocatioContractorVerifiedEvent   = "ContractorVerified"
	collocatioContractorRevokedEvent    = "ContractorRevoked"
	collocatioCommitmentCreatedEvent    = "CommitmentCreated"
	collocatioCommitmentRevealedEvent   = "CommitmentRevealed"
	collocatioCommitmentCanceledEvent   = "CommitmentCanceled"
)

// RoleInvestmentManager is the role ID for investment management.
const RoleInvestmentManager uint64 = 28

// Default config values.
const (
	defaultMinPIOParticipants    uint64 = 100
	defaultMinEIOParticipants    uint64 = 10
	defaultMinCIOParticipants    uint64 = 25
	defaultMinInvestment         uint64 = 100_00000000 // 100 VTS
	defaultMaxIndividualCap      uint64 = 1_000_000_00000000 // 1M VTS
	defaultWealthConcentration   uint64 = 500  // 5%
	defaultCreationFee           uint64 = 1000_00000000 // 1000 VTS
	defaultInvestmentFee         uint64 = 50   // 0.5%
	defaultWithdrawalPenalty     uint64 = 200  // 2%
	defaultMinVotingPeriod       uint32 = 10000
	defaultMinInvestmentPeriod   uint32 = 20000
	defaultMinMaturityPeriod     uint32 = 50000
	defaultMaxViolationsBeforeBan uint8 = 3
	defaultCommitRevealDelay     uint32 = 10   // Min blocks between commit and reveal
	defaultCommitRevealWindow    uint32 = 1000 // Max blocks to reveal after delay
)

var _ interop.Contract = (*Collocatio)(nil)

func newCollocatio() *Collocatio {
	c := &Collocatio{
		ContractMD: *interop.NewContractMD(nativenames.Collocatio, nativeids.Collocatio),
	}
	defer c.BuildHFSpecificMD(c.ActiveIn())

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

	// getOpportunityCount
	desc = NewDescriptor("getOpportunityCount", smartcontract.IntegerType)
	md = NewMethodAndPrice(c.getOpportunityCount, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// getOpportunity
	desc = NewDescriptor("getOpportunity", smartcontract.ArrayType,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.getOpportunity, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// createOpportunity
	desc = NewDescriptor("createOpportunity", smartcontract.IntegerType,
		manifest.NewParameter("oppType", smartcontract.IntegerType),
		manifest.NewParameter("name", smartcontract.StringType),
		manifest.NewParameter("description", smartcontract.StringType),
		manifest.NewParameter("termsHash", smartcontract.Hash256Type),
		manifest.NewParameter("minParticipants", smartcontract.IntegerType),
		manifest.NewParameter("maxParticipants", smartcontract.IntegerType),
		manifest.NewParameter("minInvestment", smartcontract.IntegerType),
		manifest.NewParameter("maxInvestment", smartcontract.IntegerType),
		manifest.NewParameter("targetPool", smartcontract.IntegerType),
		manifest.NewParameter("expectedReturns", smartcontract.IntegerType),
		manifest.NewParameter("riskLevel", smartcontract.IntegerType),
		manifest.NewParameter("maturityBlocks", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.createOpportunity, 1<<17, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// activateOpportunity
	desc = NewDescriptor("activateOpportunity", smartcontract.BoolType,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.activateOpportunity, 1<<16, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// invest
	desc = NewDescriptor("invest", smartcontract.IntegerType,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType),
		manifest.NewParameter("amount", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.invest, 1<<17, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// withdraw
	desc = NewDescriptor("withdraw", smartcontract.BoolType,
		manifest.NewParameter("investmentID", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.withdraw, 1<<17, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// getEligibility
	desc = NewDescriptor("getEligibility", smartcontract.ArrayType,
		manifest.NewParameter("investor", smartcontract.Hash160Type))
	md = NewMethodAndPrice(c.getEligibility, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// setEligibility
	desc = NewDescriptor("setEligibility", smartcontract.BoolType,
		manifest.NewParameter("investor", smartcontract.Hash160Type),
		manifest.NewParameter("eligibilityFlags", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.setEligibility, 1<<16, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// isEligible
	desc = NewDescriptor("isEligible", smartcontract.BoolType,
		manifest.NewParameter("investor", smartcontract.Hash160Type),
		manifest.NewParameter("oppType", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.isEligible, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// getInvestment
	desc = NewDescriptor("getInvestment", smartcontract.ArrayType,
		manifest.NewParameter("investmentID", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.getInvestment, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// getInvestmentCount
	desc = NewDescriptor("getInvestmentCount", smartcontract.IntegerType)
	md = NewMethodAndPrice(c.getInvestmentCount, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// recordViolation
	desc = NewDescriptor("recordViolation", smartcontract.IntegerType,
		manifest.NewParameter("violator", smartcontract.Hash160Type),
		manifest.NewParameter("opportunityID", smartcontract.IntegerType),
		manifest.NewParameter("violationType", smartcontract.StringType),
		manifest.NewParameter("description", smartcontract.StringType),
		manifest.NewParameter("evidenceHash", smartcontract.Hash256Type),
		manifest.NewParameter("penalty", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.recordViolation, 1<<17, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// resolveViolation
	desc = NewDescriptor("resolveViolation", smartcontract.BoolType,
		manifest.NewParameter("violationID", smartcontract.IntegerType),
		manifest.NewParameter("resolution", smartcontract.StringType))
	md = NewMethodAndPrice(c.resolveViolation, 1<<16, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// getViolation
	desc = NewDescriptor("getViolation", smartcontract.ArrayType,
		manifest.NewParameter("violationID", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.getViolation, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// verifyEmployment
	desc = NewDescriptor("verifyEmployment", smartcontract.BoolType,
		manifest.NewParameter("employee", smartcontract.Hash160Type),
		manifest.NewParameter("employer", smartcontract.Hash160Type),
		manifest.NewParameter("position", smartcontract.StringType),
		manifest.NewParameter("startDate", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.verifyEmployment, 1<<16, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// revokeEmployment
	desc = NewDescriptor("revokeEmployment", smartcontract.BoolType,
		manifest.NewParameter("employee", smartcontract.Hash160Type),
		manifest.NewParameter("employer", smartcontract.Hash160Type),
		manifest.NewParameter("endDate", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.revokeEmployment, 1<<16, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// getEmploymentStatus
	desc = NewDescriptor("getEmploymentStatus", smartcontract.ArrayType,
		manifest.NewParameter("employee", smartcontract.Hash160Type))
	md = NewMethodAndPrice(c.getEmploymentStatus, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// verifyContractor
	desc = NewDescriptor("verifyContractor", smartcontract.BoolType,
		manifest.NewParameter("contractor", smartcontract.Hash160Type),
		manifest.NewParameter("platform", smartcontract.Hash160Type),
		manifest.NewParameter("platformID", smartcontract.StringType),
		manifest.NewParameter("contractorID", smartcontract.StringType),
		manifest.NewParameter("startDate", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.verifyContractor, 1<<16, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// revokeContractor
	desc = NewDescriptor("revokeContractor", smartcontract.BoolType,
		manifest.NewParameter("contractor", smartcontract.Hash160Type),
		manifest.NewParameter("platform", smartcontract.Hash160Type),
		manifest.NewParameter("endDate", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.revokeContractor, 1<<16, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// getContractorStatus
	desc = NewDescriptor("getContractorStatus", smartcontract.ArrayType,
		manifest.NewParameter("contractor", smartcontract.Hash160Type))
	md = NewMethodAndPrice(c.getContractorStatus, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// completeInvestmentEducation
	desc = NewDescriptor("completeInvestmentEducation", smartcontract.BoolType,
		manifest.NewParameter("investor", smartcontract.Hash160Type),
		manifest.NewParameter("certificationID", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.completeInvestmentEducation, 1<<16, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// distributeReturns
	desc = NewDescriptor("distributeReturns", smartcontract.BoolType,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType),
		manifest.NewParameter("actualReturns", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.distributeReturns, 1<<18, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// cancelOpportunity
	desc = NewDescriptor("cancelOpportunity", smartcontract.BoolType,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.cancelOpportunity, 1<<18, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// getInvestmentsByOpportunity
	desc = NewDescriptor("getInvestmentsByOpportunity", smartcontract.ArrayType,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.getInvestmentsByOpportunity, 1<<16, callflag.ReadStates)
	c.AddMethod(md, desc)

	// getInvestmentsByInvestor
	desc = NewDescriptor("getInvestmentsByInvestor", smartcontract.ArrayType,
		manifest.NewParameter("investor", smartcontract.Hash160Type))
	md = NewMethodAndPrice(c.getInvestmentsByInvestor, 1<<16, callflag.ReadStates)
	c.AddMethod(md, desc)

	// getOpportunitiesByType
	desc = NewDescriptor("getOpportunitiesByType", smartcontract.ArrayType,
		manifest.NewParameter("oppType", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.getOpportunitiesByType, 1<<16, callflag.ReadStates)
	c.AddMethod(md, desc)

	// getOpportunitiesByStatus
	desc = NewDescriptor("getOpportunitiesByStatus", smartcontract.ArrayType,
		manifest.NewParameter("status", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.getOpportunitiesByStatus, 1<<16, callflag.ReadStates)
	c.AddMethod(md, desc)

	// commitInvestment - Phase 1 of commit-reveal (anti-front-running)
	desc = NewDescriptor("commitInvestment", smartcontract.IntegerType,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType),
		manifest.NewParameter("commitment", smartcontract.Hash256Type))
	md = NewMethodAndPrice(c.commitInvestment, 1<<16, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// revealInvestment - Phase 2 of commit-reveal
	desc = NewDescriptor("revealInvestment", smartcontract.IntegerType,
		manifest.NewParameter("commitmentID", smartcontract.IntegerType),
		manifest.NewParameter("amount", smartcontract.IntegerType),
		manifest.NewParameter("nonce", smartcontract.ByteArrayType))
	md = NewMethodAndPrice(c.revealInvestment, 1<<17, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// cancelCommitment - Cancel a pending commitment
	desc = NewDescriptor("cancelCommitment", smartcontract.BoolType,
		manifest.NewParameter("commitmentID", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.cancelCommitment, 1<<16, callflag.States|callflag.AllowNotify)
	c.AddMethod(md, desc)

	// getCommitment - Query commitment details
	desc = NewDescriptor("getCommitment", smartcontract.ArrayType,
		manifest.NewParameter("commitmentID", smartcontract.IntegerType))
	md = NewMethodAndPrice(c.getCommitment, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// getCommitmentCount
	desc = NewDescriptor("getCommitmentCount", smartcontract.IntegerType)
	md = NewMethodAndPrice(c.getCommitmentCount, 1<<15, callflag.ReadStates)
	c.AddMethod(md, desc)

	// ===== Events =====
	eDesc := NewEventDescriptor(collocatioOpportunityCreatedEvent,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType),
		manifest.NewParameter("oppType", smartcontract.IntegerType),
		manifest.NewParameter("creator", smartcontract.Hash160Type))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioOpportunityActivatedEvent,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioInvestmentMadeEvent,
		manifest.NewParameter("investmentID", smartcontract.IntegerType),
		manifest.NewParameter("opportunityID", smartcontract.IntegerType),
		manifest.NewParameter("investor", smartcontract.Hash160Type),
		manifest.NewParameter("amount", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioInvestmentWithdrawnEvent,
		manifest.NewParameter("investmentID", smartcontract.IntegerType),
		manifest.NewParameter("returnAmount", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioEligibilityUpdatedEvent,
		manifest.NewParameter("investor", smartcontract.Hash160Type),
		manifest.NewParameter("eligibility", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioOpportunityClosedEvent,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioOpportunityFailedEvent,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType),
		manifest.NewParameter("reason", smartcontract.StringType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioOpportunityCancelledEvent,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioReturnsDistributedEvent,
		manifest.NewParameter("opportunityID", smartcontract.IntegerType),
		manifest.NewParameter("totalReturns", smartcontract.IntegerType),
		manifest.NewParameter("investorCount", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioEducationCompletedEvent,
		manifest.NewParameter("investor", smartcontract.Hash160Type),
		manifest.NewParameter("certificationID", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioViolationRecordedEvent,
		manifest.NewParameter("violationID", smartcontract.IntegerType),
		manifest.NewParameter("violator", smartcontract.Hash160Type),
		manifest.NewParameter("penalty", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioViolationResolvedEvent,
		manifest.NewParameter("violationID", smartcontract.IntegerType),
		manifest.NewParameter("resolution", smartcontract.StringType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioEmploymentVerifiedEvent,
		manifest.NewParameter("employee", smartcontract.Hash160Type),
		manifest.NewParameter("employer", smartcontract.Hash160Type))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioEmploymentRevokedEvent,
		manifest.NewParameter("employee", smartcontract.Hash160Type),
		manifest.NewParameter("employer", smartcontract.Hash160Type))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioContractorVerifiedEvent,
		manifest.NewParameter("contractor", smartcontract.Hash160Type),
		manifest.NewParameter("platform", smartcontract.Hash160Type))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioContractorRevokedEvent,
		manifest.NewParameter("contractor", smartcontract.Hash160Type),
		manifest.NewParameter("platform", smartcontract.Hash160Type))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioCommitmentCreatedEvent,
		manifest.NewParameter("commitmentID", smartcontract.IntegerType),
		manifest.NewParameter("opportunityID", smartcontract.IntegerType),
		manifest.NewParameter("investor", smartcontract.Hash160Type))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioCommitmentRevealedEvent,
		manifest.NewParameter("commitmentID", smartcontract.IntegerType),
		manifest.NewParameter("investmentID", smartcontract.IntegerType),
		manifest.NewParameter("amount", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	eDesc = NewEventDescriptor(collocatioCommitmentCanceledEvent,
		manifest.NewParameter("commitmentID", smartcontract.IntegerType))
	c.AddEvent(NewEvent(eDesc))

	return c
}

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

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

	// Initialize default config
	cfg := state.CollocatioConfig{
		MinPIOParticipants:     defaultMinPIOParticipants,
		MinEIOParticipants:     defaultMinEIOParticipants,
		MinCIOParticipants:     defaultMinCIOParticipants,
		DefaultMinInvestment:   defaultMinInvestment,
		MaxIndividualCap:       defaultMaxIndividualCap,
		WealthConcentration:    defaultWealthConcentration,
		CreationFee:            defaultCreationFee,
		InvestmentFee:          defaultInvestmentFee,
		WithdrawalPenalty:      defaultWithdrawalPenalty,
		MinVotingPeriod:        defaultMinVotingPeriod,
		MinInvestmentPeriod:    defaultMinInvestmentPeriod,
		MinMaturityPeriod:      defaultMinMaturityPeriod,
		MaxViolationsBeforeBan: defaultMaxViolationsBeforeBan,
		ViolationCooldown:      1000000,
		CommitRevealDelay:      defaultCommitRevealDelay,
		CommitRevealWindow:     defaultCommitRevealWindow,
	}
	c.setConfigInternal(ic.DAO, &cfg)

	return nil
}

// InitializeCache fills native Collocatio cache from DAO.
func (c *Collocatio) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
	return nil
}

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

// PostPersist implements the Contract interface.
// Handles lifecycle automation for opportunities.
func (c *Collocatio) PostPersist(ic *interop.Context) error {
	// Run every 100 blocks for performance
	if ic.Block.Index%100 != 0 {
		return nil
	}

	// Process opportunities that need status updates
	c.processActiveOpportunities(ic)
	c.processClosedOpportunities(ic)
	return nil
}

// processActiveOpportunities handles Active opportunities past their investment deadline.
func (c *Collocatio) processActiveOpportunities(ic *interop.Context) {
	prefix := []byte{collocatioPrefixOpportunityByStatus, byte(state.OpportunityActive)}
	cfg := c.getConfigInternal(ic.DAO)

	ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) < 9 {
			return true
		}

		oppID := binary.BigEndian.Uint64(k[1:9])
		opp := c.getOpportunityInternal(ic.DAO, oppID)
		if opp == nil {
			return true
		}

		// Check if investment deadline passed
		if ic.Block.Index < opp.InvestmentDeadline {
			return true
		}

		// Get minimum participants for this opportunity type
		var minParticipants uint64
		switch opp.Type {
		case state.OpportunityPIO:
			minParticipants = cfg.MinPIOParticipants
		case state.OpportunityEIO:
			minParticipants = cfg.MinEIOParticipants
		case state.OpportunityCIO:
			minParticipants = cfg.MinCIOParticipants
		default:
			minParticipants = 1
		}

		// Use opportunity's own min if set
		if opp.MinParticipants > minParticipants {
			minParticipants = opp.MinParticipants
		}

		// Check if opportunity met minimum participants
		if opp.CurrentParticipants < minParticipants {
			// Failed - didn't meet minimum participants
			c.updateOpportunityStatus(ic, opp, state.OpportunityFailed)
			ic.AddNotification(c.Hash, collocatioOpportunityFailedEvent, stackitem.NewArray([]stackitem.Item{
				stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
				stackitem.NewByteArray([]byte("insufficient participants")),
			}))
		} else {
			// Success - close and move to maturity phase
			c.updateOpportunityStatus(ic, opp, state.OpportunityClosed)
			ic.AddNotification(c.Hash, collocatioOpportunityClosedEvent, stackitem.NewArray([]stackitem.Item{
				stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
			}))
		}
		return true
	})
}

// processClosedOpportunities handles Closed opportunities past their maturity date.
func (c *Collocatio) processClosedOpportunities(ic *interop.Context) {
	prefix := []byte{collocatioPrefixOpportunityByStatus, byte(state.OpportunityClosed)}

	ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) < 9 {
			return true
		}

		oppID := binary.BigEndian.Uint64(k[1:9])
		opp := c.getOpportunityInternal(ic.DAO, oppID)
		if opp == nil {
			return true
		}

		// Check if maturity date passed
		if ic.Block.Index < opp.MaturityDate {
			return true
		}

		// Opportunity is mature and ready for returns distribution
		// Note: Actual distribution is triggered by distributeReturns call
		// This could emit a notification for off-chain systems
		// For now, we just log that it's ready (no state change needed)
		return true
	})
}

// updateOpportunityStatus updates an opportunity's status and maintains status index.
func (c *Collocatio) updateOpportunityStatus(ic *interop.Context, opp *state.InvestmentOpportunity, newStatus state.OpportunityStatus) {
	oldStatus := opp.Status

	// Remove from old status index
	oldStatusKey := makeCollocatioOppByStatusKey(oldStatus, opp.ID)
	ic.DAO.DeleteStorageItem(c.ID, oldStatusKey)

	// Update status
	opp.Status = newStatus
	opp.UpdatedAt = ic.Block.Index

	// Add to new status index
	newStatusKey := makeCollocatioOppByStatusKey(newStatus, opp.ID)
	ic.DAO.PutStorageItem(c.ID, newStatusKey, []byte{1})

	// Save opportunity
	c.putOpportunity(ic.DAO, opp)
}

// ActiveIn returns nil (always active from genesis).
func (c *Collocatio) ActiveIn() *config.Hardfork {
	return nil
}

// ============================================================================
// Storage Key Helpers
// ============================================================================

func makeCollocatioConfigKey() []byte {
	return []byte{collocatioPrefixConfig}
}

func makeCollocatioOppKey(oppID uint64) []byte {
	key := make([]byte, 9)
	key[0] = collocatioPrefixOpportunity
	binary.BigEndian.PutUint64(key[1:], oppID)
	return key
}

func makeCollocatioOppByTypeKey(oppType state.OpportunityType, oppID uint64) []byte {
	key := make([]byte, 10)
	key[0] = collocatioPrefixOpportunityByType
	key[1] = byte(oppType)
	binary.BigEndian.PutUint64(key[2:], oppID)
	return key
}

func makeCollocatioOppByStatusKey(status state.OpportunityStatus, oppID uint64) []byte {
	key := make([]byte, 10)
	key[0] = collocatioPrefixOpportunityByStatus
	key[1] = byte(status)
	binary.BigEndian.PutUint64(key[2:], oppID)
	return key
}

func makeCollocatioOppCounterKey() []byte {
	return []byte{collocatioPrefixOppCounter}
}

func makeCollocatioInvKey(invID uint64) []byte {
	key := make([]byte, 9)
	key[0] = collocatioPrefixInvestment
	binary.BigEndian.PutUint64(key[1:], invID)
	return key
}

func makeCollocatioInvByOppKey(oppID, invID uint64) []byte {
	key := make([]byte, 17)
	key[0] = collocatioPrefixInvestmentByOpp
	binary.BigEndian.PutUint64(key[1:9], oppID)
	binary.BigEndian.PutUint64(key[9:], invID)
	return key
}

func makeCollocatioInvByInvestorKey(vitaID, invID uint64) []byte {
	key := make([]byte, 17)
	key[0] = collocatioPrefixInvestmentByInvestor
	binary.BigEndian.PutUint64(key[1:9], vitaID)
	binary.BigEndian.PutUint64(key[9:], invID)
	return key
}

func makeCollocatioInvCounterKey() []byte {
	return []byte{collocatioPrefixInvCounter}
}

func makeCollocatioEligKey(vitaID uint64) []byte {
	key := make([]byte, 9)
	key[0] = collocatioPrefixEligibility
	binary.BigEndian.PutUint64(key[1:], vitaID)
	return key
}

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

func makeCollocatioViolationKey(violationID uint64) []byte {
	key := make([]byte, 9)
	key[0] = collocatioPrefixViolation
	binary.BigEndian.PutUint64(key[1:], violationID)
	return key
}

func makeCollocatioViolationByInvestorKey(vitaID, violationID uint64) []byte {
	key := make([]byte, 17)
	key[0] = collocatioPrefixViolationByInvestor
	binary.BigEndian.PutUint64(key[1:9], vitaID)
	binary.BigEndian.PutUint64(key[9:], violationID)
	return key
}

func makeCollocatioViolationCounterKey() []byte {
	return []byte{collocatioPrefixViolationCounter}
}

func makeCollocatioEmploymentKey(employeeVitaID uint64) []byte {
	key := make([]byte, 9)
	key[0] = collocatioPrefixEmployment
	binary.BigEndian.PutUint64(key[1:], employeeVitaID)
	return key
}

func makeCollocatioEmploymentByEmployerKey(employerVitaID, employeeVitaID uint64) []byte {
	key := make([]byte, 17)
	key[0] = collocatioPrefixEmploymentByEmployer
	binary.BigEndian.PutUint64(key[1:9], employerVitaID)
	binary.BigEndian.PutUint64(key[9:], employeeVitaID)
	return key
}

func makeCollocatioContractorKey(contractorVitaID uint64) []byte {
	key := make([]byte, 9)
	key[0] = collocatioPrefixContractor
	binary.BigEndian.PutUint64(key[1:], contractorVitaID)
	return key
}

func makeCollocatioContractorByPlatformKey(platform util.Uint160, contractorVitaID uint64) []byte {
	key := make([]byte, 1+util.Uint160Size+8)
	key[0] = collocatioPrefixContractorByPlatform
	copy(key[1:1+util.Uint160Size], platform.BytesBE())
	binary.BigEndian.PutUint64(key[1+util.Uint160Size:], contractorVitaID)
	return key
}

func makeCollocatioCommitmentKey(commitmentID uint64) []byte {
	key := make([]byte, 9)
	key[0] = collocatioPrefixCommitment
	binary.BigEndian.PutUint64(key[1:], commitmentID)
	return key
}

func makeCollocatioCommitmentByOppKey(oppID, commitmentID uint64) []byte {
	key := make([]byte, 17)
	key[0] = collocatioPrefixCommitmentByOpp
	binary.BigEndian.PutUint64(key[1:9], oppID)
	binary.BigEndian.PutUint64(key[9:], commitmentID)
	return key
}

func makeCollocatioCommitmentByInvestorKey(vitaID, commitmentID uint64) []byte {
	key := make([]byte, 17)
	key[0] = collocatioPrefixCommitmentByInvestor
	binary.BigEndian.PutUint64(key[1:9], vitaID)
	binary.BigEndian.PutUint64(key[9:], commitmentID)
	return key
}

func makeCollocatioCommitmentCounterKey() []byte {
	return []byte{collocatioPrefixCommitmentCounter}
}

// ============================================================================
// Internal Storage Methods
// ============================================================================

func (c *Collocatio) getConfigInternal(d *dao.Simple) *state.CollocatioConfig {
	si := d.GetStorageItem(c.ID, makeCollocatioConfigKey())
	if si == nil {
		return &state.CollocatioConfig{
			MinPIOParticipants:     defaultMinPIOParticipants,
			MinEIOParticipants:     defaultMinEIOParticipants,
			MinCIOParticipants:     defaultMinCIOParticipants,
			DefaultMinInvestment:   defaultMinInvestment,
			MaxIndividualCap:       defaultMaxIndividualCap,
			WealthConcentration:    defaultWealthConcentration,
			CreationFee:            defaultCreationFee,
			InvestmentFee:          defaultInvestmentFee,
			WithdrawalPenalty:      defaultWithdrawalPenalty,
			MinVotingPeriod:        defaultMinVotingPeriod,
			MinInvestmentPeriod:    defaultMinInvestmentPeriod,
			MinMaturityPeriod:      defaultMinMaturityPeriod,
			MaxViolationsBeforeBan: defaultMaxViolationsBeforeBan,
			ViolationCooldown:      1000000,
		}
	}
	cfg := new(state.CollocatioConfig)
	item, _ := stackitem.Deserialize(si)
	cfg.FromStackItem(item)
	return cfg
}

func (c *Collocatio) setConfigInternal(d *dao.Simple, cfg *state.CollocatioConfig) {
	item, _ := cfg.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(c.ID, makeCollocatioConfigKey(), data)
}

func (c *Collocatio) getCounter(d *dao.Simple, key []byte) uint64 {
	si := d.GetStorageItem(c.ID, key)
	if si == nil || len(si) < 8 {
		return 0
	}
	return binary.BigEndian.Uint64(si)
}

func (c *Collocatio) incrementCounter(d *dao.Simple, key []byte) uint64 {
	current := c.getCounter(d, key)
	next := current + 1
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, next)
	d.PutStorageItem(c.ID, key, buf)
	return next
}

func (c *Collocatio) getOpportunityInternal(d *dao.Simple, oppID uint64) *state.InvestmentOpportunity {
	si := d.GetStorageItem(c.ID, makeCollocatioOppKey(oppID))
	if si == nil {
		return nil
	}
	opp := new(state.InvestmentOpportunity)
	item, _ := stackitem.Deserialize(si)
	opp.FromStackItem(item)
	return opp
}

func (c *Collocatio) putOpportunity(d *dao.Simple, opp *state.InvestmentOpportunity) {
	item, _ := opp.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(c.ID, makeCollocatioOppKey(opp.ID), data)
}

func (c *Collocatio) getInvestmentInternal(d *dao.Simple, invID uint64) *state.Investment {
	si := d.GetStorageItem(c.ID, makeCollocatioInvKey(invID))
	if si == nil {
		return nil
	}
	inv := new(state.Investment)
	item, _ := stackitem.Deserialize(si)
	inv.FromStackItem(item)
	return inv
}

func (c *Collocatio) putInvestment(d *dao.Simple, inv *state.Investment) {
	item, _ := inv.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(c.ID, makeCollocatioInvKey(inv.ID), data)
}

func (c *Collocatio) getEligibilityInternal(d *dao.Simple, investor util.Uint160) *state.InvestorEligibility {
	// First get vitaID from owner mapping
	si := d.GetStorageItem(c.ID, makeCollocatioEligByOwnerKey(investor))
	if si == nil || len(si) < 8 {
		return nil
	}
	vitaID := binary.BigEndian.Uint64(si)

	// Then get eligibility
	eligSI := d.GetStorageItem(c.ID, makeCollocatioEligKey(vitaID))
	if eligSI == nil {
		return nil
	}
	elig := new(state.InvestorEligibility)
	item, _ := stackitem.Deserialize(eligSI)
	elig.FromStackItem(item)
	return elig
}

func (c *Collocatio) putEligibility(d *dao.Simple, elig *state.InvestorEligibility) {
	item, _ := elig.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(c.ID, makeCollocatioEligKey(elig.VitaID), data)

	// Also store owner -> vitaID mapping
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, elig.VitaID)
	d.PutStorageItem(c.ID, makeCollocatioEligByOwnerKey(elig.Investor), buf)
}

func (c *Collocatio) getCommitmentInternal(d *dao.Simple, commitmentID uint64) *state.InvestmentCommitment {
	si := d.GetStorageItem(c.ID, makeCollocatioCommitmentKey(commitmentID))
	if si == nil {
		return nil
	}
	commitment := new(state.InvestmentCommitment)
	item, _ := stackitem.Deserialize(si)
	commitment.FromStackItem(item)
	return commitment
}

func (c *Collocatio) putCommitment(d *dao.Simple, commitment *state.InvestmentCommitment) {
	item, _ := commitment.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(c.ID, makeCollocatioCommitmentKey(commitment.ID), data)
}

// getInvestorTotalInOpportunity returns the total amount an investor has invested in a specific opportunity.
func (c *Collocatio) getInvestorTotalInOpportunity(d *dao.Simple, vitaID, oppID uint64) uint64 {
	prefix := []byte{collocatioPrefixInvestmentByInvestor}
	prefix = append(prefix, make([]byte, 8)...)
	binary.BigEndian.PutUint64(prefix[1:], vitaID)

	var total uint64
	d.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) >= 16 {
			invID := binary.BigEndian.Uint64(k[8:16])
			inv := c.getInvestmentInternal(d, invID)
			if inv != nil && inv.OpportunityID == oppID && inv.Status == state.InvestmentActive {
				total += inv.Amount
			}
		}
		return true
	})
	return total
}

// ============================================================================
// Contract Methods
// ============================================================================

func (c *Collocatio) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	cfg := c.getConfigInternal(ic.DAO)
	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinPIOParticipants)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinEIOParticipants)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinCIOParticipants)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.DefaultMinInvestment)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MaxIndividualCap)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.WealthConcentration)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.CreationFee)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.InvestmentFee)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.WithdrawalPenalty)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinVotingPeriod))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinInvestmentPeriod))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinMaturityPeriod))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MaxViolationsBeforeBan))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.ViolationCooldown))),
	})
}

func (c *Collocatio) getOpportunityCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	count := c.getCounter(ic.DAO, makeCollocatioOppCounterKey())
	return stackitem.NewBigInteger(new(big.Int).SetUint64(count))
}

func (c *Collocatio) getOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	oppID := toUint64(args[0])
	opp := c.getOpportunityInternal(ic.DAO, oppID)
	if opp == nil {
		return stackitem.Null{}
	}
	return opportunityToStackItem(opp)
}

func (c *Collocatio) createOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	caller := ic.VM.GetCallingScriptHash()

	oppType := state.OpportunityType(toUint64(args[0]))
	name := toString(args[1])
	description := toString(args[2])
	termsHashBytes, err := args[3].TryBytes()
	if err != nil {
		panic(err)
	}
	termsHash, err := util.Uint256DecodeBytesBE(termsHashBytes)
	if err != nil {
		panic(err)
	}
	minParticipants := toUint64(args[4])
	maxParticipants := toUint64(args[5])
	minInvestment := toUint64(args[6])
	maxInvestment := toUint64(args[7])
	targetPool := toUint64(args[8])
	expectedReturns := toUint64(args[9])
	riskLevel := uint8(toUint64(args[10]))
	maturityBlocks := uint32(toUint64(args[11]))

	// Validate caller has Vita
	vita, err := c.Vita.GetTokenByOwner(ic.DAO, caller)
	if err != nil {
		panic("caller must have Vita token")
	}
	vitaID := vita.TokenID

	// Validate opportunity type
	if oppType > state.OpportunityCIO {
		panic("invalid opportunity type")
	}

	// Validate parameters
	cfg := c.getConfigInternal(ic.DAO)
	if maturityBlocks < cfg.MinMaturityPeriod {
		panic("maturity period too short")
	}
	if riskLevel < 1 || riskLevel > 10 {
		panic("risk level must be 1-10")
	}

	// Get minimum participants based on type
	var minRequired uint64
	switch oppType {
	case state.OpportunityPIO:
		minRequired = cfg.MinPIOParticipants
	case state.OpportunityEIO:
		minRequired = cfg.MinEIOParticipants
	case state.OpportunityCIO:
		minRequired = cfg.MinCIOParticipants
	}
	if minParticipants < minRequired {
		panic(fmt.Sprintf("minimum participants must be at least %d for this type", minRequired))
	}

	// Charge creation fee to Treasury
	if cfg.CreationFee > 0 {
		if err := c.VTS.transferUnrestricted(ic, caller, nativehashes.Treasury, new(big.Int).SetUint64(cfg.CreationFee), nil); err != nil {
			panic("failed to pay creation fee")
		}
	}

	// Create opportunity
	oppID := c.incrementCounter(ic.DAO, makeCollocatioOppCounterKey())
	currentBlock := ic.Block.Index

	opp := &state.InvestmentOpportunity{
		ID:                  oppID,
		Type:                oppType,
		Status:              state.OpportunityDraft,
		Creator:             caller,
		SponsorVitaID:       vitaID,
		Name:                name,
		Description:         description,
		TermsHash:           termsHash,
		MinParticipants:     minParticipants,
		MaxParticipants:     maxParticipants,
		CurrentParticipants: 0,
		MinInvestment:       minInvestment,
		MaxInvestment:       maxInvestment,
		TotalPool:           0,
		TargetPool:          targetPool,
		ExpectedReturns:     expectedReturns,
		RiskLevel:           riskLevel,
		VotingDeadline:      currentBlock + cfg.MinVotingPeriod,
		InvestmentDeadline:  currentBlock + cfg.MinVotingPeriod + cfg.MinInvestmentPeriod,
		MaturityDate:        currentBlock + cfg.MinVotingPeriod + cfg.MinInvestmentPeriod + maturityBlocks,
		ProposalID:          0,
		CreatedAt:           currentBlock,
		UpdatedAt:           currentBlock,
	}

	c.putOpportunity(ic.DAO, opp)

	// Store type index
	ic.DAO.PutStorageItem(c.ID, makeCollocatioOppByTypeKey(oppType, oppID), []byte{1})

	// Emit event
	ic.AddNotification(c.Hash, collocatioOpportunityCreatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(oppType))),
		stackitem.NewByteArray(caller.BytesBE()),
	}))

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

func (c *Collocatio) activateOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	oppID := toUint64(args[0])

	opp := c.getOpportunityInternal(ic.DAO, oppID)
	if opp == nil {
		panic("opportunity not found")
	}

	caller := ic.VM.GetCallingScriptHash()
	if caller != opp.Creator && !c.Tutus.CheckCommittee(ic) {
		panic("only creator or committee can activate")
	}

	if opp.Status != state.OpportunityDraft {
		panic("opportunity must be in draft status")
	}

	opp.Status = state.OpportunityActive
	opp.UpdatedAt = ic.Block.Index
	c.putOpportunity(ic.DAO, opp)

	ic.AddNotification(c.Hash, collocatioOpportunityActivatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
	}))

	return stackitem.NewBool(true)
}

func (c *Collocatio) invest(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	oppID := toUint64(args[0])
	amount := toUint64(args[1])
	caller := ic.VM.GetCallingScriptHash()

	// Validate caller has Vita
	vita, err := c.Vita.GetTokenByOwner(ic.DAO, caller)
	if err != nil {
		panic("caller must have Vita token")
	}
	vitaID := vita.TokenID

	// Get opportunity
	opp := c.getOpportunityInternal(ic.DAO, oppID)
	if opp == nil {
		panic("opportunity not found")
	}

	if opp.Status != state.OpportunityActive {
		panic("opportunity is not active")
	}
	if ic.Block.Index > opp.InvestmentDeadline {
		panic("investment deadline has passed")
	}

	if opp.MaxParticipants > 0 && opp.CurrentParticipants >= opp.MaxParticipants {
		panic("maximum participants reached")
	}

	if amount < opp.MinInvestment {
		panic("investment below minimum")
	}
	if amount > opp.MaxInvestment {
		panic("investment exceeds maximum")
	}

	// Check eligibility
	if !c.isEligibleInternal(ic.DAO, caller, opp.Type) {
		panic("investor not eligible for this opportunity type")
	}

	// Calculate fee
	cfg := c.getConfigInternal(ic.DAO)

	// Whale concentration check - prevent any single investor from holding too much of the pool
	if cfg.WealthConcentration > 0 {
		existingInvestment := c.getInvestorTotalInOpportunity(ic.DAO, vitaID, oppID)
		newTotal := existingInvestment + amount
		// Calculate what percentage of the pool this investor would hold
		// (newTotal / (opp.TotalPool + amount)) * 10000 > WealthConcentration
		futurePool := opp.TotalPool + amount
		if futurePool > 0 {
			concentration := (newTotal * 10000) / futurePool
			if concentration > cfg.WealthConcentration {
				panic("investment would exceed whale concentration limit")
			}
		}
	}
	fee := (amount * cfg.InvestmentFee) / 10000
	netAmount := amount - fee

	// Transfer VTS from investor
	if err := c.VTS.transferUnrestricted(ic, caller, c.Hash, new(big.Int).SetUint64(amount), nil); err != nil {
		panic("failed to transfer investment amount")
	}

	// Send fee to Treasury
	if fee > 0 {
		if err := c.VTS.transferUnrestricted(ic, c.Hash, nativehashes.Treasury, new(big.Int).SetUint64(fee), nil); err != nil {
			panic("failed to transfer fee to treasury")
		}
	}

	// Create investment record
	invID := c.incrementCounter(ic.DAO, makeCollocatioInvCounterKey())

	inv := &state.Investment{
		ID:            invID,
		OpportunityID: oppID,
		VitaID:        vitaID,
		Investor:      caller,
		Amount:        netAmount,
		Status:        state.InvestmentActive,
		ReturnAmount:  0,
		CreatedAt:     ic.Block.Index,
		UpdatedAt:     ic.Block.Index,
	}

	c.putInvestment(ic.DAO, inv)

	// Store indexes
	ic.DAO.PutStorageItem(c.ID, makeCollocatioInvByOppKey(oppID, invID), []byte{1})
	ic.DAO.PutStorageItem(c.ID, makeCollocatioInvByInvestorKey(vitaID, invID), []byte{1})

	// Update opportunity
	opp.CurrentParticipants++
	opp.TotalPool += netAmount
	opp.UpdatedAt = ic.Block.Index
	c.putOpportunity(ic.DAO, opp)

	// Update eligibility stats
	c.updateEligibilityOnInvest(ic.DAO, caller, vitaID, netAmount, ic.Block.Index)

	// Emit event
	ic.AddNotification(c.Hash, collocatioInvestmentMadeEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(invID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
		stackitem.NewByteArray(caller.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(netAmount)),
	}))

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

func (c *Collocatio) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	invID := toUint64(args[0])
	caller := ic.VM.GetCallingScriptHash()

	inv := c.getInvestmentInternal(ic.DAO, invID)
	if inv == nil {
		panic("investment not found")
	}

	if inv.Investor != caller {
		panic("only investor can withdraw")
	}

	if inv.Status != state.InvestmentActive {
		panic("investment is not active")
	}

	opp := c.getOpportunityInternal(ic.DAO, inv.OpportunityID)
	if opp == nil {
		panic("opportunity not found")
	}

	// Calculate penalty for early withdrawal
	cfg := c.getConfigInternal(ic.DAO)
	returnAmount := inv.Amount
	if ic.Block.Index < opp.MaturityDate && cfg.WithdrawalPenalty > 0 {
		penalty := (inv.Amount * cfg.WithdrawalPenalty) / 10000
		returnAmount = inv.Amount - penalty
		if penalty > 0 {
			if err := c.VTS.transferUnrestricted(ic, c.Hash, nativehashes.Treasury, new(big.Int).SetUint64(penalty), nil); err != nil {
				panic("failed to transfer penalty")
			}
		}
	}

	// Return funds
	if err := c.VTS.transferUnrestricted(ic, c.Hash, caller, new(big.Int).SetUint64(returnAmount), nil); err != nil {
		panic("failed to return investment")
	}

	// Update investment
	inv.Status = state.InvestmentWithdrawn
	inv.UpdatedAt = ic.Block.Index
	c.putInvestment(ic.DAO, inv)

	// Update opportunity
	opp.CurrentParticipants--
	opp.TotalPool -= inv.Amount
	opp.UpdatedAt = ic.Block.Index
	c.putOpportunity(ic.DAO, opp)

	// Update eligibility
	c.updateEligibilityOnWithdraw(ic.DAO, caller, inv.Amount, ic.Block.Index)

	// Emit event
	ic.AddNotification(c.Hash, collocatioInvestmentWithdrawnEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(invID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(returnAmount)),
	}))

	return stackitem.NewBool(true)
}

func (c *Collocatio) getEligibility(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	investor := toUint160(args[0])
	elig := c.getEligibilityInternal(ic.DAO, investor)
	if elig == nil {
		return stackitem.Null{}
	}
	return eligibilityToStackItem(elig)
}

func (c *Collocatio) setEligibility(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	investor := toUint160(args[0])
	eligFlags := state.EligibilityType(toUint64(args[1]))

	// Only committee or RoleInvestmentManager can set eligibility
	if !c.Tutus.CheckCommittee(ic) {
		caller := ic.VM.GetCallingScriptHash()
		if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) {
			panic("only committee or investment manager can set eligibility")
		}
	}

	vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor)
	if err != nil {
		panic("investor must have Vita token")
	}
	vitaID := vita.TokenID

	elig := c.getEligibilityInternal(ic.DAO, investor)
	if elig == nil {
		elig = &state.InvestorEligibility{
			VitaID:    vitaID,
			Investor:  investor,
			CreatedAt: ic.Block.Index,
		}
	}

	elig.Eligibility = eligFlags
	elig.UpdatedAt = ic.Block.Index
	c.putEligibility(ic.DAO, elig)

	ic.AddNotification(c.Hash, collocatioEligibilityUpdatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(investor.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(eligFlags))),
	}))

	return stackitem.NewBool(true)
}

func (c *Collocatio) isEligible(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	investor := toUint160(args[0])
	oppType := state.OpportunityType(toUint64(args[1]))
	return stackitem.NewBool(c.isEligibleInternal(ic.DAO, investor, oppType))
}

func (c *Collocatio) isEligibleInternal(d *dao.Simple, investor util.Uint160, oppType state.OpportunityType) bool {
	elig := c.getEligibilityInternal(d, investor)
	if elig == nil {
		return false
	}

	// Must have completed investment education
	if !elig.ScireCompleted {
		return false
	}

	// Check for ban
	if elig.HasViolations {
		cfg := c.getConfigInternal(d)
		if elig.ViolationCount >= cfg.MaxViolationsBeforeBan {
			return false
		}
	}

	switch oppType {
	case state.OpportunityPIO:
		return elig.Eligibility&state.EligibilityPIO != 0
	case state.OpportunityEIO:
		if elig.Eligibility&state.EligibilityEIO == 0 {
			return false
		}
		// Additionally verify active employment
		return c.hasActiveEmployment(d, elig.VitaID)
	case state.OpportunityCIO:
		if elig.Eligibility&state.EligibilityCIO == 0 {
			return false
		}
		// Additionally verify active contractor status
		return c.hasActiveContractor(d, elig.VitaID)
	default:
		return false
	}
}

func (c *Collocatio) getInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	invID := toUint64(args[0])
	inv := c.getInvestmentInternal(ic.DAO, invID)
	if inv == nil {
		return stackitem.Null{}
	}
	return investmentToStackItem(inv)
}

func (c *Collocatio) getInvestmentCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	count := c.getCounter(ic.DAO, makeCollocatioInvCounterKey())
	return stackitem.NewBigInteger(new(big.Int).SetUint64(count))
}

// ============================================================================
// Internal Helpers
// ============================================================================

func (c *Collocatio) updateEligibilityOnInvest(d *dao.Simple, investor util.Uint160, vitaID, amount uint64, blockHeight uint32) {
	elig := c.getEligibilityInternal(d, investor)
	if elig == nil {
		elig = &state.InvestorEligibility{
			VitaID:    vitaID,
			Investor:  investor,
			CreatedAt: blockHeight,
		}
	}
	elig.TotalInvested += amount
	elig.ActiveInvestments++
	elig.LastActivity = blockHeight
	elig.UpdatedAt = blockHeight
	c.putEligibility(d, elig)
}

func (c *Collocatio) updateEligibilityOnWithdraw(d *dao.Simple, investor util.Uint160, amount uint64, blockHeight uint32) {
	elig := c.getEligibilityInternal(d, investor)
	if elig == nil {
		return
	}
	if elig.TotalInvested >= amount {
		elig.TotalInvested -= amount
	}
	if elig.ActiveInvestments > 0 {
		elig.ActiveInvestments--
	}
	elig.LastActivity = blockHeight
	elig.UpdatedAt = blockHeight
	c.putEligibility(d, elig)
}

// ============================================================================
// Violation System
// ============================================================================

func (c *Collocatio) getViolationInternal(d *dao.Simple, violationID uint64) *state.InvestmentViolation {
	si := d.GetStorageItem(c.ID, makeCollocatioViolationKey(violationID))
	if si == nil {
		return nil
	}
	v := new(state.InvestmentViolation)
	item, _ := stackitem.Deserialize(si)
	v.FromStackItem(item)
	return v
}

func (c *Collocatio) putViolation(d *dao.Simple, v *state.InvestmentViolation) {
	item, _ := v.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(c.ID, makeCollocatioViolationKey(v.ID), data)
}

func (c *Collocatio) recordViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	violator := toUint160(args[0])
	opportunityID := toUint64(args[1])
	violationType := toString(args[2])
	description := toString(args[3])
	evidenceHashBytes, err := args[4].TryBytes()
	if err != nil {
		panic(err)
	}
	evidenceHash, err := util.Uint256DecodeBytesBE(evidenceHashBytes)
	if err != nil {
		panic(err)
	}
	penalty := toUint64(args[5])

	// Authorization: Committee or RoleInvestmentManager
	if !c.Tutus.CheckCommittee(ic) {
		caller := ic.VM.GetCallingScriptHash()
		if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) {
			panic("only committee or investment manager can record violations")
		}
	}

	// Verify violator has Vita
	vita, err := c.Vita.GetTokenByOwner(ic.DAO, violator)
	if err != nil || vita == nil {
		panic("violator must have active Vita")
	}
	vitaID := vita.TokenID

	// Create violation record
	violationID := c.incrementCounter(ic.DAO, makeCollocatioViolationCounterKey())
	caller := ic.VM.GetCallingScriptHash()

	v := &state.InvestmentViolation{
		ID:            violationID,
		VitaID:        vitaID,
		Violator:      violator,
		OpportunityID: opportunityID,
		ViolationType: violationType,
		Description:   description,
		EvidenceHash:  evidenceHash,
		Penalty:       penalty,
		ReportedBy:    caller,
		ReportedAt:    ic.Block.Index,
		ResolvedAt:    0,
		Resolution:    "",
	}

	c.putViolation(ic.DAO, v)

	// Store index by investor
	ic.DAO.PutStorageItem(c.ID, makeCollocatioViolationByInvestorKey(vitaID, violationID), []byte{1})

	// Update eligibility
	elig := c.getEligibilityInternal(ic.DAO, violator)
	if elig == nil {
		elig = &state.InvestorEligibility{
			VitaID:    vitaID,
			Investor:  violator,
			CreatedAt: ic.Block.Index,
		}
	}
	elig.HasViolations = true
	elig.ViolationCount++
	elig.UpdatedAt = ic.Block.Index
	c.putEligibility(ic.DAO, elig)

	// Apply penalty if specified
	if penalty > 0 {
		if err := c.VTS.transferUnrestricted(ic, violator, nativehashes.Treasury, new(big.Int).SetUint64(penalty), nil); err != nil {
			// Don't panic if transfer fails, just record violation without penalty
		}
	}

	// Emit event
	ic.AddNotification(c.Hash, collocatioViolationRecordedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)),
		stackitem.NewByteArray(violator.BytesBE()),
		stackitem.NewByteArray([]byte(violationType)),
	}))

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

func (c *Collocatio) resolveViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	violationID := toUint64(args[0])
	resolution := toString(args[1])

	// Authorization: Committee or RoleInvestmentManager
	if !c.Tutus.CheckCommittee(ic) {
		caller := ic.VM.GetCallingScriptHash()
		if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) {
			panic("only committee or investment manager can resolve violations")
		}
	}

	v := c.getViolationInternal(ic.DAO, violationID)
	if v == nil {
		panic("violation not found")
	}
	if v.ResolvedAt != 0 {
		panic("violation already resolved")
	}

	v.ResolvedAt = ic.Block.Index
	v.Resolution = resolution
	c.putViolation(ic.DAO, v)

	// Emit event
	ic.AddNotification(c.Hash, collocatioViolationResolvedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)),
		stackitem.NewByteArray([]byte(resolution)),
	}))

	return stackitem.NewBool(true)
}

func (c *Collocatio) getViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	violationID := toUint64(args[0])
	v := c.getViolationInternal(ic.DAO, violationID)
	if v == nil {
		return stackitem.Null{}
	}
	return violationToStackItem(v)
}

// ============================================================================
// Employment Verification (EIO)
// ============================================================================

func (c *Collocatio) getEmploymentInternal(d *dao.Simple, employeeVitaID uint64) *state.EmploymentVerification {
	si := d.GetStorageItem(c.ID, makeCollocatioEmploymentKey(employeeVitaID))
	if si == nil {
		return nil
	}
	ev := new(state.EmploymentVerification)
	item, _ := stackitem.Deserialize(si)
	ev.FromStackItem(item)
	return ev
}

func (c *Collocatio) putEmployment(d *dao.Simple, ev *state.EmploymentVerification) {
	item, _ := ev.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(c.ID, makeCollocatioEmploymentKey(ev.VitaID), data)
}

func (c *Collocatio) verifyEmployment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	employee := toUint160(args[0])
	employer := toUint160(args[1])
	position := toString(args[2])
	startDate := uint32(toUint64(args[3]))

	caller := ic.VM.GetCallingScriptHash()

	// Authorization: Committee, RoleInvestmentManager, or employer
	isAuthorized := c.Tutus.CheckCommittee(ic) ||
		c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) ||
		caller == employer

	if !isAuthorized {
		panic("only committee, investment manager, or employer can verify employment")
	}

	// Verify both employee and employer have Vita
	employeeVita, err := c.Vita.GetTokenByOwner(ic.DAO, employee)
	if err != nil {
		panic("employee must have Vita token")
	}
	employerVita, err := c.Vita.GetTokenByOwner(ic.DAO, employer)
	if err != nil {
		panic("employer must have Vita token")
	}

	ev := &state.EmploymentVerification{
		VitaID:         employeeVita.TokenID,
		Employee:       employee,
		EmployerVitaID: employerVita.TokenID,
		Employer:       employer,
		Position:       position,
		StartDate:      startDate,
		EndDate:        0,
		IsActive:       true,
		VerifiedAt:     ic.Block.Index,
		VerifiedBy:     caller,
	}

	c.putEmployment(ic.DAO, ev)

	// Store index by employer
	ic.DAO.PutStorageItem(c.ID, makeCollocatioEmploymentByEmployerKey(employerVita.TokenID, employeeVita.TokenID), []byte{1})

	// Update eligibility
	elig := c.getEligibilityInternal(ic.DAO, employee)
	if elig == nil {
		elig = &state.InvestorEligibility{
			VitaID:    employeeVita.TokenID,
			Investor:  employee,
			CreatedAt: ic.Block.Index,
		}
	}
	elig.Eligibility |= state.EligibilityEIO
	elig.UpdatedAt = ic.Block.Index
	c.putEligibility(ic.DAO, elig)

	// Emit event
	ic.AddNotification(c.Hash, collocatioEmploymentVerifiedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(employee.BytesBE()),
		stackitem.NewByteArray(employer.BytesBE()),
		stackitem.NewByteArray([]byte(position)),
	}))

	return stackitem.NewBool(true)
}

func (c *Collocatio) revokeEmployment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	employee := toUint160(args[0])
	employer := toUint160(args[1])
	endDate := uint32(toUint64(args[2]))

	caller := ic.VM.GetCallingScriptHash()

	// Authorization: Committee, RoleInvestmentManager, or employer
	isAuthorized := c.Tutus.CheckCommittee(ic) ||
		c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) ||
		caller == employer

	if !isAuthorized {
		panic("only committee, investment manager, or employer can revoke employment")
	}

	// Get employee Vita
	employeeVita, err := c.Vita.GetTokenByOwner(ic.DAO, employee)
	if err != nil {
		panic("employee must have Vita token")
	}

	ev := c.getEmploymentInternal(ic.DAO, employeeVita.TokenID)
	if ev == nil {
		panic("employment not found")
	}
	if !ev.IsActive {
		panic("employment already revoked")
	}
	if ev.Employer != employer {
		panic("employer mismatch")
	}

	ev.IsActive = false
	ev.EndDate = endDate
	c.putEmployment(ic.DAO, ev)

	// Remove EIO eligibility
	elig := c.getEligibilityInternal(ic.DAO, employee)
	if elig != nil {
		elig.Eligibility &^= state.EligibilityEIO
		elig.UpdatedAt = ic.Block.Index
		c.putEligibility(ic.DAO, elig)
	}

	// Emit event
	ic.AddNotification(c.Hash, collocatioEmploymentRevokedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(employee.BytesBE()),
		stackitem.NewByteArray(employer.BytesBE()),
	}))

	return stackitem.NewBool(true)
}

func (c *Collocatio) getEmploymentStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	employee := toUint160(args[0])

	vita, err := c.Vita.GetTokenByOwner(ic.DAO, employee)
	if err != nil {
		return stackitem.Null{}
	}

	ev := c.getEmploymentInternal(ic.DAO, vita.TokenID)
	if ev == nil {
		return stackitem.Null{}
	}
	return employmentToStackItem(ev)
}

func (c *Collocatio) hasActiveEmployment(d *dao.Simple, vitaID uint64) bool {
	ev := c.getEmploymentInternal(d, vitaID)
	return ev != nil && ev.IsActive
}

// ============================================================================
// Contractor Verification (CIO)
// ============================================================================

func (c *Collocatio) getContractorInternal(d *dao.Simple, contractorVitaID uint64) *state.ContractorVerification {
	si := d.GetStorageItem(c.ID, makeCollocatioContractorKey(contractorVitaID))
	if si == nil {
		return nil
	}
	cv := new(state.ContractorVerification)
	item, _ := stackitem.Deserialize(si)
	cv.FromStackItem(item)
	return cv
}

func (c *Collocatio) putContractor(d *dao.Simple, cv *state.ContractorVerification) {
	item, _ := cv.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(c.ID, makeCollocatioContractorKey(cv.VitaID), data)
}

func (c *Collocatio) verifyContractor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	contractor := toUint160(args[0])
	platform := toUint160(args[1])
	platformID := toString(args[2])
	contractorID := toString(args[3])
	startDate := uint32(toUint64(args[4]))

	caller := ic.VM.GetCallingScriptHash()

	// Authorization: Committee, RoleInvestmentManager, or platform
	isAuthorized := c.Tutus.CheckCommittee(ic) ||
		c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) ||
		caller == platform

	if !isAuthorized {
		panic("only committee, investment manager, or platform can verify contractor")
	}

	// Verify contractor has Vita
	contractorVita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor)
	if err != nil {
		panic("contractor must have Vita token")
	}

	cv := &state.ContractorVerification{
		VitaID:       contractorVita.TokenID,
		Contractor:   contractor,
		PlatformID:   platformID,
		Platform:     platform,
		ContractorID: contractorID,
		StartDate:    startDate,
		EndDate:      0,
		IsActive:     true,
		VerifiedAt:   ic.Block.Index,
		VerifiedBy:   caller,
	}

	c.putContractor(ic.DAO, cv)

	// Store index by platform
	ic.DAO.PutStorageItem(c.ID, makeCollocatioContractorByPlatformKey(platform, contractorVita.TokenID), []byte{1})

	// Update eligibility
	elig := c.getEligibilityInternal(ic.DAO, contractor)
	if elig == nil {
		elig = &state.InvestorEligibility{
			VitaID:    contractorVita.TokenID,
			Investor:  contractor,
			CreatedAt: ic.Block.Index,
		}
	}
	elig.Eligibility |= state.EligibilityCIO
	elig.UpdatedAt = ic.Block.Index
	c.putEligibility(ic.DAO, elig)

	// Emit event
	ic.AddNotification(c.Hash, collocatioContractorVerifiedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(contractor.BytesBE()),
		stackitem.NewByteArray(platform.BytesBE()),
		stackitem.NewByteArray([]byte(platformID)),
	}))

	return stackitem.NewBool(true)
}

func (c *Collocatio) revokeContractor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	contractor := toUint160(args[0])
	platform := toUint160(args[1])
	endDate := uint32(toUint64(args[2]))

	caller := ic.VM.GetCallingScriptHash()

	// Authorization: Committee, RoleInvestmentManager, or platform
	isAuthorized := c.Tutus.CheckCommittee(ic) ||
		c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) ||
		caller == platform

	if !isAuthorized {
		panic("only committee, investment manager, or platform can revoke contractor")
	}

	// Get contractor Vita
	contractorVita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor)
	if err != nil {
		panic("contractor must have Vita token")
	}

	cv := c.getContractorInternal(ic.DAO, contractorVita.TokenID)
	if cv == nil {
		panic("contractor not found")
	}
	if !cv.IsActive {
		panic("contractor already revoked")
	}
	if cv.Platform != platform {
		panic("platform mismatch")
	}

	cv.IsActive = false
	cv.EndDate = endDate
	c.putContractor(ic.DAO, cv)

	// Remove CIO eligibility
	elig := c.getEligibilityInternal(ic.DAO, contractor)
	if elig != nil {
		elig.Eligibility &^= state.EligibilityCIO
		elig.UpdatedAt = ic.Block.Index
		c.putEligibility(ic.DAO, elig)
	}

	// Emit event
	ic.AddNotification(c.Hash, collocatioContractorRevokedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(contractor.BytesBE()),
		stackitem.NewByteArray(platform.BytesBE()),
	}))

	return stackitem.NewBool(true)
}

func (c *Collocatio) getContractorStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	contractor := toUint160(args[0])

	vita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor)
	if err != nil {
		return stackitem.Null{}
	}

	cv := c.getContractorInternal(ic.DAO, vita.TokenID)
	if cv == nil {
		return stackitem.Null{}
	}
	return contractorToStackItem(cv)
}

func (c *Collocatio) hasActiveContractor(d *dao.Simple, vitaID uint64) bool {
	cv := c.getContractorInternal(d, vitaID)
	return cv != nil && cv.IsActive
}

// ============================================================================
// Education Completion
// ============================================================================

func (c *Collocatio) completeInvestmentEducation(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	investor := toUint160(args[0])

	caller := ic.VM.GetCallingScriptHash()

	// Authorization: Committee or RoleInvestmentManager
	if !c.Tutus.CheckCommittee(ic) {
		if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) {
			panic("only committee or investment manager can complete education")
		}
	}

	// Verify investor has Vita
	vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor)
	if err != nil {
		panic("investor must have Vita token")
	}

	// Update eligibility
	elig := c.getEligibilityInternal(ic.DAO, investor)
	if elig == nil {
		elig = &state.InvestorEligibility{
			VitaID:    vita.TokenID,
			Investor:  investor,
			CreatedAt: ic.Block.Index,
		}
	}
	elig.ScireCompleted = true
	elig.UpdatedAt = ic.Block.Index
	c.putEligibility(ic.DAO, elig)

	// Emit event
	ic.AddNotification(c.Hash, collocatioEducationCompletedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewByteArray(investor.BytesBE()),
	}))

	return stackitem.NewBool(true)
}

// ============================================================================
// Returns Distribution & Cancellation
// ============================================================================

func (c *Collocatio) distributeReturns(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	oppID := toUint64(args[0])
	actualReturns := toUint64(args[1])

	// Authorization: Committee or RoleInvestmentManager
	if !c.Tutus.CheckCommittee(ic) {
		caller := ic.VM.GetCallingScriptHash()
		if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) {
			panic("only committee or investment manager can distribute returns")
		}
	}

	opp := c.getOpportunityInternal(ic.DAO, oppID)
	if opp == nil {
		panic("opportunity not found")
	}
	if opp.Status != state.OpportunityClosed {
		panic("opportunity must be in closed status")
	}
	if ic.Block.Index < opp.MaturityDate {
		panic("maturity date not reached")
	}
	if opp.TotalPool == 0 {
		panic("no investments to distribute")
	}

	// Collect all investment IDs first (to avoid modifying during iteration)
	prefix := []byte{collocatioPrefixInvestmentByOpp}
	prefix = append(prefix, make([]byte, 8)...)
	binary.BigEndian.PutUint64(prefix[1:], oppID)

	var invIDs []uint64
	ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) >= 16 {
			invID := binary.BigEndian.Uint64(k[8:16])
			invIDs = append(invIDs, invID)
		}
		return true
	})

	// Process each investment
	for _, invID := range invIDs {
		inv := c.getInvestmentInternal(ic.DAO, invID)
		if inv == nil || inv.Status != state.InvestmentActive {
			continue
		}

		// Calculate proportional return
		returnAmount := (inv.Amount * actualReturns) / opp.TotalPool

		// Transfer return to investor
		if returnAmount > 0 {
			if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(returnAmount), nil); err != nil {
				continue // Skip failed transfers
			}
		}

		// Also return principal
		if inv.Amount > 0 {
			if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(inv.Amount), nil); err != nil {
				continue
			}
		}

		// Update investment
		inv.ReturnAmount = returnAmount
		inv.Status = state.InvestmentCompleted
		inv.UpdatedAt = ic.Block.Index
		c.putInvestment(ic.DAO, inv)

		// Update eligibility
		elig := c.getEligibilityInternal(ic.DAO, inv.Investor)
		if elig != nil {
			elig.TotalReturns += returnAmount
			elig.CompletedInvestments++
			if elig.ActiveInvestments > 0 {
				elig.ActiveInvestments--
			}
			if elig.TotalInvested >= inv.Amount {
				elig.TotalInvested -= inv.Amount
			}
			elig.LastActivity = ic.Block.Index
			elig.UpdatedAt = ic.Block.Index
			c.putEligibility(ic.DAO, elig)
		}

		// Emit event for each distribution
		ic.AddNotification(c.Hash, collocatioReturnsDistributedEvent, stackitem.NewArray([]stackitem.Item{
			stackitem.NewBigInteger(new(big.Int).SetUint64(invID)),
			stackitem.NewByteArray(inv.Investor.BytesBE()),
			stackitem.NewBigInteger(new(big.Int).SetUint64(returnAmount)),
		}))
	}

	// Update opportunity status
	opp.Status = state.OpportunityCompleted
	opp.UpdatedAt = ic.Block.Index
	c.putOpportunity(ic.DAO, opp)

	return stackitem.NewBool(true)
}

func (c *Collocatio) cancelOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	oppID := toUint64(args[0])
	caller := ic.VM.GetCallingScriptHash()

	opp := c.getOpportunityInternal(ic.DAO, oppID)
	if opp == nil {
		panic("opportunity not found")
	}

	// Authorization: Creator (if Draft/Voting) or Committee
	isCreator := caller == opp.Creator
	isCommittee := c.Tutus.CheckCommittee(ic)

	if opp.Status == state.OpportunityDraft || opp.Status == state.OpportunityVoting {
		if !isCreator && !isCommittee {
			panic("only creator or committee can cancel draft/voting opportunity")
		}
	} else if opp.Status == state.OpportunityActive {
		if !isCommittee {
			panic("only committee can cancel active opportunity")
		}
	} else {
		panic("opportunity cannot be cancelled in current status")
	}

	// Collect all investment IDs first (to avoid modifying during iteration)
	prefix := []byte{collocatioPrefixInvestmentByOpp}
	prefix = append(prefix, make([]byte, 8)...)
	binary.BigEndian.PutUint64(prefix[1:], oppID)

	var invIDs []uint64
	ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) >= 16 {
			invID := binary.BigEndian.Uint64(k[8:16])
			invIDs = append(invIDs, invID)
		}
		return true
	})

	// Process each investment
	for _, invID := range invIDs {
		inv := c.getInvestmentInternal(ic.DAO, invID)
		if inv == nil || inv.Status != state.InvestmentActive {
			continue
		}

		// Refund full amount
		if inv.Amount > 0 {
			if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(inv.Amount), nil); err != nil {
				continue
			}
		}

		// Update investment
		inv.Status = state.InvestmentRefunded
		inv.UpdatedAt = ic.Block.Index
		c.putInvestment(ic.DAO, inv)

		// Update eligibility
		elig := c.getEligibilityInternal(ic.DAO, inv.Investor)
		if elig != nil {
			if elig.ActiveInvestments > 0 {
				elig.ActiveInvestments--
			}
			if elig.TotalInvested >= inv.Amount {
				elig.TotalInvested -= inv.Amount
			}
			elig.LastActivity = ic.Block.Index
			elig.UpdatedAt = ic.Block.Index
			c.putEligibility(ic.DAO, elig)
		}
	}

	// Update opportunity status
	opp.Status = state.OpportunityCancelled
	opp.UpdatedAt = ic.Block.Index
	c.putOpportunity(ic.DAO, opp)

	// Emit event
	ic.AddNotification(c.Hash, collocatioOpportunityCancelledEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
	}))

	return stackitem.NewBool(true)
}

// ============================================================================
// Query Methods
// ============================================================================

func (c *Collocatio) getInvestmentsByOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	oppID := toUint64(args[0])

	prefix := []byte{collocatioPrefixInvestmentByOpp}
	prefix = append(prefix, make([]byte, 8)...)
	binary.BigEndian.PutUint64(prefix[1:], oppID)

	var ids []stackitem.Item
	ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) >= 16 {
			invID := binary.BigEndian.Uint64(k[8:16])
			ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(invID)))
		}
		return true
	})

	return stackitem.NewArray(ids)
}

func (c *Collocatio) getInvestmentsByInvestor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	investor := toUint160(args[0])

	vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor)
	if err != nil {
		return stackitem.NewArray(nil)
	}

	prefix := []byte{collocatioPrefixInvestmentByInvestor}
	prefix = append(prefix, make([]byte, 8)...)
	binary.BigEndian.PutUint64(prefix[1:], vita.TokenID)

	var ids []stackitem.Item
	ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) >= 16 {
			invID := binary.BigEndian.Uint64(k[8:16])
			ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(invID)))
		}
		return true
	})

	return stackitem.NewArray(ids)
}

func (c *Collocatio) getOpportunitiesByType(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	oppType := state.OpportunityType(toUint64(args[0]))

	prefix := []byte{collocatioPrefixOpportunityByType, byte(oppType)}

	var ids []stackitem.Item
	ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) >= 9 {
			oppID := binary.BigEndian.Uint64(k[1:9])
			ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)))
		}
		return true
	})

	return stackitem.NewArray(ids)
}

func (c *Collocatio) getOpportunitiesByStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	status := state.OpportunityStatus(toUint64(args[0]))

	prefix := []byte{collocatioPrefixOpportunityByStatus, byte(status)}

	var ids []stackitem.Item
	ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) >= 9 {
			oppID := binary.BigEndian.Uint64(k[1:9])
			ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)))
		}
		return true
	})

	return stackitem.NewArray(ids)
}

// ============================================================================
// Stack Item Converters
// ============================================================================

func opportunityToStackItem(opp *state.InvestmentOpportunity) stackitem.Item {
	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.Type))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.Status))),
		stackitem.NewByteArray(opp.Creator.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.SponsorVitaID)),
		stackitem.NewByteArray([]byte(opp.Name)),
		stackitem.NewByteArray([]byte(opp.Description)),
		stackitem.NewByteArray(opp.TermsHash.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MinParticipants)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MaxParticipants)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.CurrentParticipants)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MinInvestment)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MaxInvestment)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.TotalPool)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.TargetPool)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ExpectedReturns)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.RiskLevel))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.VotingDeadline))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.InvestmentDeadline))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.MaturityDate))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ProposalID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.CreatedAt))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.UpdatedAt))),
	})
}

func investmentToStackItem(inv *state.Investment) stackitem.Item {
	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(inv.ID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(inv.OpportunityID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(inv.VitaID)),
		stackitem.NewByteArray(inv.Investor.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(inv.Amount)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.Status))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(inv.ReturnAmount)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.CreatedAt))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.UpdatedAt))),
	})
}

func eligibilityToStackItem(elig *state.InvestorEligibility) stackitem.Item {
	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(elig.VitaID)),
		stackitem.NewByteArray(elig.Investor.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.Eligibility))),
		stackitem.NewBool(elig.ScireCompleted),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.RiskScore))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(elig.TotalInvested)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(elig.TotalReturns)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(elig.ActiveInvestments)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(elig.CompletedInvestments)),
		stackitem.NewBool(elig.HasViolations),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.ViolationCount))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.LastActivity))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.CreatedAt))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.UpdatedAt))),
	})
}

func violationToStackItem(v *state.InvestmentViolation) stackitem.Item {
	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(v.ID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(v.VitaID)),
		stackitem.NewByteArray(v.Violator.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(v.OpportunityID)),
		stackitem.NewByteArray([]byte(v.ViolationType)),
		stackitem.NewByteArray([]byte(v.Description)),
		stackitem.NewByteArray(v.EvidenceHash.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(v.Penalty)),
		stackitem.NewByteArray(v.ReportedBy.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(v.ReportedAt))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(v.ResolvedAt))),
		stackitem.NewByteArray([]byte(v.Resolution)),
	})
}

func employmentToStackItem(ev *state.EmploymentVerification) stackitem.Item {
	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(ev.VitaID)),
		stackitem.NewByteArray(ev.Employee.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(ev.EmployerVitaID)),
		stackitem.NewByteArray(ev.Employer.BytesBE()),
		stackitem.NewByteArray([]byte(ev.Position)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.StartDate))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.EndDate))),
		stackitem.NewBool(ev.IsActive),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.VerifiedAt))),
		stackitem.NewByteArray(ev.VerifiedBy.BytesBE()),
	})
}

func contractorToStackItem(cv *state.ContractorVerification) stackitem.Item {
	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(cv.VitaID)),
		stackitem.NewByteArray(cv.Contractor.BytesBE()),
		stackitem.NewByteArray([]byte(cv.PlatformID)),
		stackitem.NewByteArray(cv.Platform.BytesBE()),
		stackitem.NewByteArray([]byte(cv.ContractorID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.StartDate))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.EndDate))),
		stackitem.NewBool(cv.IsActive),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.VerifiedAt))),
		stackitem.NewByteArray(cv.VerifiedBy.BytesBE()),
	})
}

func commitmentToStackItem(c *state.InvestmentCommitment) stackitem.Item {
	return stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(c.ID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(c.OpportunityID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(c.VitaID)),
		stackitem.NewByteArray(c.Investor.BytesBE()),
		stackitem.NewByteArray(c.Commitment.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.Status))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.CommittedAt))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.RevealDeadline))),
		stackitem.NewBigInteger(new(big.Int).SetUint64(c.RevealedAmount)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(c.InvestmentID)),
	})
}

// ============================================================================
// Commit-Reveal System (Anti-Front-Running)
// ============================================================================

// commitInvestment creates a commitment to invest without revealing the amount.
// The commitment is hash(amount || nonce || investor).
func (col *Collocatio) commitInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	oppID := toUint64(args[0])
	commitmentHashBytes, err := args[1].TryBytes()
	if err != nil {
		panic(err)
	}
	commitmentHash, err := util.Uint256DecodeBytesBE(commitmentHashBytes)
	if err != nil {
		panic("invalid commitment hash")
	}

	caller := ic.VM.GetCallingScriptHash()

	// Validate caller has Vita
	vita, err := col.Vita.GetTokenByOwner(ic.DAO, caller)
	if err != nil {
		panic("caller must have Vita token")
	}
	vitaID := vita.TokenID

	// Get opportunity
	opp := col.getOpportunityInternal(ic.DAO, oppID)
	if opp == nil {
		panic("opportunity not found")
	}

	if opp.Status != state.OpportunityActive {
		panic("opportunity is not active")
	}
	if ic.Block.Index > opp.InvestmentDeadline {
		panic("investment deadline has passed")
	}

	// Check eligibility
	if !col.isEligibleInternal(ic.DAO, caller, opp.Type) {
		panic("investor not eligible for this opportunity type")
	}

	cfg := col.getConfigInternal(ic.DAO)

	// Create commitment
	commitmentID := col.incrementCounter(ic.DAO, makeCollocatioCommitmentCounterKey())
	currentBlock := ic.Block.Index

	commitment := &state.InvestmentCommitment{
		ID:             commitmentID,
		OpportunityID:  oppID,
		VitaID:         vitaID,
		Investor:       caller,
		Commitment:     commitmentHash,
		Status:         state.CommitmentPending,
		CommittedAt:    currentBlock,
		RevealDeadline: currentBlock + cfg.CommitRevealDelay + cfg.CommitRevealWindow,
		RevealedAmount: 0,
		InvestmentID:   0,
	}

	col.putCommitment(ic.DAO, commitment)

	// Store indexes
	ic.DAO.PutStorageItem(col.ID, makeCollocatioCommitmentByOppKey(oppID, commitmentID), []byte{1})
	ic.DAO.PutStorageItem(col.ID, makeCollocatioCommitmentByInvestorKey(vitaID, commitmentID), []byte{1})

	// Emit event
	ic.AddNotification(col.Hash, collocatioCommitmentCreatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)),
		stackitem.NewByteArray(caller.BytesBE()),
	}))

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

// revealInvestment reveals the investment amount and executes the investment.
// The reveal must happen after CommitRevealDelay blocks but before RevealDeadline.
func (col *Collocatio) revealInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	commitmentID := toUint64(args[0])
	amount := toUint64(args[1])
	nonce, err := args[2].TryBytes()
	if err != nil {
		panic("invalid nonce")
	}

	caller := ic.VM.GetCallingScriptHash()

	// Get commitment
	commitment := col.getCommitmentInternal(ic.DAO, commitmentID)
	if commitment == nil {
		panic("commitment not found")
	}

	if commitment.Investor != caller {
		panic("only commitment owner can reveal")
	}

	if commitment.Status != state.CommitmentPending {
		panic("commitment already processed")
	}

	cfg := col.getConfigInternal(ic.DAO)

	// Check timing
	currentBlock := ic.Block.Index
	revealStart := commitment.CommittedAt + cfg.CommitRevealDelay
	if currentBlock < revealStart {
		panic("reveal period not started yet")
	}
	if currentBlock > commitment.RevealDeadline {
		panic("reveal deadline passed")
	}

	// Verify commitment: hash(amount || nonce || investor)
	preimage := make([]byte, 8+len(nonce)+util.Uint160Size)
	binary.BigEndian.PutUint64(preimage[:8], amount)
	copy(preimage[8:8+len(nonce)], nonce)
	copy(preimage[8+len(nonce):], caller.BytesBE())

	// Hash the preimage using SHA256
	computedHash := hash.Sha256(preimage)
	if computedHash != commitment.Commitment {
		panic("commitment verification failed")
	}

	// Get opportunity
	opp := col.getOpportunityInternal(ic.DAO, commitment.OpportunityID)
	if opp == nil {
		panic("opportunity not found")
	}

	if opp.Status != state.OpportunityActive {
		panic("opportunity is not active")
	}
	if currentBlock > opp.InvestmentDeadline {
		panic("investment deadline has passed")
	}

	// Validate amount
	if amount < opp.MinInvestment {
		panic("investment below minimum")
	}
	if amount > opp.MaxInvestment {
		panic("investment exceeds maximum")
	}

	if opp.MaxParticipants > 0 && opp.CurrentParticipants >= opp.MaxParticipants {
		panic("maximum participants reached")
	}

	// Whale concentration check
	if cfg.WealthConcentration > 0 {
		existingInvestment := col.getInvestorTotalInOpportunity(ic.DAO, commitment.VitaID, commitment.OpportunityID)
		newTotal := existingInvestment + amount
		futurePool := opp.TotalPool + amount
		if futurePool > 0 {
			concentration := (newTotal * 10000) / futurePool
			if concentration > cfg.WealthConcentration {
				panic("investment would exceed whale concentration limit")
			}
		}
	}

	// Calculate fee
	fee := (amount * cfg.InvestmentFee) / 10000
	netAmount := amount - fee

	// Transfer VTS from investor
	if err := col.VTS.transferUnrestricted(ic, caller, col.Hash, new(big.Int).SetUint64(amount), nil); err != nil {
		panic("failed to transfer investment amount")
	}

	// Send fee to Treasury
	if fee > 0 {
		if err := col.VTS.transferUnrestricted(ic, col.Hash, nativehashes.Treasury, new(big.Int).SetUint64(fee), nil); err != nil {
			panic("failed to transfer fee to treasury")
		}
	}

	// Create investment record
	invID := col.incrementCounter(ic.DAO, makeCollocatioInvCounterKey())

	inv := &state.Investment{
		ID:            invID,
		OpportunityID: commitment.OpportunityID,
		VitaID:        commitment.VitaID,
		Investor:      caller,
		Amount:        netAmount,
		Status:        state.InvestmentActive,
		ReturnAmount:  0,
		CreatedAt:     currentBlock,
		UpdatedAt:     currentBlock,
	}

	col.putInvestment(ic.DAO, inv)

	// Store indexes
	ic.DAO.PutStorageItem(col.ID, makeCollocatioInvByOppKey(commitment.OpportunityID, invID), []byte{1})
	ic.DAO.PutStorageItem(col.ID, makeCollocatioInvByInvestorKey(commitment.VitaID, invID), []byte{1})

	// Update opportunity
	opp.CurrentParticipants++
	opp.TotalPool += netAmount
	opp.UpdatedAt = currentBlock
	col.putOpportunity(ic.DAO, opp)

	// Update commitment
	commitment.Status = state.CommitmentRevealed
	commitment.RevealedAmount = amount
	commitment.InvestmentID = invID
	col.putCommitment(ic.DAO, commitment)

	// Update eligibility
	col.updateEligibilityOnInvest(ic.DAO, caller, commitment.VitaID, netAmount, currentBlock)

	// Emit events
	ic.AddNotification(col.Hash, collocatioCommitmentRevealedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(invID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(amount)),
	}))

	ic.AddNotification(col.Hash, collocatioInvestmentMadeEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(invID)),
		stackitem.NewBigInteger(new(big.Int).SetUint64(commitment.OpportunityID)),
		stackitem.NewByteArray(caller.BytesBE()),
		stackitem.NewBigInteger(new(big.Int).SetUint64(netAmount)),
	}))

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

// cancelCommitment cancels a pending commitment.
func (col *Collocatio) cancelCommitment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	commitmentID := toUint64(args[0])
	caller := ic.VM.GetCallingScriptHash()

	commitment := col.getCommitmentInternal(ic.DAO, commitmentID)
	if commitment == nil {
		panic("commitment not found")
	}

	if commitment.Investor != caller {
		panic("only commitment owner can cancel")
	}

	if commitment.Status != state.CommitmentPending {
		panic("commitment already processed")
	}

	// Update commitment status
	commitment.Status = state.CommitmentCanceled
	col.putCommitment(ic.DAO, commitment)

	// Emit event
	ic.AddNotification(col.Hash, collocatioCommitmentCanceledEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(new(big.Int).SetUint64(commitmentID)),
	}))

	return stackitem.NewBool(true)
}

// getCommitment returns commitment details.
func (col *Collocatio) getCommitment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	commitmentID := toUint64(args[0])
	commitment := col.getCommitmentInternal(ic.DAO, commitmentID)
	if commitment == nil {
		return stackitem.Null{}
	}
	return commitmentToStackItem(commitment)
}

// getCommitmentCount returns the total number of commitments.
func (col *Collocatio) getCommitmentCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
	count := col.getCounter(ic.DAO, makeCollocatioCommitmentCounterKey())
	return stackitem.NewBigInteger(new(big.Int).SetUint64(count))
}
