package native

import (
	"errors"
	"fmt"
	"math/big"

	"github.com/nspcc-dev/neo-go/pkg/core/interop"
	"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// GAS represents GAS native contract.
type GAS struct {
	nep17TokenNative
	NEO *NEO
}

const gasContractID = -6

// GASFactor is a divisor for finding GAS integral value.
const GASFactor = NEOTotalSupply
const initialGAS = 30000000

// newGAS returns GAS native contract.
func newGAS() *GAS {
	g := &GAS{}
	defer g.UpdateHash()

	nep17 := newNEP17Native(nativenames.Gas, gasContractID)
	nep17.symbol = "GAS"
	nep17.decimals = 8
	nep17.factor = GASFactor
	nep17.incBalance = g.increaseBalance
	nep17.balFromBytes = g.balanceFromBytes

	g.nep17TokenNative = *nep17

	desc := newDescriptor("refuel", smartcontract.VoidType,
		manifest.NewParameter("account", smartcontract.Hash160Type),
		manifest.NewParameter("amount", smartcontract.IntegerType))
	md := newMethodAndPrice(g.refuel, 1<<15, callflag.States|callflag.AllowNotify)
	g.AddMethod(md, desc)

	return g
}

func (g *GAS) increaseBalance(_ *interop.Context, _ util.Uint160, si *state.StorageItem, amount *big.Int) error {
	acc, err := state.NEP17BalanceStateFromBytes(*si)
	if err != nil {
		return err
	}
	if sign := amount.Sign(); sign == 0 {
		return nil
	} else if sign == -1 && acc.Balance.Cmp(new(big.Int).Neg(amount)) == -1 {
		return errors.New("insufficient funds")
	}
	acc.Balance.Add(&acc.Balance, amount)
	if acc.Balance.Sign() != 0 {
		*si = acc.Bytes()
	} else {
		*si = nil
	}
	return nil
}

func (g *GAS) balanceFromBytes(si *state.StorageItem) (*big.Int, error) {
	acc, err := state.NEP17BalanceStateFromBytes(*si)
	if err != nil {
		return nil, err
	}
	return &acc.Balance, err
}

func (g *GAS) refuel(ic *interop.Context, args []stackitem.Item) stackitem.Item {
	acc := toUint160(args[0])
	gas := toBigInt(args[1])

	if !gas.IsInt64() || gas.Sign() == -1 {
		panic("invalid GAS value")
	}

	ok, err := runtime.CheckHashedWitness(ic, acc)
	if !ok || err != nil {
		panic(fmt.Errorf("%w: %v", ErrInvalidWitness, err))
	}

	g.burn(ic, acc, gas)
	ic.VM.GasLimit += gas.Int64()
	return stackitem.Null{}
}

// Initialize initializes GAS contract.
func (g *GAS) Initialize(ic *interop.Context) error {
	if err := g.nep17TokenNative.Initialize(ic); err != nil {
		return err
	}
	if g.nep17TokenNative.getTotalSupply(ic.DAO).Sign() != 0 {
		return errors.New("already initialized")
	}
	h, err := getStandbyValidatorsHash(ic)
	if err != nil {
		return err
	}
	g.mint(ic, h, big.NewInt(initialGAS*GASFactor), false)
	return nil
}

// OnPersist implements Contract interface.
func (g *GAS) OnPersist(ic *interop.Context) error {
	if len(ic.Block.Transactions) == 0 {
		return nil
	}
	for _, tx := range ic.Block.Transactions {
		absAmount := big.NewInt(tx.SystemFee + tx.NetworkFee)
		g.burn(ic, tx.Sender(), absAmount)
	}
	validators := g.NEO.GetNextBlockValidatorsInternal()
	primary := validators[ic.Block.PrimaryIndex].GetScriptHash()
	var netFee int64
	for _, tx := range ic.Block.Transactions {
		netFee += tx.NetworkFee
	}
	g.mint(ic, primary, big.NewInt(int64(netFee)), false)
	return nil
}

// PostPersist implements Contract interface.
func (g *GAS) PostPersist(ic *interop.Context) error {
	return nil
}

func getStandbyValidatorsHash(ic *interop.Context) (util.Uint160, error) {
	s, err := smartcontract.CreateDefaultMultiSigRedeemScript(ic.Chain.GetStandByValidators())
	if err != nil {
		return util.Uint160{}, err
	}
	return hash.Hash160(s), nil
}