package helper

import (
	"archive/tar"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"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/pkg/innerring"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/spf13/viper"
)

func GetAlphabetWallets(v *viper.Viper, walletDir string) ([]*wallet.Wallet, error) {
	wallets, err := openAlphabetWallets(v, walletDir)
	if err != nil {
		return nil, err
	}

	if len(wallets) > constants.MaxAlphabetNodes {
		return nil, ErrTooManyAlphabetNodes
	}
	return wallets, nil
}

func openAlphabetWallets(v *viper.Viper, walletDir string) ([]*wallet.Wallet, error) {
	walletFiles, err := os.ReadDir(walletDir)
	if err != nil {
		return nil, fmt.Errorf("can't read alphabet wallets dir: %w", err)
	}

	var wallets []*wallet.Wallet
	var letter string
	for i := range constants.MaxAlphabetNodes {
		letter = innerring.GlagoliticLetter(i).String()
		p := filepath.Join(walletDir, letter+".json")
		var w *wallet.Wallet
		w, err = wallet.NewWalletFromFile(p)
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				err = nil
			} else {
				err = fmt.Errorf("can't open wallet: %w", err)
			}
			break
		}

		var password string
		password, err = config.GetPassword(v, letter)
		if err != nil {
			err = fmt.Errorf("can't fetch password: %w", err)
			break
		}

		for i := range w.Accounts {
			if err = w.Accounts[i].Decrypt(password, keys.NEP2ScryptParams()); err != nil {
				err = fmt.Errorf("can't unlock wallet: %w", err)
				break
			}
		}

		wallets = append(wallets, w)
	}
	if err != nil {
		return nil, fmt.Errorf("can't read wallet for letter '%s': %w", letter, err)
	}
	if len(wallets) == 0 {
		err = errors.New("there are no alphabet wallets in dir (run `generate-alphabet` command first)")
		if len(walletFiles) > 0 {
			err = fmt.Errorf("use glagolitic names for wallets(run `print-alphabet`): %w", err)
		}
		return nil, err
	}
	return wallets, nil
}

func NewActor(c actor.RPCActor, committeeAcc *wallet.Account) (*actor.Actor, error) {
	return actor.New(c, []actor.SignerAccount{{
		Signer: transaction.Signer{
			Account: committeeAcc.Contract.ScriptHash(),
			Scopes:  transaction.Global,
		},
		Account: committeeAcc,
	}})
}

func ReadContract(ctrPath, ctrName string) (*ContractState, error) {
	rawNef, err := os.ReadFile(filepath.Join(ctrPath, ctrName+"_contract.nef"))
	if err != nil {
		return nil, fmt.Errorf("can't read NEF file for %s contract: %w", ctrName, err)
	}
	rawManif, err := os.ReadFile(filepath.Join(ctrPath, "config.json"))
	if err != nil {
		return nil, fmt.Errorf("can't read manifest file for %s contract: %w", ctrName, err)
	}

	cs := &ContractState{
		RawNEF:      rawNef,
		RawManifest: rawManif,
	}

	return cs, cs.Parse()
}

func readContractsFromArchive(file io.Reader, names []string) (map[string]*ContractState, error) {
	m := make(map[string]*ContractState, len(names))
	for i := range names {
		m[names[i]] = new(ContractState)
	}

	gr, err := gzip.NewReader(file)
	if err != nil {
		return nil, fmt.Errorf("contracts file must be tar.gz archive: %w", err)
	}

	r := tar.NewReader(gr)
	var h *tar.Header
	for h, err = r.Next(); err == nil && h != nil; h, err = r.Next() {
		if h.Typeflag != tar.TypeReg {
			continue
		}
		dir, _ := filepath.Split(h.Name)
		ctrName := filepath.Base(dir)

		cs, ok := m[ctrName]
		if !ok {
			continue
		}

		switch {
		case strings.HasSuffix(h.Name, filepath.Join(ctrName, ctrName+"_contract.nef")):
			cs.RawNEF, err = io.ReadAll(r)
			if err != nil {
				return nil, fmt.Errorf("can't read NEF file for %s contract: %w", ctrName, err)
			}
		case strings.HasSuffix(h.Name, "config.json"):
			cs.RawManifest, err = io.ReadAll(r)
			if err != nil {
				return nil, fmt.Errorf("can't read manifest file for %s contract: %w", ctrName, err)
			}
		}
		m[ctrName] = cs
	}
	if err != nil && err != io.EOF {
		return nil, fmt.Errorf("can't read contracts from archive: %w", err)
	}

	for ctrName, cs := range m {
		if cs.RawNEF == nil {
			return nil, fmt.Errorf("NEF for %s contract wasn't found", ctrName)
		}
		if cs.RawManifest == nil {
			return nil, fmt.Errorf("manifest for %s contract wasn't found", ctrName)
		}
	}
	return m, nil
}

func GetAlphabetNNSDomain(i int) string {
	return constants.AlphabetContract + strconv.FormatUint(uint64(i), 10) + ".frostfs"
}

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
}

// GetContractByID retrieves a contract by its ID using the standard GetContractByID method.
// However, if the returned state.Contract is nil, it returns an error indicating that the contract was not found.
// See https://git.frostfs.info/TrueCloudLab/frostfs-node/issues/1210
func GetContractByID(r *management.ContractReader, id int32) (*state.Contract, error) {
	cs, err := r.GetContractByID(id)
	if err != nil {
		return nil, err
	}

	if cs == nil {
		return nil, errors.New("contract not found")
	}
	return cs, nil
}