package accounting

import (
	"crypto/ecdsa"
	"crypto/rand"
	"encoding/binary"
	"reflect"

	"github.com/mr-tron/base58"
	"github.com/nspcc-dev/neofs-api/chain"
	"github.com/nspcc-dev/neofs-api/decimal"
	"github.com/nspcc-dev/neofs-api/internal"
	"github.com/nspcc-dev/neofs-api/refs"
	crypto "github.com/nspcc-dev/neofs-crypto"
	"github.com/pkg/errors"
)

type (
	// Cheque structure that describes a user request for withdrawal of funds.
	Cheque struct {
		ID         ChequeID
		Owner      refs.OwnerID
		Amount     *decimal.Decimal
		Height     uint64
		Signatures []ChequeSignature
	}

	// BalanceReceiver interface that is used to retrieve user balance by address.
	BalanceReceiver interface {
		Balance(accountAddress string) (*Account, error)
	}

	// ChequeID is identifier of user request for withdrawal of funds.
	ChequeID string

	// CID type alias.
	CID = refs.CID

	// SGID type alias.
	SGID = refs.SGID

	// ChequeSignature contains public key and hash, and is used to verify signatures.
	ChequeSignature struct {
		Key  *ecdsa.PublicKey
		Hash []byte
	}
)

const (
	// ErrWrongSignature is raised when wrong signature is passed.
	ErrWrongSignature = internal.Error("wrong signature")

	// ErrWrongPublicKey is raised when wrong public key is passed.
	ErrWrongPublicKey = internal.Error("wrong public key")

	// ErrWrongChequeData is raised when passed bytes cannot not be parsed as valid Cheque.
	ErrWrongChequeData = internal.Error("wrong cheque data")

	// ErrInvalidLength is raised when passed bytes cannot not be parsed as valid ChequeID.
	ErrInvalidLength = internal.Error("invalid length")

	u16size = 2
	u64size = 8

	signaturesOffset = chain.AddressLength + refs.OwnerIDSize + u64size + u64size
)

// NewChequeID generates valid random ChequeID using crypto/rand.Reader.
func NewChequeID() (ChequeID, error) {
	d := make([]byte, chain.AddressLength)
	if _, err := rand.Read(d); err != nil {
		return "", err
	}

	id := base58.Encode(d)

	return ChequeID(id), nil
}

// String returns string representation of ChequeID.
func (b ChequeID) String() string { return string(b) }

// Empty returns true, if ChequeID is empty.
func (b ChequeID) Empty() bool { return len(b) == 0 }

// Valid validates ChequeID.
func (b ChequeID) Valid() bool {
	d, err := base58.Decode(string(b))
	return err == nil && len(d) == chain.AddressLength
}

// Bytes returns bytes representation of ChequeID.
func (b ChequeID) Bytes() []byte {
	d, err := base58.Decode(string(b))
	if err != nil {
		return make([]byte, chain.AddressLength)
	}
	return d
}

// Equal checks that current ChequeID is equal to passed ChequeID.
func (b ChequeID) Equal(b2 ChequeID) bool {
	return b.Valid() && b2.Valid() && string(b) == string(b2)
}

// Unmarshal tries to parse []byte into valid ChequeID.
func (b *ChequeID) Unmarshal(data []byte) error {
	*b = ChequeID(base58.Encode(data))
	if !b.Valid() {
		return ErrInvalidLength
	}
	return nil
}

// Size returns size (chain.AddressLength).
func (b ChequeID) Size() int {
	return chain.AddressLength
}

// MarshalTo tries to marshal ChequeID into passed bytes and returns
// count of copied bytes or error, if bytes len is not enough to contain ChequeID.
func (b ChequeID) MarshalTo(data []byte) (int, error) {
	if len(data) < chain.AddressLength {
		return 0, ErrInvalidLength
	}
	return copy(data, b.Bytes()), nil
}

// Equals checks that m and tx are valid and equal Tx values.
func (m Tx) Equals(tx Tx) bool {
	return m.From == tx.From &&
		m.To == tx.To &&
		m.Type == tx.Type &&
		m.Amount == tx.Amount
}

// Verify validates current Cheque and Signatures that are generated for current Cheque.
func (b Cheque) Verify() error {
	data := b.marshalBody()
	for i, sign := range b.Signatures {
		if err := crypto.VerifyRFC6979(sign.Key, data, sign.Hash); err != nil {
			return errors.Wrapf(ErrWrongSignature, "item #%d: %s", i, err.Error())
		}
	}

	return nil
}

// Sign is used to sign current Cheque and stores result inside b.Signatures.
func (b *Cheque) Sign(key *ecdsa.PrivateKey) error {
	hash, err := crypto.SignRFC6979(key, b.marshalBody())
	if err != nil {
		return err
	}

	b.Signatures = append(b.Signatures, ChequeSignature{
		Key:  &key.PublicKey,
		Hash: hash,
	})

	return nil
}

func (b *Cheque) marshalBody() []byte {
	buf := make([]byte, signaturesOffset)

	var offset int

	offset += copy(buf, b.ID.Bytes())
	offset += copy(buf[offset:], b.Owner.Bytes())

	binary.LittleEndian.PutUint64(buf[offset:], uint64(b.Amount.Value))
	offset += u64size

	binary.LittleEndian.PutUint64(buf[offset:], b.Height)

	return buf
}

