package txbuilder

import (
	"context"
	stdjson "encoding/json"
	"errors"

	"golang.org/x/crypto/sha3"

	"github.com/vapor/common"
	cfg "github.com/vapor/config"
	"github.com/vapor/consensus"
	"github.com/vapor/encoding/json"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
	"github.com/vapor/protocol/vm/vmutil"
)

// DecodeControlAddressAction convert input data to action struct
func DecodeControlAddressAction(data []byte) (Action, error) {
	a := new(controlAddressAction)
	err := stdjson.Unmarshal(data, a)
	return a, err
}

type controlAddressAction struct {
	bc.AssetAmount
	Address string `json:"address"`
}

func (a *controlAddressAction) Build(ctx context.Context, b *TemplateBuilder) error {
	var missing []string
	if a.Address == "" {
		missing = append(missing, "address")
	}
	if a.AssetId.IsZero() {
		missing = append(missing, "asset_id")
	}
	if a.Amount == 0 {
		missing = append(missing, "amount")
	}
	if len(missing) > 0 {
		return MissingFieldsError(missing...)
	}

	address, err := common.DecodeAddress(a.Address, &consensus.ActiveNetParams)
	if err != nil {
		return err
	}

	redeemContract := address.ScriptAddress()
	program := []byte{}
	switch address.(type) {
	case *common.AddressWitnessPubKeyHash:
		program, err = vmutil.P2WPKHProgram(redeemContract)
	case *common.AddressWitnessScriptHash:
		program, err = vmutil.P2WSHProgram(redeemContract)
	default:
		return errors.New("unsupport address type")
	}
	if err != nil {
		return err
	}

	out := types.NewIntraChainOutput(*a.AssetId, a.Amount, program)
	return b.AddOutput(out)
}

func (a *controlAddressAction) ActionType() string {
	return "control_address"
}

// DecodeControlProgramAction convert input data to action struct
func DecodeControlProgramAction(data []byte) (Action, error) {
	a := new(controlProgramAction)
	err := stdjson.Unmarshal(data, a)
	return a, err
}

type controlProgramAction struct {
	bc.AssetAmount
	Program json.HexBytes `json:"control_program"`
}

func (a *controlProgramAction) Build(ctx context.Context, b *TemplateBuilder) error {
	var missing []string
	if len(a.Program) == 0 {
		missing = append(missing, "control_program")
	}
	if a.AssetId.IsZero() {
		missing = append(missing, "asset_id")
	}
	if a.Amount == 0 {
		missing = append(missing, "amount")
	}
	if len(missing) > 0 {
		return MissingFieldsError(missing...)
	}

	out := types.NewIntraChainOutput(*a.AssetId, a.Amount, a.Program)
	return b.AddOutput(out)
}

func (a *controlProgramAction) ActionType() string {
	return "control_program"
}

// DecodeRetireAction convert input data to action struct
func DecodeRetireAction(data []byte) (Action, error) {
	a := new(retireAction)
	err := stdjson.Unmarshal(data, a)
	return a, err
}

type retireAction struct {
	bc.AssetAmount
	Arbitrary json.HexBytes `json:"arbitrary"`
}

func (a *retireAction) Build(ctx context.Context, b *TemplateBuilder) error {
	var missing []string
	if a.AssetId.IsZero() {
		missing = append(missing, "asset_id")
	}
	if a.Amount == 0 {
		missing = append(missing, "amount")
	}
	if len(missing) > 0 {
		return MissingFieldsError(missing...)
	}

	program, err := vmutil.RetireProgram(a.Arbitrary)
	if err != nil {
		return err
	}
	out := types.NewIntraChainOutput(*a.AssetId, a.Amount, program)
	return b.AddOutput(out)
}

func (a *retireAction) ActionType() string {
	return "retire"
}

// DecodeCrossOutAction convert input data to action struct
func DecodeCrossOutAction(data []byte) (Action, error) {
	a := new(crossOutAction)
	err := stdjson.Unmarshal(data, a)
	return a, err
}

type crossOutAction struct {
	bc.AssetAmount
	Address string `json:"address"`
}

