package auditcontract

import (
	"github.com/nspcc-dev/neo-go/pkg/interop/binary"
	"github.com/nspcc-dev/neo-go/pkg/interop/contract"
	"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
	"github.com/nspcc-dev/neo-go/pkg/interop/storage"
	"github.com/nspcc-dev/neo-go/pkg/interop/util"
)

type (
	irNode struct {
		key []byte
	}

	CheckedNode struct {
		Key    []byte // 33 bytes
		Pair   int    // 2 bytes
		Reward int    // ? up to 32 byte
	}

	AuditResult struct {
		InnerRingNode  []byte // 33 bytes
		Epoch          int    // 8 bytes
		ContainerID    []byte // 32 bytes
		StorageGroupID []byte // 16 bytes
		PoR            bool   // 1 byte
		PDP            bool   // 1 byte
		// --- 91 bytes -- //
		// --- 2 more bytes to size of the []CheckedNode //
		Nodes []CheckedNode // <= 67 bytes per node
		// about 1400 nodes may be presented in container
	}
)

const (
	version = 1

	// 1E-8 GAS in precision of balance container.
	// This value may be calculated in runtime based on decimal value of
	// balance contract. We can also provide methods to change fee
	// in runtime.
	auditFee = 1 * 100_000_000

	ownerIDLength = 25

	journalKey           = "auditJournal"
	balanceContractKey   = "balanceScriptHash"
	containerContractKey = "containerScriptHash"
	netmapContractKey    = "netmapScriptHash"
)

var (
	auditFeeTransferMsg    = []byte("audit execution fee")
	auditRewardTransferMsg = []byte("data audit reward")

	ctx storage.Context
)

func init() {
	if runtime.GetTrigger() != runtime.Application {
		panic("contract has not been called in application node")
	}

	ctx = storage.GetContext()
}

func Init(addrNetmap, addrBalance, addrContainer []byte) {
	if storage.Get(ctx, netmapContractKey) != nil &&
		storage.Get(ctx, balanceContractKey) != nil &&
		storage.Get(ctx, containerContractKey) != nil {
		panic("init: contract already deployed")
	}

	if len(addrNetmap) != 20 || len(addrBalance) != 20 || len(addrContainer) != 20 {
		panic("init: incorrect length of contract script hash")
	}

	storage.Put(ctx, netmapContractKey, addrNetmap)
	storage.Put(ctx, balanceContractKey, addrBalance)
	storage.Put(ctx, containerContractKey, addrContainer)

	setSerialized(ctx, journalKey, []AuditResult{})

	runtime.Log("audit contract initialized")
}

func Put(rawAuditResult []byte) bool {
	netmapContractAddr := storage.Get(ctx, netmapContractKey).([]byte)
	innerRing := contract.Call(netmapContractAddr, "innerRingList").([]irNode)

	auditResult, err := newAuditResult(rawAuditResult)
	if err {
		panic("put: can't parse audit result")
	}

	var presented = false

	for i := range innerRing {
		ir := innerRing[i]
		if bytesEqual(ir.key, auditResult.InnerRingNode) {
			presented = true
			break
		}
	}

	if !runtime.CheckWitness(auditResult.InnerRingNode) || !presented {
		panic("put: access denied")
	}

	// todo: limit size of the audit journal:
	//       history will be stored in chain (args or notifies)
	//       contract storage will be used as a cache if needed
	journal := getAuditResult(ctx)
	journal = append(journal, auditResult)

	setSerialized(ctx, journalKey, journal)

	if auditResult.PDP && auditResult.PoR {
		// find who is the ownerID
		containerContract := storage.Get(ctx, containerContractKey).([]byte)

		// todo: implement easy way to get owner from the container id
		ownerID := contract.Call(containerContract, "owner", auditResult.ContainerID).([]byte)
		if len(ownerID) != ownerIDLength {
			runtime.Log("put: can't get owner id of the container")

			return false
		}

		ownerScriptHash := walletToScripHash(ownerID)

		// transfer fee to the inner ring node
		balanceContract := storage.Get(ctx, balanceContractKey).([]byte)
		irScriptHash := contract.CreateStandardAccount(auditResult.InnerRingNode)

		tx := contract.Call(balanceContract, "transferX",
			ownerScriptHash,
			irScriptHash,
			auditFee,
			auditFeeTransferMsg, // todo: add epoch, container and storage group info
		)
		if !tx.(bool) {
			panic("put: can't transfer inner ring fee")
		}

		for i := 0; i < len(auditResult.Nodes); i++ {
			node := auditResult.Nodes[i]
			nodeScriptHash := contract.CreateStandardAccount(node.Key)

			tx := contract.Call(balanceContract, "transferX",
				ownerScriptHash,
				nodeScriptHash,
				node.Reward,
				auditRewardTransferMsg, // todo: add epoch, container and storage group info
			)
			if !tx.(bool) {
				runtime.Log("put: can't transfer storage payment")

				return false
			}
		}
	}

	return true
}

func Version() int {
	return version
}

func newAuditResult(data []byte) (AuditResult, bool) {
	var (
		tmp    interface{}
		ln     = len(data)
		result = AuditResult{
			InnerRingNode:  nil, // neo-go#949
			ContainerID:    nil,
			StorageGroupID: nil,
			Nodes:          []CheckedNode{},
		}
	)

	if len(data) < 91 { // all required headers
		runtime.Log("newAuditResult: can't parse audit result header")
		return result, true
	}

	result.InnerRingNode = data[0:33]

	epoch := data[33:41]
	tmp = epoch
	result.Epoch = tmp.(int)

	result.ContainerID = data[41:73]
	result.StorageGroupID = data[73:89]
	result.PoR = util.Equals(data[90], 0x01)
	result.PDP = util.Equals(data[91], 0x01)

	// if there are nodes, that were checked
	if len(data) > 93 {
		rawCounter := data[91:93]
		tmp = rawCounter
		counter := tmp.(int)

		ptr := 93

		for i := 0; i < counter; i++ {
			if ptr+33+2+32 > ln {
				runtime.Log("newAuditResult: broken node")
				return result, false
			}

			node := CheckedNode{
				Key: nil, // neo-go#949
			}
			node.Key = data[ptr : ptr+33]

			pair := data[ptr+33 : ptr+35]
			tmp = pair
			node.Pair = tmp.(int)

			reward := data[ptr+35 : ptr+67]
			tmp = reward
			node.Reward = tmp.(int)

			result.Nodes = append(result.Nodes, node)
		}
	}

	return result, false
}

func getAuditResult(ctx storage.Context) []AuditResult {
	data := storage.Get(ctx, journalKey)
	if data != nil {
		return binary.Deserialize(data.([]byte)).([]AuditResult)
	}

	return []AuditResult{}
}

func setSerialized(ctx storage.Context, key interface{}, value interface{}) {
	data := binary.Serialize(value)
	storage.Put(ctx, key, data)
}

func walletToScripHash(wallet []byte) []byte {
	return wallet[1 : len(wallet)-4]
}

// neo-go#1176
func bytesEqual(a []byte, b []byte) bool {
	return util.Equals(string(a), string(b))
}