func (b *Cheque) unmarshalBody(buf []byte) error {
	var offset int

	if len(buf) < signaturesOffset {
		return ErrWrongChequeData
	}

	{ // unmarshal UUID
		if err := b.ID.Unmarshal(buf[offset : offset+chain.AddressLength]); err != nil {
			return err
		}
		offset += chain.AddressLength
	}

	{ // unmarshal OwnerID
		if err := b.Owner.Unmarshal(buf[offset : offset+refs.OwnerIDSize]); err != nil {
			return err
		}
		offset += refs.OwnerIDSize
	}

	{ // unmarshal amount
		amount := int64(binary.LittleEndian.Uint64(buf[offset:]))
		b.Amount = decimal.New(amount)
		offset += u64size
	}

	{ // unmarshal height
		b.Height = binary.LittleEndian.Uint64(buf[offset:])
		offset += u64size
	}

	return nil
}

// MarshalBinary is used to marshal Cheque into bytes.
func (b Cheque) MarshalBinary() ([]byte, error) {
	var (
		count  = len(b.Signatures)
		buf    = make([]byte, b.Size())
		offset = copy(buf, b.marshalBody())
	)

	binary.LittleEndian.PutUint16(buf[offset:], uint16(count))
	offset += u16size

	for _, sign := range b.Signatures {
		key := crypto.MarshalPublicKey(sign.Key)
		offset += copy(buf[offset:], key)
		offset += copy(buf[offset:], sign.Hash)
	}

	return buf, nil
}

// Size returns size of Cheque (count of bytes needs to store it).
func (b Cheque) Size() int {
	return signaturesOffset + u16size +
		len(b.Signatures)*(crypto.PublicKeyCompressedSize+crypto.RFC6979SignatureSize)
}

// UnmarshalBinary tries to parse []byte into valid Cheque.
func (b *Cheque) UnmarshalBinary(buf []byte) error {
	if err := b.unmarshalBody(buf); err != nil {
		return err
	}

	body := buf[:signaturesOffset]

	count := int64(binary.LittleEndian.Uint16(buf[signaturesOffset:]))
	offset := signaturesOffset + u16size

	if ln := count * int64(crypto.PublicKeyCompressedSize+crypto.RFC6979SignatureSize); ln > int64(len(buf[offset:])) {
		return ErrWrongChequeData
	}

	for i := int64(0); i < count; i++ {
		sign := ChequeSignature{
			Key:  crypto.UnmarshalPublicKey(buf[offset : offset+crypto.PublicKeyCompressedSize]),
			Hash: make([]byte, crypto.RFC6979SignatureSize),
		}

		offset += crypto.PublicKeyCompressedSize
		if sign.Key == nil {
			return errors.Wrapf(ErrWrongPublicKey, "item #%d", i)
		}

		offset += copy(sign.Hash, buf[offset:offset+crypto.RFC6979SignatureSize])
		if err := crypto.VerifyRFC6979(sign.Key, body, sign.Hash); err != nil {
			return errors.Wrapf(ErrWrongSignature, "item #%d: %s (offset=%d, len=%d)", i, err.Error(), offset, len(sign.Hash))
		}

		b.Signatures = append(b.Signatures, sign)
	}

	return nil
}

// ErrNotEnoughFunds generates error using address and amounts.
func ErrNotEnoughFunds(addr string, needed, residue *decimal.Decimal) error {
	return errors.Errorf("not enough funds (requested=%s, residue=%s, addr=%s", needed, residue, addr)
}

func (m *Account) hasLockAcc(addr string) bool {
	for i := range m.LockAccounts {
		if m.LockAccounts[i].Address == addr {
			return true
		}
	}
	return false
}

// ValidateLock checks that account can be locked.
func (m *Account) ValidateLock() error {
	switch {
	case m.Address == "":
		return ErrEmptyAddress
	case m.ParentAddress == "":
		return ErrEmptyParentAddress
	case m.LockTarget == nil:
		return ErrEmptyLockTarget
	}

	switch v := m.LockTarget.Target.(type) {
	case *LockTarget_WithdrawTarget:
		if v.WithdrawTarget.Cheque != m.Address {
			return errors.Errorf("wrong cheque ID: expected %s, has %s", m.Address, v.WithdrawTarget.Cheque)
		}
	case *LockTarget_ContainerCreateTarget:
		switch {
		case v.ContainerCreateTarget.CID.Empty():
			return ErrEmptyContainerID
		}
	}
	return nil
}

// CanLock checks possibility to lock funds.
func (m *Account) CanLock(lockAcc *Account) error {
	switch {
	case m.ActiveFunds.LT(lockAcc.ActiveFunds):
		return ErrNotEnoughFunds(lockAcc.ParentAddress, lockAcc.ActiveFunds, m.ActiveFunds)
	case m.hasLockAcc(lockAcc.Address):
		return errors.Errorf("could not lock account(%s) funds: duplicating lock(%s)", m.Address, lockAcc.Address)
	default:
		return nil
	}
}

// LockForWithdraw checks that account contains locked funds by passed ChequeID.
func (m *Account) LockForWithdraw(chequeID string) bool {
	switch v := m.LockTarget.Target.(type) {
	case *LockTarget_WithdrawTarget:
		return v.WithdrawTarget.Cheque == chequeID
	}
	return false
}

// LockForContainerCreate checks that account contains locked funds for container creation.
func (m *Account) LockForContainerCreate(cid refs.CID) bool {
	switch v := m.LockTarget.Target.(type) {
	case *LockTarget_ContainerCreateTarget:
		return v.ContainerCreateTarget.CID.Equal(cid)
	}
	return false
}

// Equal checks that current Settlement is equal to passed Settlement.
func (m *Settlement) Equal(s *Settlement) bool {
	if s == nil || m.Epoch != s.Epoch || len(m.Transactions) != len(s.Transactions) {
		return false
	}
	return len(m.Transactions) == 0 || reflect.DeepEqual(m.Transactions, s.Transactions)
}