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/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"
)

// Scire represents the universal education native contract.
type Scire struct {
	interop.ContractMD
	Tutus ITutus
	Vita         IVita
	RoleRegistry IRoleRegistry
	Lex          ILex
}

// ScireCache represents the cached state for Scire contract.
type ScireCache struct {
	accountCount uint64
	certCount    uint64
	enrollCount  uint64
}

// Storage key prefixes for Scire.
const (
	scirePrefixAccount           byte = 0x01 // vitaID -> EducationAccount
	scirePrefixAccountByOwner    byte = 0x02 // owner -> vitaID
	scirePrefixCertification     byte = 0x10 // certID -> Certification
	scirePrefixCertByOwner       byte = 0x11 // vitaID + certID -> exists
	scirePrefixCertByInstitution byte = 0x12 // institution + certID -> exists
	scirePrefixCertByType        byte = 0x13 // certType hash + certID -> exists
	scirePrefixEnrollment        byte = 0x20 // enrollmentID -> Enrollment
	scirePrefixEnrollByStudent   byte = 0x21 // vitaID + enrollmentID -> exists
	scirePrefixEnrollByProgram   byte = 0x22 // programID hash + enrollmentID -> exists
	scirePrefixActiveEnrollment  byte = 0x23 // vitaID -> active enrollmentID
	scirePrefixAccountCounter    byte = 0xF0 // -> uint64
	scirePrefixCertCounter       byte = 0xF1 // -> next certification ID
	scirePrefixEnrollCounter     byte = 0xF2 // -> next enrollment ID
	scirePrefixConfig            byte = 0xFF // -> ScireConfig
)

// Event names for Scire.
const (
	AccountCreatedEvent       = "AccountCreated"
	CreditsAllocatedEvent     = "CreditsAllocated"
	EnrollmentCreatedEvent    = "EnrollmentCreated"
	EnrollmentCompletedEvent  = "EnrollmentCompleted"
	EnrollmentWithdrawnEvent  = "EnrollmentWithdrawn"
	EnrollmentTransferredEvent = "EnrollmentTransferred"
	CertificationIssuedEvent  = "CertificationIssued"
	CertificationRevokedEvent = "CertificationRevoked"
	CertificationRenewedEvent = "CertificationRenewed"
)

// Role constants for educators.
const (
	RoleEducator uint64 = 20 // Can issue certifications and manage enrollments
)

// Various errors for Scire.
var (
	ErrScireAccountNotFound      = errors.New("education account not found")
	ErrScireAccountExists        = errors.New("education account already exists")
	ErrScireAccountSuspended     = errors.New("education account is suspended")
	ErrScireAccountClosed        = errors.New("education account is closed")
	ErrScireNoVita               = errors.New("owner must have an active Vita")
	ErrScireInsufficientCredits  = errors.New("insufficient education credits")
	ErrScireInvalidCredits       = errors.New("invalid credit amount")
	ErrScireCertNotFound         = errors.New("certification not found")
	ErrScireCertExpired          = errors.New("certification has expired")
	ErrScireCertRevoked          = errors.New("certification is revoked")
	ErrScireEnrollNotFound       = errors.New("enrollment not found")
	ErrScireEnrollNotActive      = errors.New("enrollment is not active")
	ErrScireAlreadyEnrolled      = errors.New("already enrolled in a program")
	ErrScireNotEducator          = errors.New("caller is not an authorized educator")
	ErrScireNotCommittee         = errors.New("invalid committee signature")
	ErrScireInvalidOwner         = errors.New("invalid owner address")
	ErrScireInvalidInstitution   = errors.New("invalid institution address")
	ErrScireInvalidProgramID     = errors.New("invalid program ID")
	ErrScireInvalidCertType      = errors.New("invalid certification type")
	ErrScireInvalidName          = errors.New("invalid certification name")
	ErrScireEducationRestricted  = errors.New("education right is restricted")
	ErrScireNotStudent           = errors.New("caller is not the student")
	ErrScireNotInstitution       = errors.New("caller is not the institution")
	ErrScireExceedsMaxCredits    = errors.New("exceeds maximum credits per program")
)

var (
	_ interop.Contract        = (*Scire)(nil)
	_ dao.NativeContractCache = (*ScireCache)(nil)
)

// Copy implements NativeContractCache interface.
func (c *ScireCache) Copy() dao.NativeContractCache {
	return &ScireCache{
		accountCount: c.accountCount,
		certCount:    c.certCount,
		enrollCount:  c.enrollCount,
	}
}

// checkCommittee checks if the caller has committee authority.
func (s *Scire) checkCommittee(ic *interop.Context) bool {
	if s.RoleRegistry != nil {
		return s.RoleRegistry.CheckCommittee(ic)
	}
	return s.Tutus.CheckCommittee(ic)
}

// checkEducator checks if the caller has educator authority.
func (s *Scire) checkEducator(ic *interop.Context) bool {
	caller := ic.VM.GetCallingScriptHash()
	if s.RoleRegistry != nil {
		if s.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleEducator, ic.Block.Index) {
			return true
		}
	}
	// Committee members can also act as educators
	return s.checkCommittee(ic)
}

