package api

import (
	"context"
	"encoding/hex"
	"fmt"

	log "github.com/sirupsen/logrus"

	"github.com/vapor/account"
	"github.com/vapor/asset"
	"github.com/vapor/blockchain/query"
	"github.com/vapor/blockchain/signers"
	"github.com/vapor/common/arithmetic"
	"github.com/vapor/consensus"
	"github.com/vapor/crypto/ed25519"
	"github.com/vapor/crypto/ed25519/chainkd"
	chainjson "github.com/vapor/encoding/json"
	"github.com/vapor/errors"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
)

// POST /list-accounts
func (a *API) listAccounts(ctx context.Context, filter struct {
	ID    string `json:"id"`
	Alias string `json:"alias"`
}) Response {
	accountID := filter.ID
	if filter.Alias != "" {
		acc, err := a.wallet.AccountMgr.FindByAlias(filter.Alias)
		if err != nil {
			return NewErrorResponse(err)
		}
		accountID = acc.ID
	}

	accounts, err := a.wallet.AccountMgr.ListAccounts(accountID)
	if err != nil {
		log.Errorf("listAccounts: %v", err)
		return NewErrorResponse(err)
	}

	annotatedAccounts := []query.AnnotatedAccount{}
	for _, acc := range accounts {
		annotatedAccounts = append(annotatedAccounts, *account.Annotated(acc))
	}

	return NewSuccessResponse(annotatedAccounts)
}

// POST /get-asset
func (a *API) getAsset(ctx context.Context, filter struct {
	ID string `json:"id"`
}) Response {
	ass, err := a.wallet.AssetReg.GetAsset(filter.ID)
	if err != nil {
		log.Errorf("getAsset: %v", err)
		return NewErrorResponse(err)
	}

	annotatedAsset, err := asset.Annotated(ass)
	if err != nil {
		return NewErrorResponse(err)
	}
	return NewSuccessResponse(annotatedAsset)
}

// POST /list-assets
func (a *API) listAssets(ctx context.Context, filter struct {
	ID string `json:"id"`
}) Response {
	assets, err := a.wallet.AssetReg.ListAssets(filter.ID)
	if err != nil {
		log.Errorf("listAssets: %v", err)
		return NewErrorResponse(err)
	}

	annotatedAssets := []*query.AnnotatedAsset{}
	for _, ass := range assets {
		annotatedAsset, err := asset.Annotated(ass)
		if err != nil {
			return NewErrorResponse(err)
		}
		annotatedAssets = append(annotatedAssets, annotatedAsset)
	}
	return NewSuccessResponse(annotatedAssets)
}

// POST /list-balances
func (a *API) listBalances(ctx context.Context, filter struct {
	AccountID    string `json:"account_id"`
	AccountAlias string `json:"account_alias"`
}) Response {
	accountID := filter.AccountID
	if filter.AccountAlias != "" {
		acc, err := a.wallet.AccountMgr.FindByAlias(filter.AccountAlias)
		if err != nil {
			return NewErrorResponse(err)
		}
		accountID = acc.ID
	}

	balances, err := a.wallet.GetAccountBalances(accountID, "")
	if err != nil {
		return NewErrorResponse(err)
	}
	return NewSuccessResponse(balances)
}

func (a *API) listAccountVotes(ctx context.Context, filter struct {
	AccountID    string `json:"account_id"`
	AccountAlias string `json:"account_alias"`
}) Response {
	accountID := filter.AccountID
	if filter.AccountAlias != "" {
		acc, err := a.wallet.AccountMgr.FindByAlias(filter.AccountAlias)
		if err != nil {
			return NewErrorResponse(err)
		}
		accountID = acc.ID
	}

	votes, err := a.wallet.GetAccountVotes(accountID, "")
	if err != nil {
		return NewErrorResponse(err)
	}
	return NewSuccessResponse(votes)
}

// POST /get-transaction
func (a *API) getTransaction(ctx context.Context, txInfo struct {
	TxID string `json:"tx_id"`
}) Response {
	var annotatedTx *query.AnnotatedTx
	var err error

	annotatedTx, err = a.wallet.GetTransactionByTxID(txInfo.TxID)
	if err != nil {
		// transaction not found in blockchain db, search it from unconfirmed db
		annotatedTx, err = a.wallet.GetUnconfirmedTxByTxID(txInfo.TxID)
		if err != nil {
			return NewErrorResponse(err)
		}
	}

	return NewSuccessResponse(annotatedTx)
}

// POST /list-transactions
func (a *API) listTransactions(ctx context.Context, filter struct {
	AccountID    string `json:"account_id"`
	AccountAlias string `json:"account_alias"`
	StartTxID    string `json:"start_tx_id"`
	Detail       bool   `json:"detail"`
	Unconfirmed  bool   `json:"unconfirmed"`
	Count        uint   `json:"count"`
}) Response {
	accountID := filter.AccountID
	if filter.AccountAlias != "" {
		acc, err := a.wallet.AccountMgr.FindByAlias(filter.AccountAlias)
		if err != nil {
			return NewErrorResponse(err)
		}
		accountID = acc.ID
	}

	if accountID == "" {
		return NewErrorResponse(account.ErrAccountIDEmpty)
	}

	transactions, err := a.wallet.GetTransactions(accountID, filter.StartTxID, filter.Count, filter.Unconfirmed)
	if err != nil {
		return NewErrorResponse(err)
	}

	if filter.Detail == false {
		txSummary := a.wallet.GetTransactionsSummary(transactions)
		return NewSuccessResponse(txSummary)
	}

	return NewSuccessResponse(transactions)
}

