package morph

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"

	"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
	"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
	"github.com/nspcc-dev/neo-go/pkg/io"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/emit"
	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

const (
	singleAccountName    = "single"
	committeeAccountName = "committee"
	consensusAccountName = "consensus"
)

func generateAlphabetCreds(cmd *cobra.Command, args []string) error {
	// alphabet size is not part of the config
	size, err := cmd.Flags().GetUint(alphabetSizeFlag)
	if err != nil {
		return err
	}
	if size == 0 {
		return errors.New("size must be > 0")
	}

	v := viper.GetViper()
	walletDir := config.ResolveHomePath(viper.GetString(alphabetWalletsFlag))
	pwds, err := initializeWallets(v, walletDir, int(size))
	if err != nil {
		return err
	}

	_, err = initializeContractWallet(v, walletDir)
	if err != nil {
		return err
	}

	cmd.Println("size:", size)
	cmd.Println("alphabet-wallets:", walletDir)
	for i := range pwds {
		cmd.Printf("wallet[%d]: %s\n", i, pwds[i])
	}

	return nil
}

func initializeWallets(v *viper.Viper, walletDir string, size int) ([]string, error) {
	wallets := make([]*wallet.Wallet, size)
	pubs := make(keys.PublicKeys, size)
	passwords := make([]string, size)

	for i := range wallets {
		password, err := config.GetPassword(v, innerring.GlagoliticLetter(i).String())
		if err != nil {
			return nil, fmt.Errorf("can't fetch password: %w", err)
		}

		p := filepath.Join(walletDir, innerring.GlagoliticLetter(i).String()+".json")
		f, err := os.OpenFile(p, os.O_CREATE, 0644)
		if err != nil {
			return nil, fmt.Errorf("can't create wallet file: %w", err)
		}
		if err := f.Close(); err != nil {
			return nil, fmt.Errorf("can't close wallet file: %w", err)
		}
		w, err := wallet.NewWallet(p)
		if err != nil {
			return nil, fmt.Errorf("can't create wallet: %w", err)
		}
		if err := w.CreateAccount(singleAccountName, password); err != nil {
			return nil, fmt.Errorf("can't create account: %w", err)
		}

		passwords[i] = password
		wallets[i] = w
		pubs[i] = w.Accounts[0].PrivateKey().PublicKey()
	}

	// Create committee account with N/2+1 multi-signature.
	majCount := smartcontract.GetMajorityHonestNodeCount(size)
	for i, w := range wallets {
		if err := addMultisigAccount(w, majCount, committeeAccountName, passwords[i], pubs); err != nil {
			return nil, fmt.Errorf("can't create committee account: %w", err)
		}
	}

	// Create consensus account with 2*N/3+1 multi-signature.
	bftCount := smartcontract.GetDefaultHonestNodeCount(size)
	for i, w := range wallets {
		if err := addMultisigAccount(w, bftCount, consensusAccountName, passwords[i], pubs); err != nil {
			return nil, fmt.Errorf("can't create consensus account: %w", err)
		}
	}

	for _, w := range wallets {
		if err := w.SavePretty(); err != nil {
			return nil, fmt.Errorf("can't save wallet: %w", err)
		}
	}

	return passwords, nil
}

func addMultisigAccount(w *wallet.Wallet, m int, name, password string, pubs keys.PublicKeys) error {
	acc := wallet.NewAccountFromPrivateKey(w.Accounts[0].PrivateKey())
	acc.Label = name

	if err := acc.ConvertMultisig(m, pubs); err != nil {
		return err
	}
	if err := acc.Encrypt(password, keys.NEP2ScryptParams()); err != nil {
		return err
	}
	w.AddAccount(acc)
	return nil
}

func generateStorageCreds(cmd *cobra.Command, _ []string) error {
	return refillGas(cmd, storageGasConfigFlag, true)
}

func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) (err error) {
	// storage wallet path is not part of the config
	storageWalletPath, _ := cmd.Flags().GetString(storageWalletFlag)
	// wallet address is not part of the config
	walletAddress, _ := cmd.Flags().GetString(walletAddressFlag)

	var gasReceiver util.Uint160

	if len(walletAddress) != 0 {
		gasReceiver, err = address.StringToUint160(walletAddress)
		if err != nil {
			return fmt.Errorf("invalid wallet address %s: %w", walletAddress, err)
		}
	} else {
		if storageWalletPath == "" {
			return fmt.Errorf("missing wallet path (use '--%s <out.json>')", storageWalletFlag)
		}

		var w *wallet.Wallet

		if createWallet {
			w, err = wallet.NewWallet(storageWalletPath)
		} else {
			w, err = wallet.NewWalletFromFile(storageWalletPath)
		}

		if err != nil {
			return fmt.Errorf("can't create wallet: %w", err)
		}

		if createWallet {
			var password string

			label, _ := cmd.Flags().GetString(storageWalletLabelFlag)
			password, err := config.GetStoragePassword(viper.GetViper(), label)
			if err != nil {
				return fmt.Errorf("can't fetch password: %w", err)
			}

			if label == "" {
				label = singleAccountName
			}

			if err := w.CreateAccount(label, password); err != nil {
				return fmt.Errorf("can't create account: %w", err)
			}
		}

		gasReceiver = w.Accounts[0].Contract.ScriptHash()
	}

	gasStr := viper.GetString(gasFlag)

	gasAmount, err := parseGASAmount(gasStr)
	if err != nil {
		return err
	}

	wCtx, err := newInitializeContext(cmd, viper.GetViper())
	if err != nil {
		return err
	}

	bw := io.NewBufBinWriter()
	emit.AppCall(bw.BinWriter, gas.Hash, "transfer", callflag.All,
		wCtx.CommitteeAcc.Contract.ScriptHash(), gasReceiver, int64(gasAmount), nil)
	emit.Opcodes(bw.BinWriter, opcode.ASSERT)
	if bw.Err != nil {
		return fmt.Errorf("BUG: invalid transfer arguments: %w", bw.Err)
	}

	if err := wCtx.sendCommitteeTx(bw.Bytes(), false); err != nil {
		return err
	}

	return wCtx.awaitTx()
}

func parseGASAmount(s string) (fixedn.Fixed8, error) {
	gasAmount, err := fixedn.Fixed8FromString(s)
	if err != nil {
		return 0, fmt.Errorf("invalid GAS amount %s: %w", s, err)
	}
	if gasAmount <= 0 {
		return 0, fmt.Errorf("GAS amount must be positive (got %d)", gasAmount)
	}
	return gasAmount, nil
}