// checkEducationRight checks if subject has education rights via Lex.
func (s *Scire) checkEducationRight(ic *interop.Context, subject util.Uint160) bool {
	if s.Lex == nil {
		return true // Allow if Lex not available
	}
	return s.Lex.HasRightInternal(ic.DAO, subject, state.RightEducation, ic.Block.Index)
}

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

	// ===== Account Management =====

	// createAccount - Create education account for a Vita holder
	desc := NewDescriptor("createAccount", smartcontract.BoolType,
		manifest.NewParameter("owner", smartcontract.Hash160Type))
	md := NewMethodAndPrice(s.createAccount, 1<<17, callflag.States|callflag.AllowNotify)
	s.AddMethod(md, desc)

	// getAccount - Get education account by owner
	desc = NewDescriptor("getAccount", smartcontract.ArrayType,
		manifest.NewParameter("owner", smartcontract.Hash160Type))
	md = NewMethodAndPrice(s.getAccount, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// getAccountByVitaID - Get account by Vita ID
	desc = NewDescriptor("getAccountByVitaID", smartcontract.ArrayType,
		manifest.NewParameter("vitaID", smartcontract.IntegerType))
	md = NewMethodAndPrice(s.getAccountByVitaID, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// allocateCredits - Allocate learning credits (committee only)
	desc = NewDescriptor("allocateCredits", smartcontract.BoolType,
		manifest.NewParameter("owner", smartcontract.Hash160Type),
		manifest.NewParameter("amount", smartcontract.IntegerType),
		manifest.NewParameter("reason", smartcontract.StringType))
	md = NewMethodAndPrice(s.allocateCredits, 1<<16, callflag.States|callflag.AllowNotify)
	s.AddMethod(md, desc)

	// getCredits - Get available credits
	desc = NewDescriptor("getCredits", smartcontract.IntegerType,
		manifest.NewParameter("owner", smartcontract.Hash160Type))
	md = NewMethodAndPrice(s.getCredits, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// ===== Enrollment Management =====

	// enroll - Enroll in an education program
	desc = NewDescriptor("enroll", smartcontract.IntegerType,
		manifest.NewParameter("student", smartcontract.Hash160Type),
		manifest.NewParameter("programID", smartcontract.StringType),
		manifest.NewParameter("institution", smartcontract.Hash160Type),
		manifest.NewParameter("credits", smartcontract.IntegerType))
	md = NewMethodAndPrice(s.enroll, 1<<17, callflag.States|callflag.AllowNotify)
	s.AddMethod(md, desc)

	// completeEnrollment - Mark enrollment as completed (institution only)
	desc = NewDescriptor("completeEnrollment", smartcontract.BoolType,
		manifest.NewParameter("enrollmentID", smartcontract.IntegerType),
		manifest.NewParameter("contentHash", smartcontract.Hash256Type))
	md = NewMethodAndPrice(s.completeEnrollment, 1<<16, callflag.States|callflag.AllowNotify)
	s.AddMethod(md, desc)

	// withdrawEnrollment - Withdraw from program
	desc = NewDescriptor("withdrawEnrollment", smartcontract.BoolType,
		manifest.NewParameter("enrollmentID", smartcontract.IntegerType),
		manifest.NewParameter("reason", smartcontract.StringType))
	md = NewMethodAndPrice(s.withdrawEnrollment, 1<<16, callflag.States|callflag.AllowNotify)
	s.AddMethod(md, desc)

	// getEnrollment - Get enrollment details
	desc = NewDescriptor("getEnrollment", smartcontract.ArrayType,
		manifest.NewParameter("enrollmentID", smartcontract.IntegerType))
	md = NewMethodAndPrice(s.getEnrollment, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// getActiveEnrollment - Get student's active enrollment
	desc = NewDescriptor("getActiveEnrollment", smartcontract.ArrayType,
		manifest.NewParameter("student", smartcontract.Hash160Type))
	md = NewMethodAndPrice(s.getActiveEnrollment, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// ===== Certification Management =====

	// issueCertification - Issue a certification (educator only)
	desc = NewDescriptor("issueCertification", smartcontract.IntegerType,
		manifest.NewParameter("owner", smartcontract.Hash160Type),
		manifest.NewParameter("certType", smartcontract.StringType),
		manifest.NewParameter("name", smartcontract.StringType),
		manifest.NewParameter("contentHash", smartcontract.Hash256Type),
		manifest.NewParameter("expiresAt", smartcontract.IntegerType))
	md = NewMethodAndPrice(s.issueCertification, 1<<17, callflag.States|callflag.AllowNotify)
	s.AddMethod(md, desc)

	// revokeCertification - Revoke a certification (institution only)
	desc = NewDescriptor("revokeCertification", smartcontract.BoolType,
		manifest.NewParameter("certID", smartcontract.IntegerType),
		manifest.NewParameter("reason", smartcontract.StringType))
	md = NewMethodAndPrice(s.revokeCertification, 1<<16, callflag.States|callflag.AllowNotify)
	s.AddMethod(md, desc)

	// renewCertification - Extend certification expiry
	desc = NewDescriptor("renewCertification", smartcontract.BoolType,
		manifest.NewParameter("certID", smartcontract.IntegerType),
		manifest.NewParameter("newExpiresAt", smartcontract.IntegerType))
	md = NewMethodAndPrice(s.renewCertification, 1<<16, callflag.States|callflag.AllowNotify)
	s.AddMethod(md, desc)

	// getCertification - Get certification details
	desc = NewDescriptor("getCertification", smartcontract.ArrayType,
		manifest.NewParameter("certID", smartcontract.IntegerType))
	md = NewMethodAndPrice(s.getCertification, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// verifyCertification - Check if certification is valid
	desc = NewDescriptor("verifyCertification", smartcontract.BoolType,
		manifest.NewParameter("certID", smartcontract.IntegerType))
	md = NewMethodAndPrice(s.verifyCertification, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// hasCertification - Check if owner has specific cert type
	desc = NewDescriptor("hasCertification", smartcontract.BoolType,
		manifest.NewParameter("owner", smartcontract.Hash160Type),
		manifest.NewParameter("certType", smartcontract.StringType))
	md = NewMethodAndPrice(s.hasCertification, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

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

	// getConfig - Get Scire configuration
	desc = NewDescriptor("getConfig", smartcontract.ArrayType)
	md = NewMethodAndPrice(s.getConfig, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// getTotalAccounts - Get total education accounts
	desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType)
	md = NewMethodAndPrice(s.getTotalAccounts, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// getTotalCertifications - Get total certifications issued
	desc = NewDescriptor("getTotalCertifications", smartcontract.IntegerType)
	md = NewMethodAndPrice(s.getTotalCertifications, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// getTotalEnrollments - Get total enrollments
	desc = NewDescriptor("getTotalEnrollments", smartcontract.IntegerType)
	md = NewMethodAndPrice(s.getTotalEnrollments, 1<<15, callflag.ReadStates)
	s.AddMethod(md, desc)

	// ===== Events =====

	// AccountCreated event
	eDesc := NewEventDescriptor(AccountCreatedEvent,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("owner", smartcontract.Hash160Type))
	s.AddEvent(NewEvent(eDesc))

	// CreditsAllocated event
	eDesc = NewEventDescriptor(CreditsAllocatedEvent,
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("amount", smartcontract.IntegerType),
		manifest.NewParameter("total", smartcontract.IntegerType))
	s.AddEvent(NewEvent(eDesc))

	// EnrollmentCreated event
	eDesc = NewEventDescriptor(EnrollmentCreatedEvent,
		manifest.NewParameter("enrollmentID", smartcontract.IntegerType),
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("programID", smartcontract.StringType))
	s.AddEvent(NewEvent(eDesc))

	// EnrollmentCompleted event
	eDesc = NewEventDescriptor(EnrollmentCompletedEvent,
		manifest.NewParameter("enrollmentID", smartcontract.IntegerType),
		manifest.NewParameter("vitaID", smartcontract.IntegerType))
	s.AddEvent(NewEvent(eDesc))

	// EnrollmentWithdrawn event
	eDesc = NewEventDescriptor(EnrollmentWithdrawnEvent,
		manifest.NewParameter("enrollmentID", smartcontract.IntegerType),
		manifest.NewParameter("reason", smartcontract.StringType))
	s.AddEvent(NewEvent(eDesc))

	// CertificationIssued event
	eDesc = NewEventDescriptor(CertificationIssuedEvent,
		manifest.NewParameter("certID", smartcontract.IntegerType),
		manifest.NewParameter("vitaID", smartcontract.IntegerType),
		manifest.NewParameter("certType", smartcontract.StringType))
	s.AddEvent(NewEvent(eDesc))

	// CertificationRevoked event
	eDesc = NewEventDescriptor(CertificationRevokedEvent,
		manifest.NewParameter("certID", smartcontract.IntegerType),
		manifest.NewParameter("reason", smartcontract.StringType))
	s.AddEvent(NewEvent(eDesc))

	// CertificationRenewed event
	eDesc = NewEventDescriptor(CertificationRenewedEvent,
		manifest.NewParameter("certID", smartcontract.IntegerType),
		manifest.NewParameter("newExpiresAt", smartcontract.IntegerType))
	s.AddEvent(NewEvent(eDesc))

	return s
}

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

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

	// Initialize counters
	s.setAccountCounter(ic.DAO, 0)
	s.setCertCounter(ic.DAO, 0)
	s.setEnrollCounter(ic.DAO, 0)

	// Initialize config with defaults
	cfg := &state.ScireConfig{
		AnnualCreditAllocation: 1000,      // 1000 credits per year
		MaxCreditsPerProgram:   500,       // Max 500 credits per program
		CertificationFee:       0,         // Free certification
		MinEnrollmentDuration:  86400,     // ~1 day in blocks (1-second blocks)
	}
	s.setConfig(ic.DAO, cfg)

	// Initialize cache
	cache := &ScireCache{
		accountCount: 0,
		certCount:    0,
		enrollCount:  0,
	}
	ic.DAO.SetCache(s.ID, cache)

	return nil
}

// InitializeCache initializes the cache from storage.
func (s *Scire) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
	cache := &ScireCache{
		accountCount: s.getAccountCounter(d),
		certCount:    s.getCertCounter(d),
		enrollCount:  s.getEnrollCounter(d),
	}
	d.SetCache(s.ID, cache)
	return nil
}

// OnPersist is called before block is committed.
func (s *Scire) OnPersist(ic *interop.Context) error {
	return nil
}

// PostPersist is called after block is committed.
func (s *Scire) PostPersist(ic *interop.Context) error {
	return nil
}

// ActiveIn returns the hardfork at which this contract is activated.
func (s *Scire) ActiveIn() *config.Hardfork {
	return nil // Always active
}

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

func (s *Scire) makeAccountKey(vitaID uint64) []byte {
	key := make([]byte, 9)
	key[0] = scirePrefixAccount
	binary.BigEndian.PutUint64(key[1:], vitaID)
	return key
}

func (s *Scire) makeAccountByOwnerKey(owner util.Uint160) []byte {
	key := make([]byte, 21)
	key[0] = scirePrefixAccountByOwner
	copy(key[1:], owner.BytesBE())
	return key
}

func (s *Scire) makeCertificationKey(certID uint64) []byte {
	key := make([]byte, 9)
	key[0] = scirePrefixCertification
	binary.BigEndian.PutUint64(key[1:], certID)
	return key
}

func (s *Scire) makeCertByOwnerKey(vitaID, certID uint64) []byte {
	key := make([]byte, 17)
	key[0] = scirePrefixCertByOwner
	binary.BigEndian.PutUint64(key[1:9], vitaID)
	binary.BigEndian.PutUint64(key[9:], certID)
	return key
}

func (s *Scire) makeEnrollmentKey(enrollID uint64) []byte {
	key := make([]byte, 9)
	key[0] = scirePrefixEnrollment
	binary.BigEndian.PutUint64(key[1:], enrollID)
	return key
}

func (s *Scire) makeActiveEnrollmentKey(vitaID uint64) []byte {
	key := make([]byte, 9)
	key[0] = scirePrefixActiveEnrollment
	binary.BigEndian.PutUint64(key[1:], vitaID)
	return key
}

// Counter getters/setters
func (s *Scire) getAccountCounter(d *dao.Simple) uint64 {
	si := d.GetStorageItem(s.ID, []byte{scirePrefixAccountCounter})
	if si == nil {
		return 0
	}
	return binary.BigEndian.Uint64(si)
}

func (s *Scire) setAccountCounter(d *dao.Simple, count uint64) {
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, count)
	d.PutStorageItem(s.ID, []byte{scirePrefixAccountCounter}, buf)
}

func (s *Scire) getCertCounter(d *dao.Simple) uint64 {
	si := d.GetStorageItem(s.ID, []byte{scirePrefixCertCounter})
	if si == nil {
		return 0
	}
	return binary.BigEndian.Uint64(si)
}

func (s *Scire) setCertCounter(d *dao.Simple, count uint64) {
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, count)
	d.PutStorageItem(s.ID, []byte{scirePrefixCertCounter}, buf)
}

func (s *Scire) getEnrollCounter(d *dao.Simple) uint64 {
	si := d.GetStorageItem(s.ID, []byte{scirePrefixEnrollCounter})
	if si == nil {
		return 0
	}
	return binary.BigEndian.Uint64(si)
}

func (s *Scire) setEnrollCounter(d *dao.Simple, count uint64) {
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, count)
	d.PutStorageItem(s.ID, []byte{scirePrefixEnrollCounter}, buf)
}

// Config getter/setter
func (s *Scire) getConfigInternal(d *dao.Simple) *state.ScireConfig {
	si := d.GetStorageItem(s.ID, []byte{scirePrefixConfig})
	if si == nil {
		return &state.ScireConfig{
			AnnualCreditAllocation: 1000,
			MaxCreditsPerProgram:   500,
			CertificationFee:       0,
			MinEnrollmentDuration:  86400,
		}
	}
	cfg := new(state.ScireConfig)
	item, _ := stackitem.Deserialize(si)
	cfg.FromStackItem(item)
	return cfg
}

func (s *Scire) setConfig(d *dao.Simple, cfg *state.ScireConfig) {
	item, _ := cfg.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(s.ID, []byte{scirePrefixConfig}, data)
}

// Account storage
func (s *Scire) getAccountInternal(d *dao.Simple, vitaID uint64) *state.EducationAccount {
	si := d.GetStorageItem(s.ID, s.makeAccountKey(vitaID))
	if si == nil {
		return nil
	}
	acc := new(state.EducationAccount)
	item, _ := stackitem.Deserialize(si)
	acc.FromStackItem(item)
	return acc
}

func (s *Scire) putAccount(d *dao.Simple, acc *state.EducationAccount) {
	item, _ := acc.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(s.ID, s.makeAccountKey(acc.VitaID), data)
}

func (s *Scire) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) {
	si := d.GetStorageItem(s.ID, s.makeAccountByOwnerKey(owner))
	if si == nil {
		return 0, false
	}
	return binary.BigEndian.Uint64(si), true
}

func (s *Scire) setOwnerToVitaID(d *dao.Simple, owner util.Uint160, vitaID uint64) {
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, vitaID)
	d.PutStorageItem(s.ID, s.makeAccountByOwnerKey(owner), buf)
}

// Enrollment storage
func (s *Scire) getEnrollmentInternal(d *dao.Simple, enrollID uint64) *state.Enrollment {
	si := d.GetStorageItem(s.ID, s.makeEnrollmentKey(enrollID))
	if si == nil {
		return nil
	}
	enroll := new(state.Enrollment)
	item, _ := stackitem.Deserialize(si)
	enroll.FromStackItem(item)
	return enroll
}

func (s *Scire) putEnrollment(d *dao.Simple, enroll *state.Enrollment) {
	item, _ := enroll.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(s.ID, s.makeEnrollmentKey(enroll.ID), data)
}

func (s *Scire) getActiveEnrollmentID(d *dao.Simple, vitaID uint64) uint64 {
	si := d.GetStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID))
	if si == nil {
		return 0
	}
	return binary.BigEndian.Uint64(si)
}

