package morph

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"

	"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
	"github.com/nspcc-dev/neo-go/cli/cmdargs"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/io"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
	"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv/params"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
	"github.com/nspcc-dev/neo-go/pkg/vm/emit"
	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

const (
	contractPathFlag = "contract"
	updateFlag       = "update"
	customZoneFlag   = "domain"
)

var deployCmd = &cobra.Command{
	Use:   "deploy",
	Short: "Deploy additional smart-contracts",
	Long: `Deploy additional smart-contract which are not related to core.
All contracts are deployed by the committee, so access to the alphabet wallets is required.
Optionally, arguments can be provided to be passed to a contract's _deploy function.
The syntax is the same as for 'neo-go contract testinvokefunction' command.
Compiled contract file name must contain '_contract.nef' suffix.
Contract's manifest file name must be 'config.json'.
NNS name is taken by stripping '_contract.nef' from the NEF file (similar to frostfs contracts).`,
	PreRun: func(cmd *cobra.Command, _ []string) {
		_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
		_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
	},
	RunE: deployContractCmd,
}

func init() {
	ff := deployCmd.Flags()

	ff.String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
	_ = deployCmd.MarkFlagFilename(alphabetWalletsFlag)

	ff.StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
	ff.String(contractPathFlag, "", "Path to the contract directory")
	_ = deployCmd.MarkFlagFilename(contractPathFlag)

	ff.Bool(updateFlag, false, "Update an existing contract")
	ff.String(customZoneFlag, "frostfs", "Custom zone for NNS")
}

func deployContractCmd(cmd *cobra.Command, args []string) error {
	v := viper.GetViper()
	c, err := newInitializeContext(cmd, v)
	if err != nil {
		return fmt.Errorf("initialization error: %w", err)
	}
	defer c.close()

	ctrPath, _ := cmd.Flags().GetString(contractPathFlag)
	ctrName, err := probeContractName(ctrPath)
	if err != nil {
		return err
	}

	cs, err := readContract(ctrPath, ctrName)
	if err != nil {
		return err
	}

	nnsCs, err := c.Client.GetContractStateByID(1)
	if err != nil {
		return fmt.Errorf("can't fetch NNS contract state: %w", err)
	}

	callHash := management.Hash
	method := deployMethodName
	zone, _ := cmd.Flags().GetString(customZoneFlag)
	domain := ctrName + "." + zone
	isUpdate, _ := cmd.Flags().GetBool(updateFlag)
	if isUpdate {
		cs.Hash, err = nnsResolveHash(c.ReadOnlyInvoker, nnsCs.Hash, domain)
		if err != nil {
			return fmt.Errorf("can't fetch contract hash from NNS: %w", err)
		}
		callHash = cs.Hash
		method = updateMethodName
	} else {
		cs.Hash = state.CreateContractHash(
			c.CommitteeAcc.Contract.ScriptHash(),
			cs.NEF.Checksum,
			cs.Manifest.Name)
	}

	writer := io.NewBufBinWriter()
	if err := emitDeploymentArguments(writer.BinWriter, args); err != nil {
		return err
	}
	emit.Bytes(writer.BinWriter, cs.RawManifest)
	emit.Bytes(writer.BinWriter, cs.RawNEF)
	emit.Int(writer.BinWriter, 3)
	emit.Opcodes(writer.BinWriter, opcode.PACK)
	emit.AppCallNoArgs(writer.BinWriter, callHash, method, callflag.All)
	emit.Opcodes(writer.BinWriter, opcode.DROP) // contract state on stack
	if !isUpdate {
		err := registerNNS(nnsCs, c, zone, domain, cs, writer)
		if err != nil {
			return err
		}
	}

	if writer.Err != nil {
		panic(fmt.Errorf("BUG: can't create deployment script: %w", writer.Err))
	}

	if err := c.sendCommitteeTx(writer.Bytes(), false); err != nil {
		return err
	}
	return c.awaitTx()
}

func registerNNS(nnsCs *state.Contract, c *initializeContext, zone string, domain string, cs *contractState, writer *io.BufBinWriter) error {
	bw := io.NewBufBinWriter()
	emit.Instruction(bw.BinWriter, opcode.INITSSLOT, []byte{1})
	emit.AppCall(bw.BinWriter, nnsCs.Hash, "getPrice", callflag.All)
	emit.Opcodes(bw.BinWriter, opcode.STSFLD0)
	emit.AppCall(bw.BinWriter, nnsCs.Hash, "setPrice", callflag.All, 1)

	start := bw.Len()
	needRecord := false

	ok, err := c.nnsRootRegistered(nnsCs.Hash, zone)
	if err != nil {
		return err
	} else if !ok {
		needRecord = true

		emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
			zone, c.CommitteeAcc.Contract.ScriptHash(),
			"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
		emit.Opcodes(bw.BinWriter, opcode.ASSERT)

		emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
			domain, c.CommitteeAcc.Contract.ScriptHash(),
			"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
		emit.Opcodes(bw.BinWriter, opcode.ASSERT)
	} else {
		s, ok, err := c.nnsRegisterDomainScript(nnsCs.Hash, cs.Hash, domain)
		if err != nil {
			return err
		}
		needRecord = !ok
		if len(s) != 0 {
			bw.WriteBytes(s)
		}
	}
	if needRecord {
		emit.AppCall(bw.BinWriter, nnsCs.Hash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
		emit.AppCall(bw.BinWriter, nnsCs.Hash, "addRecord", callflag.All,
			domain, int64(nns.TXT), address.Uint160ToString(cs.Hash))
	}

	if bw.Err != nil {
		panic(fmt.Errorf("BUG: can't create deployment script: %w", writer.Err))
	} else if bw.Len() != start {
		writer.WriteBytes(bw.Bytes())
		emit.Opcodes(writer.BinWriter, opcode.LDSFLD0, opcode.PUSH1, opcode.PACK)
		emit.AppCallNoArgs(writer.BinWriter, nnsCs.Hash, "setPrice", callflag.All)

		if needRecord {
			c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
		}
	}
	return nil
}

func emitDeploymentArguments(w *io.BinWriter, args []string) error {
	_, ps, err := cmdargs.ParseParams(args, true)
	if err != nil {
		return err
	}

	if len(ps) == 0 {
		emit.Opcodes(w, opcode.NEWARRAY0)
		return nil
	}

	if len(ps) != 1 {
		return fmt.Errorf("at most one argument is expected for deploy, got %d", len(ps))
	}

	// We could emit this directly, but round-trip through JSON is more robust.
	// This a CLI, so optimizing the conversion is not worth the effort.
	data, err := json.Marshal(ps)
	if err != nil {
		return err
	}

	var pp params.Params
	if err := json.Unmarshal(data, &pp); err != nil {
		return err
	}
	return params.ExpandArrayIntoScript(w, pp)
}

func probeContractName(ctrPath string) (string, error) {
	ds, err := os.ReadDir(ctrPath)
	if err != nil {
		return "", fmt.Errorf("can't read directory: %w", err)
	}

	var ctrName string
	for i := range ds {
		if strings.HasSuffix(ds[i].Name(), "_contract.nef") {
			ctrName = strings.TrimSuffix(ds[i].Name(), "_contract.nef")
			break
		}
	}

	if ctrName == "" {
		return "", fmt.Errorf("can't find any NEF files in %s", ctrPath)
	}
	return ctrName, nil
}