package wallet

import (
	"errors"
	"fmt"
	"slices"

	"git.marketally.com/tutus-one/tutus-chain/pkg/config/netmode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/hash"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/encoding/address"
	"git.marketally.com/tutus-one/tutus-chain/pkg/io"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/emit"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
)

// Account represents a NEO account. It holds the private and the public key
// along with some metadata.
type Account struct {
	// NEO private key.
	privateKey *keys.PrivateKey

	// Script hash corresponding to the Address.
	scriptHash util.Uint160

	// NEO public address.
	Address string `json:"address"`

	// Encrypted WIF of the account also known as the key.
	EncryptedWIF string `json:"key"`

	// Label is a label the user had made for this account.
	Label string `json:"label"`

	// Contract is a Contract object which describes the details of the contract.
	// This field can be null (for watch-only address).
	Contract *Contract `json:"contract"`

	// Indicates whether the account is locked by the user.
	// the client shouldn't spend the funds in a locked account.
	Locked bool `json:"lock"`

	// Indicates whether the account is the default change account.
	Default bool `json:"isDefault"`
}

// Contract represents a subset of the smartcontract to embed in the
// Account so it's NEP-6 compliant.
type Contract struct {
	// Script of the contract deployed on the blockchain.
	Script []byte `json:"script"`

	// A list of parameters used deploying this contract.
	Parameters []ContractParam `json:"parameters"`

	// Indicates whether the contract has been deployed to the blockchain.
	Deployed bool `json:"deployed"`

	// InvocationBuilder returns invocation script for deployed contracts.
	// In case contract is not deployed or has 0 arguments, this field is ignored.
	// It might be executed on a partially formed tx, and is primarily needed to properly
	// calculate network fee for complex contract signers.
	InvocationBuilder func(tx *transaction.Transaction) ([]byte, error) `json:"-"`
}

// ContractParam is a descriptor of a contract parameter
// containing type and optional name.
type ContractParam struct {
	Name string                  `json:"name"`
	Type smartcontract.ParamType `json:"type"`
}

// ScriptHash returns the hash of contract's script.
func (c Contract) ScriptHash() util.Uint160 {
	return hash.Hash160(c.Script)
}

// NewAccount creates a new Account with a random generated PrivateKey.
func NewAccount() (*Account, error) {
	priv, err := keys.NewPrivateKey()
	if err != nil {
		return nil, err
	}
	return NewAccountFromPrivateKey(priv), nil
}

// NewContractAccount creates a contract account belonging to some deployed contract.
// SignTx can be called on this account with no error and will create invocation script,
// which puts provided arguments on stack for use in `verify`.
func NewContractAccount(hash util.Uint160, args ...any) *Account {
	return &Account{
		Address: address.Uint160ToString(hash),
		Contract: &Contract{
			Parameters: make([]ContractParam, len(args)),
			Deployed:   true,
			InvocationBuilder: func(tx *transaction.Transaction) ([]byte, error) {
				w := io.NewBufBinWriter()
				for i := range args {
					emit.Any(w.BinWriter, args[i])
				}
				if w.Err != nil {
					return nil, w.Err
				}
				return w.Bytes(), nil
			},
		},
	}
}

// SignTx signs transaction t and updates it's Witnesses.
func (a *Account) SignTx(net netmode.Magic, t *transaction.Transaction) error {
	if a.Locked {
		return errors.New("account is locked")
	}
	if a.Contract == nil {
		return errors.New("account has no contract")
	}
	var pos = slices.IndexFunc(t.Signers, func(s transaction.Signer) bool {
		return s.Account.Equals(a.ScriptHash())
	})
	if pos == -1 {
		return errors.New("transaction is not signed by this account")
	}
	if len(t.Scripts) < pos {
		return errors.New("transaction is not yet signed by the previous signer")
	}
	if len(t.Scripts) == pos {
		t.Scripts = append(t.Scripts, transaction.Witness{
			VerificationScript: a.Contract.Script, // Can be nil for deployed contract.
		})
	}
	if a.Contract.Deployed && a.Contract.InvocationBuilder != nil {
		invoc, err := a.Contract.InvocationBuilder(t)
		t.Scripts[pos].InvocationScript = invoc
		return err
	}
	if len(a.Contract.Parameters) == 0 {
		return nil
	}
	if a.privateKey == nil {
		return errors.New("account key is not available (need to decrypt?)")
	}

	if len(a.Contract.Parameters) == 1 && t.Scripts[pos].InvocationScript != nil {
		t.Scripts[pos].InvocationScript = t.Scripts[pos].InvocationScript[:0]
	}
	t.Scripts[pos].InvocationScript = append(t.Scripts[pos].InvocationScript, byte(opcode.PUSHDATA1), keys.SignatureLen)
	t.Scripts[pos].InvocationScript = append(t.Scripts[pos].InvocationScript, a.privateKey.SignHashable(uint32(net), t)...)

	return nil
}

// SignHashable signs the given Hashable item and returns the signature. If this
// account can't sign (CanSign() returns false) nil is returned.
func (a *Account) SignHashable(net netmode.Magic, item hash.Hashable) []byte {
	if !a.CanSign() {
		return nil
	}
	return a.privateKey.SignHashable(uint32(net), item)
}

// CanSign returns true when account is not locked and has a decrypted private
// key inside, so it's ready to create real signatures.
func (a *Account) CanSign() bool {
	return !a.Locked && a.privateKey != nil
}