func (s *Scire) setActiveEnrollmentID(d *dao.Simple, vitaID, enrollID uint64) {
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, enrollID)
	d.PutStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID), buf)
}

func (s *Scire) clearActiveEnrollment(d *dao.Simple, vitaID uint64) {
	d.DeleteStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID))
}

// Certification storage
func (s *Scire) getCertificationInternal(d *dao.Simple, certID uint64) *state.Certification {
	si := d.GetStorageItem(s.ID, s.makeCertificationKey(certID))
	if si == nil {
		return nil
	}
	cert := new(state.Certification)
	item, _ := stackitem.Deserialize(si)
	cert.FromStackItem(item)
	return cert
}

func (s *Scire) putCertification(d *dao.Simple, cert *state.Certification) {
	item, _ := cert.ToStackItem()
	data, _ := stackitem.Serialize(item)
	d.PutStorageItem(s.ID, s.makeCertificationKey(cert.ID), data)
}

func (s *Scire) setCertByOwner(d *dao.Simple, vitaID, certID uint64) {
	d.PutStorageItem(s.ID, s.makeCertByOwnerKey(vitaID, certID), []byte{1})
}

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

// createAccount creates an education account for a Vita holder.
func (s *Scire) createAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])

	// Check owner has active Vita
	if s.Vita == nil {
		panic(ErrScireNoVita)
	}
	vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner)
	if err != nil || vita == nil {
		panic(ErrScireNoVita)
	}
	if vita.Status != state.TokenStatusActive {
		panic(ErrScireNoVita)
	}

	// Check if account already exists
	existing := s.getAccountInternal(ic.DAO, vita.TokenID)
	if existing != nil {
		panic(ErrScireAccountExists)
	}

	// Check education rights
	if !s.checkEducationRight(ic, owner) {
		// Log but allow (EnforcementLogging)
		// In the future, we could emit an event here
	}

	// Get cache and increment counter
	cache := ic.DAO.GetRWCache(s.ID).(*ScireCache)
	accountNum := cache.accountCount
	cache.accountCount++
	s.setAccountCounter(ic.DAO, cache.accountCount)

	// Create account
	acc := &state.EducationAccount{
		VitaID:           vita.TokenID,
		Owner:            owner,
		TotalCredits:     0,
		UsedCredits:      0,
		AvailableCredits: 0,
		Status:           state.EducationAccountActive,
		CreatedAt:        ic.Block.Index,
		UpdatedAt:        ic.Block.Index,
	}

	// Store account
	s.putAccount(ic.DAO, acc)
	s.setOwnerToVitaID(ic.DAO, owner, vita.TokenID)

	// Emit event
	ic.AddNotification(s.Hash, AccountCreatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))),
		stackitem.NewByteArray(owner.BytesBE()),
	}))

	_ = accountNum // suppress unused warning
	return stackitem.NewBool(true)
}

