package api

import (
	set "gopkg.in/fatih/set.v0"

	"github.com/vapor/blockchain/query"
	chainjson "github.com/vapor/encoding/json"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
)

// return best block hash
func (a *API) getBestBlockHash() Response {
	blockHash := map[string]string{"block_hash": a.chain.BestBlockHash().String()}
	return NewSuccessResponse(blockHash)
}

// return current block count
func (a *API) getBlockCount() Response {
	blockHeight := map[string]uint64{"block_count": a.chain.BestBlockHeight()}
	return NewSuccessResponse(blockHeight)
}

// BlockTx is the tx struct for getBlock func
type BlockTx struct {
	ID         bc.Hash                  `json:"id"`
	Version    uint64                   `json:"version"`
	Size       uint64                   `json:"size"`
	TimeRange  uint64                   `json:"time_range"`
	Inputs     []*query.AnnotatedInput  `json:"inputs"`
	Outputs    []*query.AnnotatedOutput `json:"outputs"`
	StatusFail bool                     `json:"status_fail"`
	MuxID      bc.Hash                  `json:"mux_id"`
}

// BlockReq is used to handle getBlock req
type BlockReq struct {
	BlockHeight uint64             `json:"block_height"`
	BlockHash   chainjson.HexBytes `json:"block_hash"`
}

// GetBlockResp is the resp for getBlock api
type GetBlockResp struct {
	Hash                   *bc.Hash             `json:"hash"`
	Size                   uint64               `json:"size"`
	Version                uint64               `json:"version"`
	Height                 uint64               `json:"height"`
	PreviousBlockHash      *bc.Hash             `json:"previous_block_hash"`
	Timestamp              uint64               `json:"timestamp"`
	Witness                []chainjson.HexBytes `json:"witness"`
	Blocker                string               `json:"blocker"`
	TransactionsMerkleRoot *bc.Hash             `json:"transaction_merkle_root"`
	TransactionStatusHash  *bc.Hash             `json:"transaction_status_hash"`
	Transactions           []*BlockTx           `json:"transactions"`
}

// return block by hash/height
func (a *API) getBlock(ins BlockReq) Response {
	block, err := a.getBlockHelper(ins)
	if err != nil {
		return NewErrorResponse(err)
	}

	blockHash := block.Hash()
	txStatus, err := a.chain.GetTransactionStatus(&blockHash)
	rawBlock, err := block.MarshalText()
	if err != nil {
		return NewErrorResponse(err)
	}

	witness := make([]chainjson.HexBytes, len(block.Witness))
	for i, w := range block.Witness {
		witness[i] = w
	}
	var blocker string
	if block.Height > 0 {
		if blocker, err = a.chain.GetBlocker(&block.PreviousBlockHash, block.Timestamp); err != nil {
			return NewErrorResponse(err)
		}
	}

	resp := &GetBlockResp{
		Hash:                   &blockHash,
		Size:                   uint64(len(rawBlock)),
		Version:                block.Version,
		Height:                 block.Height,
		PreviousBlockHash:      &block.PreviousBlockHash,
		Timestamp:              block.Timestamp,
		Witness:                witness,
		Blocker:                blocker,
		TransactionsMerkleRoot: &block.TransactionsMerkleRoot,
		TransactionStatusHash:  &block.TransactionStatusHash,
		Transactions:           []*BlockTx{},
	}

	for i, orig := range block.Transactions {
		tx := &BlockTx{
			ID:        orig.ID,
			Version:   orig.Version,
			Size:      orig.SerializedSize,
			TimeRange: orig.TimeRange,
			Inputs:    []*query.AnnotatedInput{},
			Outputs:   []*query.AnnotatedOutput{},
		}
		tx.StatusFail, err = txStatus.GetStatus(i)
		if err != nil {
			return NewSuccessResponse(resp)
		}

		resOutID := orig.ResultIds[0]
		switch resOut := orig.Entries[*resOutID].(type) {
		case *bc.IntraChainOutput:
			tx.MuxID = *resOut.Source.Ref
		case *bc.CrossChainOutput:
			tx.MuxID = *resOut.Source.Ref
		case *bc.VoteOutput:
			tx.MuxID = *resOut.Source.Ref
		case *bc.Retirement:
			resRetire, _ := orig.Entries[*resOutID].(*bc.Retirement)
			tx.MuxID = *resRetire.Source.Ref
		}

		for i := range orig.Inputs {
			tx.Inputs = append(tx.Inputs, a.wallet.BuildAnnotatedInput(orig, uint32(i)))
		}
		for i := range orig.Outputs {
			tx.Outputs = append(tx.Outputs, a.wallet.BuildAnnotatedOutput(orig, i))
		}
		resp.Transactions = append(resp.Transactions, tx)
	}
	return NewSuccessResponse(resp)
}