// GetVerificationScript returns account's verification script.
func (a *Account) GetVerificationScript() []byte {
	if a.Contract != nil {
		return a.Contract.Script
	}
	return a.privateKey.PublicKey().GetVerificationScript()
}

// Decrypt decrypts the EncryptedWIF with the given passphrase returning error
// if anything goes wrong. After the decryption Account can be used to sign
// things unless it's locked. Don't decrypt the key unless you want to sign
// something and don't forget to call Close after use for maximum safety.
func (a *Account) Decrypt(passphrase string, scrypt keys.ScryptParams) error {
	var err error

	if a.EncryptedWIF == "" {
		return errors.New("no encrypted wif in the account")
	}
	a.privateKey, err = keys.NEP2Decrypt(a.EncryptedWIF, passphrase, scrypt)
	if err != nil {
		return err
	}

	return nil
}

// Encrypt encrypts the wallet's PrivateKey with the given passphrase
// under the NEP-2 standard.
func (a *Account) Encrypt(passphrase string, scrypt keys.ScryptParams) error {
	wif, err := keys.NEP2Encrypt(a.privateKey, passphrase, scrypt)
	if err != nil {
		return err
	}
	a.EncryptedWIF = wif
	return nil
}

// PrivateKey returns private key corresponding to the account if it's unlocked.
// Please be very careful when using it, do not copy its contents and do not
// keep a pointer to it unless you absolutely need to. Most of the time you can
// use other methods (PublicKey, ScriptHash, SignHashable) depending on your
// needs and it'll be safer this way.
func (a *Account) PrivateKey() *keys.PrivateKey {
	return a.privateKey
}

// PublicKey returns the public key associated with the private key corresponding to
// the account. It can return nil if account is locked (use CanSign to check).
func (a *Account) PublicKey() *keys.PublicKey {
	if !a.CanSign() {
		return nil
	}
	return a.privateKey.PublicKey()
}

// ScriptHash returns the script hash (account) that the Account.Address is
// derived from. It never returns an error, so if this Account has an invalid
// Address you'll just get a zero script hash.
func (a *Account) ScriptHash() util.Uint160 {
	if a.scriptHash.Equals(util.Uint160{}) {
		a.scriptHash, _ = address.StringToUint160(a.Address)
	}
	return a.scriptHash
}

// Close cleans up the private key used by Account and disassociates it from
// Account. The Account can no longer sign anything after this call, but Decrypt
// can make it usable again.
func (a *Account) Close() {
	if a.privateKey == nil {
		return
	}
	a.privateKey.Destroy()
	a.privateKey = nil
}

// NewAccountFromWIF creates a new Account from the given WIF.
func NewAccountFromWIF(wif string) (*Account, error) {
	privKey, err := keys.NewPrivateKeyFromWIF(wif)
	if err != nil {
		return nil, err
	}
	return NewAccountFromPrivateKey(privKey), nil
}

// NewAccountFromEncryptedWIF creates a new Account from the given encrypted WIF.
func NewAccountFromEncryptedWIF(wif string, pass string, scrypt keys.ScryptParams) (*Account, error) {
	priv, err := keys.NEP2Decrypt(wif, pass, scrypt)
	if err != nil {
		return nil, err
	}

	a := NewAccountFromPrivateKey(priv)
	a.EncryptedWIF = wif

	return a, nil
}

// ConvertMultisig sets a's contract to multisig contract with m sufficient signatures.
func (a *Account) ConvertMultisig(m int, pubs []*keys.PublicKey) error {
	if a.Locked {
		return errors.New("account is locked")
	}
	if a.privateKey == nil {
		return errors.New("account key is not available (need to decrypt?)")
	}
	accKey := a.privateKey.PublicKey()
	return a.ConvertMultisigEncrypted(accKey, m, pubs)
}

// ConvertMultisigEncrypted sets a's contract to an encrypted multisig contract
// with m sufficient signatures. The encrypted private key is not modified and
// remains the same.
func (a *Account) ConvertMultisigEncrypted(accKey *keys.PublicKey, m int, pubs []*keys.PublicKey) error {
	if !slices.ContainsFunc(pubs, accKey.Equal) {
		return errors.New("own public key was not found among multisig keys")
	}

	script, err := smartcontract.CreateMultiSigRedeemScript(m, pubs)
	if err != nil {
		return err
	}

	a.scriptHash = hash.Hash160(script)
	a.Address = address.Uint160ToString(a.scriptHash)
	a.Contract = &Contract{
		Script:     script,
		Parameters: getContractParams(m),
	}

	return nil
}

// NewAccountFromPrivateKey creates a wallet from the given PrivateKey.
func NewAccountFromPrivateKey(p *keys.PrivateKey) *Account {
	pubKey := p.PublicKey()

	a := &Account{
		privateKey: p,
		scriptHash: p.GetScriptHash(),
		Address:    p.Address(),
		Contract: &Contract{
			Script:     pubKey.GetVerificationScript(),
			Parameters: getContractParams(1),
		},
	}

	return a
}

func getContractParams(n int) []ContractParam {
	params := make([]ContractParam, n)
	for i := range params {
		params[i].Name = fmt.Sprintf("parameter%d", i)
		params[i].Type = smartcontract.SignatureType
	}

	return params
}