// getAccount returns education account by owner.
func (s *Scire) getAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])

	vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
	if !found {
		return stackitem.Null{}
	}

	acc := s.getAccountInternal(ic.DAO, vitaID)
	if acc == nil {
		return stackitem.Null{}
	}

	item, _ := acc.ToStackItem()
	return item
}

// getAccountByVitaID returns education account by Vita ID.
func (s *Scire) getAccountByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	vitaID := toUint64(args[0])

	acc := s.getAccountInternal(ic.DAO, vitaID)
	if acc == nil {
		return stackitem.Null{}
	}

	item, _ := acc.ToStackItem()
	return item
}

// allocateCredits allocates learning credits to an account (committee only).
func (s *Scire) allocateCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])
	amount := toUint64(args[1])
	// reason := toString(args[2]) // for logging

	// Committee only
	if !s.checkCommittee(ic) {
		panic(ErrScireNotCommittee)
	}

	if amount == 0 {
		panic(ErrScireInvalidCredits)
	}

	// Get or create account
	vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
	if !found {
		// Auto-create account if Vita exists
		if s.Vita == nil {
			panic(ErrScireNoVita)
		}
		vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner)
		if err != nil || vita == nil || vita.Status != state.TokenStatusActive {
			panic(ErrScireNoVita)
		}
		vitaID = vita.TokenID

		// Create account
		cache := ic.DAO.GetRWCache(s.ID).(*ScireCache)
		cache.accountCount++
		s.setAccountCounter(ic.DAO, cache.accountCount)

		acc := &state.EducationAccount{
			VitaID:           vitaID,
			Owner:            owner,
			TotalCredits:     amount,
			UsedCredits:      0,
			AvailableCredits: amount,
			Status:           state.EducationAccountActive,
			CreatedAt:        ic.Block.Index,
			UpdatedAt:        ic.Block.Index,
		}
		s.putAccount(ic.DAO, acc)
		s.setOwnerToVitaID(ic.DAO, owner, vitaID)

		// Emit events
		ic.AddNotification(s.Hash, AccountCreatedEvent, stackitem.NewArray([]stackitem.Item{
			stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
			stackitem.NewByteArray(owner.BytesBE()),
		}))
		ic.AddNotification(s.Hash, CreditsAllocatedEvent, stackitem.NewArray([]stackitem.Item{
			stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
			stackitem.NewBigInteger(big.NewInt(int64(amount))),
			stackitem.NewBigInteger(big.NewInt(int64(amount))),
		}))

		return stackitem.NewBool(true)
	}

	acc := s.getAccountInternal(ic.DAO, vitaID)
	if acc == nil {
		panic(ErrScireAccountNotFound)
	}
	if acc.Status != state.EducationAccountActive {
		panic(ErrScireAccountSuspended)
	}

	// Add credits
	acc.TotalCredits += amount
	acc.AvailableCredits += amount
	acc.UpdatedAt = ic.Block.Index

	s.putAccount(ic.DAO, acc)

	// Emit event
	ic.AddNotification(s.Hash, CreditsAllocatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
		stackitem.NewBigInteger(big.NewInt(int64(amount))),
		stackitem.NewBigInteger(big.NewInt(int64(acc.AvailableCredits))),
	}))

	return stackitem.NewBool(true)
}

