package wallet

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"

	"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/smartcontract/scparser"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
)

const (
	// The current version of neo-go wallet implementations.
	walletVersion = "1.0"
)

var (
	// ErrPathIsEmpty appears if wallet was created without linking to file system path,
	// for instance with [NewInMemoryWallet] or [NewWalletFromBytes].
	// Despite this, there was an attempt to save it via [Wallet.Save] or [Wallet.SavePretty] without [Wallet.SetPath].
	ErrPathIsEmpty = errors.New("path is empty")
)

// Wallet represents a NEO (NEP-2, NEP-6) compliant wallet.
type Wallet struct {
	// Version of the wallet, used for later upgrades.
	Version string `json:"version"`

	// A list of accounts which describes the details of each account
	// in the wallet.
	Accounts []*Account `json:"accounts"`

	Scrypt keys.ScryptParams `json:"scrypt"`

	// Extra metadata can be used for storing arbitrary data.
	// This field can be empty.
	Extra Extra `json:"extra"`

	// Path where the wallet file is located..
	path string
}

// Extra stores imported token contracts.
type Extra struct {
	// Tokens is a list of imported token contracts.
	Tokens []*Token
}

// NewWallet creates a new NEO wallet at the given location.
func NewWallet(location string) (*Wallet, error) {
	file, err := os.Create(location)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	return newWallet(file), nil
}

// NewInMemoryWallet creates a new NEO wallet without linking to the read file on file system.
// If wallet required to be written to the file system, [Wallet.SetPath] should be used to set the path.
func NewInMemoryWallet() *Wallet {
	return newWallet(nil)
}

// NewWalletFromFile creates a Wallet from the given wallet file path.
func NewWalletFromFile(path string) (*Wallet, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("open wallet: %w", err)
	}
	defer file.Close()

	wall := &Wallet{
		path: file.Name(),
	}
	if err := json.NewDecoder(file).Decode(wall); err != nil {
		return nil, fmt.Errorf("unmarshal wallet: %w", err)
	}
	return wall, nil
}

// NewWalletFromBytes creates a [Wallet] from the given byte slice.
// Parameter wallet contains JSON representation of wallet, see [Wallet.JSON] for details.
//
// NewWalletFromBytes constructor doesn't set wallet's path. If you want to save the wallet to file system,
// use [Wallet.SetPath].
func NewWalletFromBytes(wallet []byte) (*Wallet, error) {
	wall := &Wallet{}
	if err := json.NewDecoder(bytes.NewReader(wallet)).Decode(wall); err != nil {
		return nil, fmt.Errorf("unmarshal wallet: %w", err)
	}

	return wall, nil
}

func newWallet(rw io.ReadWriter) *Wallet {
	var path string
	if f, ok := rw.(*os.File); ok {
		path = f.Name()
	}
	return &Wallet{
		Version:  walletVersion,
		Accounts: []*Account{},
		Scrypt:   keys.NEP2ScryptParams(),
		path:     path,
	}
}

// CreateAccount generates a new account for the end user and encrypts
// the private key with the given passphrase.
func (w *Wallet) CreateAccount(name, passphrase string) error {
	acc, err := NewAccount()
	if err != nil {
		return err
	}
	acc.Label = name
	if err := acc.Encrypt(passphrase, w.Scrypt); err != nil {
		return err
	}
	w.AddAccount(acc)
	return w.Save()
}

// AddAccount adds an existing Account to the wallet.
func (w *Wallet) AddAccount(acc *Account) {
	w.Accounts = append(w.Accounts, acc)
}

// RemoveAccount removes an Account with the specified addr
// from the wallet.
func (w *Wallet) RemoveAccount(addr string) error {
	for i, acc := range w.Accounts {
		if acc.Address == addr {
			copy(w.Accounts[i:], w.Accounts[i+1:])
			w.Accounts = w.Accounts[:len(w.Accounts)-1]
			return nil
		}
	}
	return errors.New("account wasn't found")
}

// AddToken adds a new token to a wallet.
func (w *Wallet) AddToken(tok *Token) {
	w.Extra.Tokens = append(w.Extra.Tokens, tok)
}

// RemoveToken removes the token with the specified hash from the wallet.
func (w *Wallet) RemoveToken(h util.Uint160) error {
	for i, tok := range w.Extra.Tokens {
		if tok.Hash.Equals(h) {
			copy(w.Extra.Tokens[i:], w.Extra.Tokens[i+1:])
			w.Extra.Tokens = w.Extra.Tokens[:len(w.Extra.Tokens)-1]
			return nil
		}
	}
	return errors.New("token wasn't found")
}

// Path returns the location of the wallet on the filesystem.
func (w *Wallet) Path() string {
	return w.path
}

// SetPath sets the location of the wallet on the filesystem.
func (w *Wallet) SetPath(path string) {
	w.path = path
}

// Save saves the wallet data to the file located at the path that was either provided
// via [NewWalletFromFile] constructor or via [Wallet.SetPath].
//
// Returns [ErrPathIsEmpty] if wallet path is not set. See [Wallet.SetPath].
func (w *Wallet) Save() error {
	data, err := json.Marshal(w)
	if err != nil {
		return err
	}

	return w.writeRaw(data)
}

// SavePretty saves the wallet in a beautiful JSON.
//
// Returns [ErrPathIsEmpty] if wallet path is not set. See [Wallet.SetPath].
func (w *Wallet) SavePretty() error {
	data, err := json.MarshalIndent(w, "", "  ")
	if err != nil {
		return err
	}

	return w.writeRaw(data)
}

func (w *Wallet) writeRaw(data []byte) error {
	if w.path == "" {
		return ErrPathIsEmpty
	}

	return os.WriteFile(w.path, data, 0644)
}

// JSON outputs a pretty JSON representation of the wallet.
func (w *Wallet) JSON() ([]byte, error) {
	return json.MarshalIndent(w, " ", "	")
}

// Close closes all Wallet accounts making them incapable of signing anything
// (unless they're decrypted again). It's not doing anything to the underlying
// wallet file.
func (w *Wallet) Close() {
	for _, acc := range w.Accounts {
		acc.Close()
	}
}

// GetAccount returns an account corresponding to the provided scripthash.
func (w *Wallet) GetAccount(h util.Uint160) *Account {
	addr := address.Uint160ToString(h)
	for _, acc := range w.Accounts {
		if acc.Address == addr {
			return acc
		}
	}

	return nil
}

// GetChangeAddress returns the default address to send transaction's change to.
func (w *Wallet) GetChangeAddress() util.Uint160 {
	var res util.Uint160
	var acc *Account

	for i := range w.Accounts {
		if acc == nil || w.Accounts[i].Default {
			if w.Accounts[i].Contract != nil && scparser.IsSignatureContract(w.Accounts[i].Contract.Script) {
				acc = w.Accounts[i]
				if w.Accounts[i].Default {
					break
				}
			}
		}
	}
	if acc != nil {
		res = acc.Contract.ScriptHash()
	}
	return res
}