// GetRawBlockResp is resp struct for getRawBlock API
type GetRawBlockResp struct {
	RawBlock          *types.Block          `json:"raw_block"`
	TransactionStatus *bc.TransactionStatus `json:"transaction_status"`
}

func (a *API) getRawBlock(ins BlockReq) Response {
	block, err := a.getBlockHelper(ins)
	if err != nil {
		return NewErrorResponse(err)
	}

	blockHash := block.Hash()
	txStatus, err := a.chain.GetTransactionStatus(&blockHash)
	if err != nil {
		return NewErrorResponse(err)
	}

	resp := GetRawBlockResp{
		RawBlock:          block,
		TransactionStatus: txStatus,
	}
	return NewSuccessResponse(resp)
}

// GetBlockHeaderResp is resp struct for getBlockHeader API
type GetBlockHeaderResp struct {
	BlockHeader *types.BlockHeader `json:"block_header"`
	Reward      uint64             `json:"reward"`
}

func (a *API) getBlockHeader(ins BlockReq) Response {
	block, err := a.getBlockHelper(ins)
	if err != nil {
		return NewErrorResponse(err)
	}

	resp := &GetBlockHeaderResp{
		BlockHeader: &block.BlockHeader,
		Reward:      block.Transactions[0].Outputs[0].AssetAmount().Amount,
	}
	return NewSuccessResponse(resp)
}

func (a *API) getBlockHelper(ins BlockReq) (*types.Block, error) {
	if len(ins.BlockHash) == 32 {
		hash := hexBytesToHash(ins.BlockHash)
		return a.chain.GetBlockByHash(&hash)
	} else {
		return a.chain.GetBlockByHeight(ins.BlockHeight)
	}
}

func hexBytesToHash(hexBytes chainjson.HexBytes) bc.Hash {
	b32 := [32]byte{}
	copy(b32[:], hexBytes)
	return bc.NewHash(b32)
}

// MerkleBlockReq is used to handle getTxOutProof req
type MerkleBlockReq struct {
	TxIDs     []chainjson.HexBytes `json:"tx_ids"`
	BlockHash chainjson.HexBytes   `json:"block_hash"`
}

// GetMerkleBlockResp is resp struct for GetTxOutProof API
type GetMerkleBlockResp struct {
	BlockHeader  types.BlockHeader `json:"block_header"`
	TxHashes     []*bc.Hash        `json:"tx_hashes"`
	StatusHashes []*bc.Hash        `json:"status_hashes"`
	Flags        []uint32          `json:"flags"`
	MatchedTxIDs []*bc.Hash        `json:"matched_tx_ids"`
}

func (a *API) getMerkleProof(ins MerkleBlockReq) Response {
	blockReq := BlockReq{BlockHash: ins.BlockHash}
	block, err := a.getBlockHelper(blockReq)
	if err != nil {
		return NewErrorResponse(err)
	}

	matchedTxs := getMatchedTx(block.Transactions, ins.TxIDs)
	var matchedTxIDs []*bc.Hash
	for _, tx := range matchedTxs {
		matchedTxIDs = append(matchedTxIDs, &tx.ID)
	}

	hashes, compactFlags := types.GetTxMerkleTreeProof(block.Transactions, matchedTxs)
	flags := make([]uint32, len(compactFlags))
	for i, flag := range compactFlags {
		flags[i] = uint32(flag)
	}

	blockHash := block.Hash()
	statuses, err := a.chain.GetTransactionStatus(&blockHash)
	if err != nil {
		return NewErrorResponse(err)
	}

	statusHashes := types.GetStatusMerkleTreeProof(statuses.VerifyStatus, compactFlags)

	resp := &GetMerkleBlockResp{
		BlockHeader:  block.BlockHeader,
		TxHashes:     hashes,
		StatusHashes: statusHashes,
		Flags:        flags,
		MatchedTxIDs: matchedTxIDs,
	}
	return NewSuccessResponse(resp)
}

func getMatchedTx(txs []*types.Tx, filterTxIDs []chainjson.HexBytes) []*types.Tx {
	txIDSet := set.New()
	for _, txID := range filterTxIDs {
		hash := hexBytesToHash(txID)
		txIDSet.Add(hash.String())
	}

	var matchedTxs []*types.Tx
	for _, tx := range txs {
		hashStr := tx.ID.String()
		if txIDSet.Has(hashStr) {
			matchedTxs = append(matchedTxs, tx)
		}
	}
	return matchedTxs
}