// getCredits returns available credits for an owner.
func (s *Scire) getCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])

	vitaID, found := s.getVitaIDByOwner(ic.DAO, owner)
	if !found {
		return stackitem.NewBigInteger(big.NewInt(0))
	}

	acc := s.getAccountInternal(ic.DAO, vitaID)
	if acc == nil {
		return stackitem.NewBigInteger(big.NewInt(0))
	}

	return stackitem.NewBigInteger(big.NewInt(int64(acc.AvailableCredits)))
}

// enroll enrolls a student in an education program.
func (s *Scire) enroll(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	student := toUint160(args[0])
	programID := toString(args[1])
	institution := toUint160(args[2])
	credits := toUint64(args[3])

	// Validate inputs
	if len(programID) == 0 || len(programID) > 128 {
		panic(ErrScireInvalidProgramID)
	}
	if credits == 0 {
		panic(ErrScireInvalidCredits)
	}

	// Check max credits
	cfg := s.getConfigInternal(ic.DAO)
	if credits > cfg.MaxCreditsPerProgram {
		panic(ErrScireExceedsMaxCredits)
	}

	// Get student's account
	vitaID, found := s.getVitaIDByOwner(ic.DAO, student)
	if !found {
		panic(ErrScireAccountNotFound)
	}

	acc := s.getAccountInternal(ic.DAO, vitaID)
	if acc == nil {
		panic(ErrScireAccountNotFound)
	}
	if acc.Status != state.EducationAccountActive {
		panic(ErrScireAccountSuspended)
	}

	// Check sufficient credits
	if acc.AvailableCredits < credits {
		panic(ErrScireInsufficientCredits)
	}

	// Check not already enrolled
	activeEnrollID := s.getActiveEnrollmentID(ic.DAO, vitaID)
	if activeEnrollID != 0 {
		panic(ErrScireAlreadyEnrolled)
	}

	// Check education rights
	if !s.checkEducationRight(ic, student) {
		// Log but allow
	}

	// Get next enrollment ID
	cache := ic.DAO.GetRWCache(s.ID).(*ScireCache)
	enrollID := cache.enrollCount
	cache.enrollCount++
	s.setEnrollCounter(ic.DAO, cache.enrollCount)

	// Deduct credits
	acc.UsedCredits += credits
	acc.AvailableCredits -= credits
	acc.UpdatedAt = ic.Block.Index
	s.putAccount(ic.DAO, acc)

	// Create enrollment
	enroll := &state.Enrollment{
		ID:               enrollID,
		VitaID:           vitaID,
		Student:          student,
		ProgramID:        programID,
		Institution:      institution,
		CreditsAllocated: credits,
		StartedAt:        ic.Block.Index,
		CompletedAt:      0,
		Status:           state.EnrollmentActive,
	}

	s.putEnrollment(ic.DAO, enroll)
	s.setActiveEnrollmentID(ic.DAO, vitaID, enrollID)

	// Emit event
	ic.AddNotification(s.Hash, EnrollmentCreatedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(enrollID))),
		stackitem.NewBigInteger(big.NewInt(int64(vitaID))),
		stackitem.NewByteArray([]byte(programID)),
	}))

	return stackitem.NewBigInteger(big.NewInt(int64(enrollID)))
}

