package client

import (
	"encoding/json"
	"errors"
	"net/http"
	"sort"

	"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/rpc/response/result"
	"github.com/nspcc-dev/neo-go/pkg/util"
	errs "github.com/pkg/errors"
)

/*
	Definition of types, helper functions and variables
	required for calculation of transaction inputs using
	NeoScan API.
*/

type (
	// NeoScanServer stores NEOSCAN URL and API path.
	NeoScanServer struct {
		URL  string // "protocol://host:port/"
		Path string // path to API endpoint without wallet address
	}

	// Unspent stores Unspents per asset
	Unspent struct {
		Unspent state.UnspentBalances
		Asset   string      // "NEO" / "GAS"
		Amount  util.Fixed8 // total unspent of this asset
	}

	// NeoScanBalance is a struct of NeoScan response to 'get_balance' request
	NeoScanBalance struct {
		Balance []*Unspent
		Address string
	}
)

// GetBalance performs a request to get balance for the address specified.
func (s NeoScanServer) GetBalance(address string) ([]*Unspent, error) {
	var (
		err        error
		req        *http.Request
		res        *http.Response
		balance    NeoScanBalance
		client     = http.Client{}
		balanceURL = s.URL + s.Path
	)

	if req, err = http.NewRequest(http.MethodGet, balanceURL+address, nil); err != nil {
		return nil, errs.Wrap(err, "Failed to compose HTTP request")
	}

	if res, err = client.Do(req); err != nil {
		return nil, errs.Wrap(err, "Failed to perform HTTP request")
	}

	defer res.Body.Close()

	if err = json.NewDecoder(res.Body).Decode(&balance); err != nil {
		return nil, errs.Wrap(err, "Failed to decode HTTP response")
	}
	return balance.Balance, nil
}

func filterSpecificAsset(asset string, balance []*Unspent, assetBalance *Unspent) {
	for _, us := range balance {
		if us.Asset == asset {
			assetBalance.Unspent = us.Unspent
			assetBalance.Asset = us.Asset
			assetBalance.Amount = us.Amount
			return
		}
	}
}

// CalculateInputs creates input transactions for the specified amount of given asset belonging to specified address.
func (s NeoScanServer) CalculateInputs(address string, assetIDUint util.Uint256, cost util.Fixed8) ([]transaction.Input, util.Fixed8, error) {
	var (
		err          error
		us           []*Unspent
		assetUnspent Unspent
		assetID      = result.GlobalAssets[assetIDUint.StringLE()]
	)
	if us, err = s.GetBalance(address); err != nil {
		return nil, util.Fixed8(0), errs.Wrapf(err, "Cannot get balance for address %v", address)
	}
	filterSpecificAsset(assetID, us, &assetUnspent)
	return unspentsToInputs(assetUnspent.Unspent, cost)
}

// unspentsToInputs uses UnspentBalances to create a slice of inputs for a new
// transcation containing the required amount of asset.
func unspentsToInputs(utxos state.UnspentBalances, required util.Fixed8) ([]transaction.Input, util.Fixed8, error) {
	var (
		num, i   uint16
		selected = util.Fixed8(0)
	)
	sort.Sort(utxos)

	for _, us := range utxos {
		if selected >= required {
			break
		}
		selected += us.Value
		num++
	}
	if selected < required {
		return nil, util.Fixed8(0), errors.New("cannot compose inputs for transaction; check sender balance")
	}

	inputs := make([]transaction.Input, 0, num)
	for i = 0; i < num; i++ {
		inputs = append(inputs, transaction.Input{
			PrevHash:  utxos[i].Tx,
			PrevIndex: utxos[i].Index,
		})
	}

	return inputs, selected, nil
}