// POST /get-unconfirmed-transaction
func (a *API) getUnconfirmedTx(ctx context.Context, filter struct {
	TxID chainjson.HexBytes `json:"tx_id"`
}) Response {
	var tmpTxID [32]byte
	copy(tmpTxID[:], filter.TxID[:])

	txHash := bc.NewHash(tmpTxID)
	txPool := a.chain.GetTxPool()
	txDesc, err := txPool.GetTransaction(&txHash)
	if err != nil {
		return NewErrorResponse(err)
	}

	tx := &BlockTx{
		ID:         txDesc.Tx.ID,
		Version:    txDesc.Tx.Version,
		Size:       txDesc.Tx.SerializedSize,
		TimeRange:  txDesc.Tx.TimeRange,
		Inputs:     []*query.AnnotatedInput{},
		Outputs:    []*query.AnnotatedOutput{},
		StatusFail: txDesc.StatusFail,
	}

	resOutID := txDesc.Tx.ResultIds[0]
	resOut := txDesc.Tx.Entries[*resOutID]
	switch out := resOut.(type) {
	case *bc.IntraChainOutput:
		tx.MuxID = *out.Source.Ref
	case *bc.VoteOutput:
		tx.MuxID = *out.Source.Ref
	case *bc.Retirement:
		tx.MuxID = *out.Source.Ref
	}

	for i := range txDesc.Tx.Inputs {
		tx.Inputs = append(tx.Inputs, a.wallet.BuildAnnotatedInput(txDesc.Tx, uint32(i)))
	}
	for i := range txDesc.Tx.Outputs {
		tx.Outputs = append(tx.Outputs, a.wallet.BuildAnnotatedOutput(txDesc.Tx, i))
	}

	return NewSuccessResponse(tx)
}

type unconfirmedTxsResp struct {
	Total uint64    `json:"total"`
	TxIDs []bc.Hash `json:"tx_ids"`
}

// POST /list-unconfirmed-transactions
func (a *API) listUnconfirmedTxs(ctx context.Context) Response {
	txIDs := []bc.Hash{}

	txPool := a.chain.GetTxPool()
	txs := txPool.GetTransactions()
	for _, txDesc := range txs {
		txIDs = append(txIDs, bc.Hash(txDesc.Tx.ID))
	}

	return NewSuccessResponse(&unconfirmedTxsResp{
		Total: uint64(len(txIDs)),
		TxIDs: txIDs,
	})
}

// RawTx is the tx struct for getRawTransaction
type RawTx struct {
	ID        bc.Hash                  `json:"tx_id"`
	Version   uint64                   `json:"version"`
	Size      uint64                   `json:"size"`
	TimeRange uint64                   `json:"time_range"`
	Inputs    []*query.AnnotatedInput  `json:"inputs"`
	Outputs   []*query.AnnotatedOutput `json:"outputs"`
	Fee       uint64                   `json:"fee"`
}

// POST /decode-raw-transaction
func (a *API) decodeRawTransaction(ctx context.Context, ins struct {
	Tx types.Tx `json:"raw_transaction"`
}) Response {
	tx := &RawTx{
		ID:        ins.Tx.ID,
		Version:   ins.Tx.Version,
		Size:      ins.Tx.SerializedSize,
		TimeRange: ins.Tx.TimeRange,
		Inputs:    []*query.AnnotatedInput{},
		Outputs:   []*query.AnnotatedOutput{},
	}

	for i := range ins.Tx.Inputs {
		tx.Inputs = append(tx.Inputs, a.wallet.BuildAnnotatedInput(&ins.Tx, uint32(i)))
	}
	for i := range ins.Tx.Outputs {
		tx.Outputs = append(tx.Outputs, a.wallet.BuildAnnotatedOutput(&ins.Tx, i))
	}

	tx.Fee, _ = arithmetic.CalculateTxFee(&ins.Tx)
	return NewSuccessResponse(tx)
}

type ListUtxosReq struct {
	AccountID     string `json:"account_id"`
	AccountAlias  string `json:"account_alias"`
	ID            string `json:"id"`
	Unconfirmed   bool   `json:"unconfirmed"`
	SmartContract bool   `json:"smart_contract"`
	From          uint   `json:"from"`
	Count         uint   `json:"count"`
}