// completeEnrollment marks an enrollment as completed (institution/educator only).
func (s *Scire) completeEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	enrollID := toUint64(args[0])
	// contentHash := toUint256(args[1]) // for proof storage

	enroll := s.getEnrollmentInternal(ic.DAO, enrollID)
	if enroll == nil {
		panic(ErrScireEnrollNotFound)
	}
	if enroll.Status != state.EnrollmentActive {
		panic(ErrScireEnrollNotActive)
	}

	// Check caller is institution or educator
	caller := ic.VM.GetCallingScriptHash()
	if caller != enroll.Institution && !s.checkEducator(ic) {
		panic(ErrScireNotInstitution)
	}

	// Update enrollment
	enroll.Status = state.EnrollmentCompleted
	enroll.CompletedAt = ic.Block.Index
	s.putEnrollment(ic.DAO, enroll)

	// Clear active enrollment
	s.clearActiveEnrollment(ic.DAO, enroll.VitaID)

	// Emit event
	ic.AddNotification(s.Hash, EnrollmentCompletedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(enrollID))),
		stackitem.NewBigInteger(big.NewInt(int64(enroll.VitaID))),
	}))

	return stackitem.NewBool(true)
}

// withdrawEnrollment withdraws from an enrollment.
func (s *Scire) withdrawEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	enrollID := toUint64(args[0])
	reason := toString(args[1])

	enroll := s.getEnrollmentInternal(ic.DAO, enrollID)
	if enroll == nil {
		panic(ErrScireEnrollNotFound)
	}
	if enroll.Status != state.EnrollmentActive {
		panic(ErrScireEnrollNotActive)
	}

	// Check caller is student or institution
	caller := ic.VM.GetCallingScriptHash()
	if caller != enroll.Student && caller != enroll.Institution && !s.checkEducator(ic) {
		panic(ErrScireNotStudent)
	}

	// Update enrollment
	enroll.Status = state.EnrollmentWithdrawn
	enroll.CompletedAt = ic.Block.Index
	s.putEnrollment(ic.DAO, enroll)

	// Clear active enrollment
	s.clearActiveEnrollment(ic.DAO, enroll.VitaID)

	// Partial refund: return 50% of credits
	acc := s.getAccountInternal(ic.DAO, enroll.VitaID)
	if acc != nil {
		refund := enroll.CreditsAllocated / 2
		acc.AvailableCredits += refund
		acc.UsedCredits -= refund
		acc.UpdatedAt = ic.Block.Index
		s.putAccount(ic.DAO, acc)
	}

	// Emit event
	ic.AddNotification(s.Hash, EnrollmentWithdrawnEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(enrollID))),
		stackitem.NewByteArray([]byte(reason)),
	}))

	return stackitem.NewBool(true)
}