func (a *crossOutAction) Build(ctx context.Context, b *TemplateBuilder) error {
	var missing []string
	if a.Address == "" {
		missing = append(missing, "address")
	}
	if a.AssetId.IsZero() {
		missing = append(missing, "asset_id")
	}
	if a.Amount == 0 {
		missing = append(missing, "amount")
	}
	if len(missing) > 0 {
		return MissingFieldsError(missing...)
	}

	address, err := common.DecodeAddress(a.Address, consensus.BytomMainNetParams(&consensus.ActiveNetParams))
	if err != nil {
		return err
	}

	redeemContract := address.ScriptAddress()
	program := []byte{}
	switch address.(type) {
	case *common.AddressWitnessPubKeyHash:
		program, err = vmutil.P2WPKHProgram(redeemContract)
	case *common.AddressWitnessScriptHash:
		program, err = vmutil.P2WSHProgram(redeemContract)
	default:
		return errors.New("unsupport address type")
	}
	if err != nil {
		return err
	}

	out := types.NewCrossChainOutput(*a.AssetId, a.Amount, program)
	return b.AddOutput(out)
}

func (a *crossOutAction) ActionType() string {
	return "cross_chain_out"
}

// DecodeVoteOutputAction convert input data to action struct
func DecodeVoteOutputAction(data []byte) (Action, error) {
	a := new(voteOutputAction)
	err := stdjson.Unmarshal(data, a)
	return a, err
}

type voteOutputAction struct {
	bc.AssetAmount
	Address string        `json:"address"`
	Vote    json.HexBytes `json:"vote"`
}

func (a *voteOutputAction) Build(ctx context.Context, b *TemplateBuilder) error {
	var missing []string
	if a.Address == "" {
		missing = append(missing, "address")
	}
	if a.AssetId.IsZero() {
		missing = append(missing, "asset_id")
	}
	if a.Amount == 0 {
		missing = append(missing, "amount")
	}
	if len(a.Vote) == 0 {
		missing = append(missing, "vote")
	}
	if len(missing) > 0 {
		return MissingFieldsError(missing...)
	}

	address, err := common.DecodeAddress(a.Address, &consensus.ActiveNetParams)
	if err != nil {
		return err
	}

	redeemContract := address.ScriptAddress()
	program := []byte{}
	switch address.(type) {
	case *common.AddressWitnessPubKeyHash:
		program, err = vmutil.P2WPKHProgram(redeemContract)
	case *common.AddressWitnessScriptHash:
		program, err = vmutil.P2WSHProgram(redeemContract)
	default:
		return errors.New("unsupport address type")
	}
	if err != nil {
		return err
	}

	out := types.NewVoteOutput(*a.AssetId, a.Amount, program, a.Vote)
	return b.AddOutput(out)
}

func (a *voteOutputAction) ActionType() string {
	return "vote_output"
}

// DecodeCrossInAction convert input data to action struct
func DecodeCrossInAction(data []byte) (Action, error) {
	a := new(crossInAction)
	err := stdjson.Unmarshal(data, a)
	return a, err
}

type crossInAction struct {
	bc.AssetAmount
	SourceID          bc.Hash       `json:"source_id"`
	SourcePos         uint64        `json:"source_pos"`
	VMVersion         uint64        `json:"vm_version"`
	RawDefinitionByte json.HexBytes `json:"raw_definition_byte"`
	IssuanceProgram   json.HexBytes `json:"issuance_program"`
}

func (a *crossInAction) Build(ctx context.Context, builder *TemplateBuilder) error {
	var missing []string
	if a.SourceID.IsZero() {
		missing = append(missing, "source_id")
	}
	if a.AssetId.IsZero() {
		missing = append(missing, "asset_id")
	}
	if a.Amount == 0 {
		missing = append(missing, "amount")
	}

	if len(missing) > 0 {
		return MissingFieldsError(missing...)
	}

	if err := a.checkAssetID(); err != nil {
		return err
	}

	// arguments will be set when materializeWitnesses
	txin := types.NewCrossChainInput(nil, a.SourceID, *a.AssetId, a.Amount, a.SourcePos, a.VMVersion, a.RawDefinitionByte, a.IssuanceProgram)
	tplIn := &SigningInstruction{}
	fed := cfg.CommonConfig.Federation
	tplIn.AddRawWitnessKeys(fed.Xpubs, cfg.FedAddressPath, fed.Quorum)
	tplIn.AddDataWitness(cfg.FederationPMultiSigScript(cfg.CommonConfig))
	return builder.AddInput(txin, tplIn)
}

func (a *crossInAction) ActionType() string {
	return "cross_chain_in"
}

func (c *crossInAction) checkAssetID() error {
	defHash := bc.NewHash(sha3.Sum256(c.RawDefinitionByte))
	assetID := bc.ComputeAssetID(c.IssuanceProgram, c.VMVersion, &defHash)

	if *c.AssetId != *consensus.BTMAssetID && assetID != *c.AssetAmount.AssetId {
		return errors.New("incorrect asset_idincorrect asset_id")
	}

	return nil
}