// POST /list-unspent-outputs
func (a *API) listUnspentOutputs(ctx context.Context, filter ListUtxosReq) Response {
	accountID := filter.AccountID
	if filter.AccountAlias != "" {
		acc, err := a.wallet.AccountMgr.FindByAlias(filter.AccountAlias)
		if err != nil {
			return NewErrorResponse(err)
		}
		accountID = acc.ID
	}
	accountUTXOs := a.wallet.GetAccountUtxos(accountID, filter.ID, filter.Unconfirmed, filter.SmartContract, false)

	UTXOs := []query.AnnotatedUTXO{}
	for _, utxo := range accountUTXOs {
		UTXOs = append([]query.AnnotatedUTXO{{
			AccountID:           utxo.AccountID,
			OutputID:            utxo.OutputID.String(),
			SourceID:            utxo.SourceID.String(),
			AssetID:             utxo.AssetID.String(),
			Amount:              utxo.Amount,
			SourcePos:           utxo.SourcePos,
			Program:             fmt.Sprintf("%x", utxo.ControlProgram),
			ControlProgramIndex: utxo.ControlProgramIndex,
			Address:             utxo.Address,
			ValidHeight:         utxo.ValidHeight,
			Alias:               a.wallet.AccountMgr.GetAliasByID(utxo.AccountID),
			AssetAlias:          a.wallet.AssetReg.GetAliasByID(utxo.AssetID.String()),
			Change:              utxo.Change,
		}}, UTXOs...)
	}
	start, end := getPageRange(len(UTXOs), filter.From, filter.Count)
	return NewSuccessResponse(UTXOs[start:end])
}

// return gasRate
func (a *API) gasRate() Response {
	gasrate := map[string]int64{"gas_rate": consensus.ActiveNetParams.VMGasRate}
	return NewSuccessResponse(gasrate)
}

// PubKeyInfo is structure of pubkey info
type PubKeyInfo struct {
	Pubkey string               `json:"pubkey"`
	Path   []chainjson.HexBytes `json:"derivation_path"`
}

// AccountPubkey is detail of account pubkey info
type AccountPubkey struct {
	RootXPub    chainkd.XPub `json:"root_xpub"`
	PubKeyInfos []PubKeyInfo `json:"pubkey_infos"`
}

func getPubkey(account *account.Account, change bool, index uint64) (*ed25519.PublicKey, []chainjson.HexBytes, error) {
	rawPath, err := signers.Path(account.Signer, signers.AccountKeySpace, change, index)
	if err != nil {
		return nil, nil, err
	}
	derivedXPub := account.XPubs[0].Derive(rawPath)
	pubkey := derivedXPub.PublicKey()
	var path []chainjson.HexBytes
	for _, p := range rawPath {
		path = append(path, chainjson.HexBytes(p))
	}

	return &pubkey, path, nil
}

// POST /list-pubkeys
func (a *API) listPubKeys(ctx context.Context, ins struct {
	AccountID    string `json:"account_id"`
	AccountAlias string `json:"account_alias"`
	PublicKey    string `json:"public_key"`
}) Response {
	var err error
	account := &account.Account{}
	if ins.AccountAlias != "" {
		account, err = a.wallet.AccountMgr.FindByAlias(ins.AccountAlias)
	} else {
		account, err = a.wallet.AccountMgr.FindByID(ins.AccountID)
	}

	if err != nil {
		return NewErrorResponse(err)
	}

	pubKeyInfos := []PubKeyInfo{}
	if account.DeriveRule == signers.BIP0032 {
		idx := a.wallet.AccountMgr.GetContractIndex(account.ID)
		for i := uint64(1); i <= idx; i++ {
			pubkey, path, err := getPubkey(account, false, i)
			if err != nil {
				return NewErrorResponse(err)
			}
			if ins.PublicKey != "" && ins.PublicKey != hex.EncodeToString(*pubkey) {
				continue
			}
			pubKeyInfos = append(pubKeyInfos, PubKeyInfo{
				Pubkey: hex.EncodeToString(*pubkey),
				Path:   path,
			})
		}
	} else if account.DeriveRule == signers.BIP0044 {
		idx := a.wallet.AccountMgr.GetBip44ContractIndex(account.ID, true)
		for i := uint64(1); i <= idx; i++ {
			pubkey, path, err := getPubkey(account, true, i)
			if err != nil {
				return NewErrorResponse(err)
			}
			if ins.PublicKey != "" && ins.PublicKey != hex.EncodeToString(*pubkey) {
				continue
			}
			pubKeyInfos = append(pubKeyInfos, PubKeyInfo{
				Pubkey: hex.EncodeToString(*pubkey),
				Path:   path,
			})
		}

		idx = a.wallet.AccountMgr.GetBip44ContractIndex(account.ID, false)
		for i := uint64(1); i <= idx; i++ {
			pubkey, path, err := getPubkey(account, false, i)
			if err != nil {
				return NewErrorResponse(err)
			}
			if ins.PublicKey != "" && ins.PublicKey != hex.EncodeToString(*pubkey) {
				continue
			}
			pubKeyInfos = append(pubKeyInfos, PubKeyInfo{
				Pubkey: hex.EncodeToString(*pubkey),
				Path:   path,
			})
		}
	}

	if len(pubKeyInfos) == 0 {
		return NewErrorResponse(errors.New("Not found publickey for the account"))
	}

	return NewSuccessResponse(&AccountPubkey{
		RootXPub:    account.XPubs[0],
		PubKeyInfos: pubKeyInfos,
	})
}