// getEnrollment returns enrollment details.
func (s *Scire) getEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	enrollID := toUint64(args[0])

	enroll := s.getEnrollmentInternal(ic.DAO, enrollID)
	if enroll == nil {
		return stackitem.Null{}
	}

	item, _ := enroll.ToStackItem()
	return item
}

// getActiveEnrollment returns a student's active enrollment.
func (s *Scire) getActiveEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	student := toUint160(args[0])

	vitaID, found := s.getVitaIDByOwner(ic.DAO, student)
	if !found {
		return stackitem.Null{}
	}

	enrollID := s.getActiveEnrollmentID(ic.DAO, vitaID)
	if enrollID == 0 {
		return stackitem.Null{}
	}

	enroll := s.getEnrollmentInternal(ic.DAO, enrollID)
	if enroll == nil {
		return stackitem.Null{}
	}

	item, _ := enroll.ToStackItem()
	return item
}

// issueCertification issues a certification (educator only).
func (s *Scire) issueCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])
	certType := toString(args[1])
	name := toString(args[2])
	contentHashBytes := toBytes(args[3])
	expiresAt := toUint32(args[4])

	// Convert bytes to Uint256
	var contentHash util.Uint256
	if len(contentHashBytes) == 32 {
		copy(contentHash[:], contentHashBytes)
	}

	// Validate inputs
	if len(certType) == 0 || len(certType) > 64 {
		panic(ErrScireInvalidCertType)
	}
	if len(name) == 0 || len(name) > 128 {
		panic(ErrScireInvalidName)
	}

	// Check educator authority
	if !s.checkEducator(ic) {
		panic(ErrScireNotEducator)
	}

	// Get owner's Vita
	if s.Vita == nil {
		panic(ErrScireNoVita)
	}
	vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner)
	if err != nil || vita == nil || vita.Status != state.TokenStatusActive {
		panic(ErrScireNoVita)
	}

	// Get issuing institution
	institution := ic.VM.GetCallingScriptHash()

	// Get next cert ID
	cache := ic.DAO.GetRWCache(s.ID).(*ScireCache)
	certID := cache.certCount
	cache.certCount++
	s.setCertCounter(ic.DAO, cache.certCount)

	// Create certification
	cert := &state.Certification{
		ID:          certID,
		VitaID:      vita.TokenID,
		Owner:       owner,
		CertType:    certType,
		Name:        name,
		Institution: institution,
		ContentHash: contentHash,
		IssuedAt:    ic.Block.Index,
		ExpiresAt:   expiresAt,
		Status:      state.CertificationActive,
	}

	s.putCertification(ic.DAO, cert)
	s.setCertByOwner(ic.DAO, vita.TokenID, certID)

	// Emit event
	ic.AddNotification(s.Hash, CertificationIssuedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(certID))),
		stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))),
		stackitem.NewByteArray([]byte(certType)),
	}))

	return stackitem.NewBigInteger(big.NewInt(int64(certID)))
}

// revokeCertification revokes a certification (institution only).
func (s *Scire) revokeCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	certID := toUint64(args[0])
	reason := toString(args[1])

	cert := s.getCertificationInternal(ic.DAO, certID)
	if cert == nil {
		panic(ErrScireCertNotFound)
	}
	if cert.Status == state.CertificationRevoked {
		panic(ErrScireCertRevoked)
	}

	// Check caller is issuing institution or educator
	caller := ic.VM.GetCallingScriptHash()
	if caller != cert.Institution && !s.checkEducator(ic) {
		panic(ErrScireNotInstitution)
	}

	// Revoke
	cert.Status = state.CertificationRevoked
	s.putCertification(ic.DAO, cert)

	// Emit event
	ic.AddNotification(s.Hash, CertificationRevokedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(certID))),
		stackitem.NewByteArray([]byte(reason)),
	}))

	return stackitem.NewBool(true)
}

