package generate

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

	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
	"git.frostfs.info/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/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"
	"golang.org/x/sync/errgroup"
)

func AlphabetCreds(cmd *cobra.Command, _ []string) error {
	// alphabet size is not part of the config
	size, err := cmd.Flags().GetUint(commonflags.AlphabetSizeFlag)
	if err != nil {
		return err
	}
	if size == 0 {
		return errors.New("size must be > 0")
	}
	if size > constants.MaxAlphabetNodes {
		return helper.ErrTooManyAlphabetNodes
	}

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

	_, err = helper.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, 0o644)
		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(constants.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()
	}

	var errG errgroup.Group

	// Create committee account with N/2+1 multi-signature.
	majCount := smartcontract.GetMajorityHonestNodeCount(size)
	// Create consensus account with 2*N/3+1 multi-signature.
	bftCount := smartcontract.GetDefaultHonestNodeCount(size)
	for i := range wallets {
		i := i
		ps := pubs.Copy()
		errG.Go(func() error {
			if err := addMultisigAccount(wallets[i], majCount, constants.CommitteeAccountName, passwords[i], ps); err != nil {
				return fmt.Errorf("can't create committee account: %w", err)
			}
			if err := addMultisigAccount(wallets[i], bftCount, constants.ConsensusAccountName, passwords[i], ps); err != nil {
				return fmt.Errorf("can't create consentus account: %w", err)
			}
			if err := wallets[i].SavePretty(); err != nil {
				return fmt.Errorf("can't save wallet: %w", err)
			}
			return nil
		})
	}
	if err := errG.Wait(); err != nil {
		return nil, 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(commonflags.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>')", commonflags.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 = constants.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 := helper.ParseGASAmount(gasStr)
	if err != nil {
		return err
	}

	wCtx, err := helper.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()
}