// renewCertification extends a certification's expiry.
func (s *Scire) renewCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	certID := toUint64(args[0])
	newExpiresAt := toUint32(args[1])

	cert := s.getCertificationInternal(ic.DAO, certID)
	if cert == nil {
		panic(ErrScireCertNotFound)
	}
	if cert.Status == state.CertificationRevoked {
		panic(ErrScireCertRevoked)
	}

	// Check caller is issuing institution or educator
	caller := ic.VM.GetCallingScriptHash()
	if caller != cert.Institution && !s.checkEducator(ic) {
		panic(ErrScireNotInstitution)
	}

	// Update expiry
	cert.ExpiresAt = newExpiresAt
	cert.Status = state.CertificationActive // Reactivate if was expired
	s.putCertification(ic.DAO, cert)

	// Emit event
	ic.AddNotification(s.Hash, CertificationRenewedEvent, stackitem.NewArray([]stackitem.Item{
		stackitem.NewBigInteger(big.NewInt(int64(certID))),
		stackitem.NewBigInteger(big.NewInt(int64(newExpiresAt))),
	}))

	return stackitem.NewBool(true)
}

// getCertification returns certification details.
func (s *Scire) getCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	certID := toUint64(args[0])

	cert := s.getCertificationInternal(ic.DAO, certID)
	if cert == nil {
		return stackitem.Null{}
	}

	item, _ := cert.ToStackItem()
	return item
}

// verifyCertification checks if a certification is currently valid.
func (s *Scire) verifyCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	certID := toUint64(args[0])

	cert := s.getCertificationInternal(ic.DAO, certID)
	if cert == nil {
		return stackitem.NewBool(false)
	}

	return stackitem.NewBool(cert.IsValid(ic.Block.Index))
}

// hasCertification checks if an owner has a specific certification type.
func (s *Scire) hasCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	owner := toUint160(args[0])
	certType := toString(args[1])

	vitaID, exists := s.getVitaIDByOwner(ic.DAO, owner)
	if !exists {
		return stackitem.NewBool(false)
	}

	// Scan certifications for this owner
	prefix := make([]byte, 9)
	prefix[0] = scirePrefixCertByOwner
	binary.BigEndian.PutUint64(prefix[1:], vitaID)

	var hasCert bool
	ic.DAO.Seek(s.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) >= 8 {
			certID := binary.BigEndian.Uint64(k[len(k)-8:])
			cert := s.getCertificationInternal(ic.DAO, certID)
			if cert != nil && cert.CertType == certType && cert.IsValid(ic.Block.Index) {
				hasCert = true
				return false // Stop iteration
			}
		}
		return true
	})

	return stackitem.NewBool(hasCert)
}

// getConfig returns the Scire configuration.
func (s *Scire) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	cfg := s.getConfigInternal(ic.DAO)
	item, _ := cfg.ToStackItem()
	return item
}

// getTotalAccounts returns the total number of education accounts.
func (s *Scire) getTotalAccounts(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	cache := ic.DAO.GetROCache(s.ID).(*ScireCache)
	return stackitem.NewBigInteger(big.NewInt(int64(cache.accountCount)))
}

// getTotalCertifications returns the total number of certifications.
func (s *Scire) getTotalCertifications(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	cache := ic.DAO.GetROCache(s.ID).(*ScireCache)
	return stackitem.NewBigInteger(big.NewInt(int64(cache.certCount)))
}

// getTotalEnrollments returns the total number of enrollments.
func (s *Scire) getTotalEnrollments(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	cache := ic.DAO.GetROCache(s.ID).(*ScireCache)
	return stackitem.NewBigInteger(big.NewInt(int64(cache.enrollCount)))
}

// ===== Public Interface Methods for Cross-Contract Access =====

// GetAccountByOwner returns an education account by owner address.
func (s *Scire) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.EducationAccount, error) {
	vitaID, found := s.getVitaIDByOwner(d, owner)
	if !found {
		return nil, ErrScireAccountNotFound
	}
	acc := s.getAccountInternal(d, vitaID)
	if acc == nil {
		return nil, ErrScireAccountNotFound
	}
	return acc, nil
}

// HasValidCertification checks if owner has a valid certification of the given type.
func (s *Scire) HasValidCertification(d *dao.Simple, owner util.Uint160, certType string, blockHeight uint32) bool {
	vitaID, exists := s.getVitaIDByOwner(d, owner)
	if !exists {
		return false
	}

	prefix := make([]byte, 9)
	prefix[0] = scirePrefixCertByOwner
	binary.BigEndian.PutUint64(prefix[1:], vitaID)

	var hasCert bool
	d.Seek(s.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
		if len(k) >= 8 {
			certID := binary.BigEndian.Uint64(k[len(k)-8:])
			cert := s.getCertificationInternal(d, certID)
			if cert != nil && cert.CertType == certType {
				if cert.Status == state.CertificationActive {
					if cert.ExpiresAt == 0 || cert.ExpiresAt > blockHeight {
						hasCert = true
						return false
					}
				}
			}
		}
		return true
	})

	return hasCert
}

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