package wallet

import (
	log "github.com/sirupsen/logrus"

	"github.com/vapor/account"
	"github.com/vapor/consensus"
	"github.com/vapor/consensus/segwit"
	"github.com/vapor/crypto/sha3pool"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
)

// GetAccountUtxos return all account unspent outputs
func (w *Wallet) GetAccountUtxos(accountID string, id string, unconfirmed, isSmartContract bool, vote bool) []*account.UTXO {
	accountUtxos := []*account.UTXO{}
	if unconfirmed {
		accountUtxos = w.AccountMgr.ListUnconfirmedUtxo(accountID, isSmartContract)
	}

	confirmedUTXOs, err := w.Store.ListAccountUTXOs(id, isSmartContract)
	if err != nil {
		log.WithFields(log.Fields{"module": logModule, "err": err}).Error("GetAccountUtxos fail.")
	}
	accountUtxos = append(accountUtxos, confirmedUTXOs...)

	newAccountUtxos := []*account.UTXO{}
	for _, accountUtxo := range accountUtxos {
		if vote && accountUtxo.Vote == nil {
			continue
		}
		if accountID == accountUtxo.AccountID || accountID == "" {
			newAccountUtxos = append(newAccountUtxos, accountUtxo)
		}
	}
	return newAccountUtxos
}

func (w *Wallet) attachUtxos(b *types.Block, txStatus *bc.TransactionStatus, store WalletStore) {
	for txIndex, tx := range b.Transactions {
		statusFail, err := txStatus.GetStatus(txIndex)
		if err != nil {
			log.WithFields(log.Fields{"module": logModule, "err": err}).Error("attachUtxos fail on get tx status")
			continue
		}

		//hand update the transaction input utxos
		inputUtxos := txInToUtxos(tx, statusFail)
		for _, inputUtxo := range inputUtxos {
			if segwit.IsP2WScript(inputUtxo.ControlProgram) {
				w.AccountMgr.DeleteStandardUTXO(inputUtxo.OutputID)
			} else {
				store.DeleteContractUTXO(inputUtxo.OutputID)
			}
		}

		//hand update the transaction output utxos
		outputUtxos := txOutToUtxos(tx, statusFail, b.Height)
		utxos := w.filterAccountUtxo(outputUtxos)
		if err := w.saveUtxos(utxos, store); err != nil {
			log.WithFields(log.Fields{"module": logModule, "err": err}).Error("attachUtxos fail on saveUtxos")
		}
	}
}

func (w *Wallet) detachUtxos(b *types.Block, txStatus *bc.TransactionStatus, store WalletStore) {
	for txIndex := len(b.Transactions) - 1; txIndex >= 0; txIndex-- {
		tx := b.Transactions[txIndex]
		for j := range tx.Outputs {
			code := []byte{}
			switch resOut := tx.Entries[*tx.ResultIds[j]].(type) {
			case *bc.IntraChainOutput:
				if resOut.Source.Value.Amount == uint64(0) {
					continue
				}
				code = resOut.ControlProgram.Code
			case *bc.VoteOutput:
				code = resOut.ControlProgram.Code
			default:
				continue
			}

			if segwit.IsP2WScript(code) {
				w.AccountMgr.DeleteStandardUTXO(*tx.ResultIds[j])
			} else {
				store.DeleteContractUTXO(*tx.ResultIds[j])
			}
		}

		statusFail, err := txStatus.GetStatus(txIndex)
		if err != nil {
			log.WithFields(log.Fields{"module": logModule, "err": err}).Error("detachUtxos fail on get tx status")
			continue
		}

		inputUtxos := txInToUtxos(tx, statusFail)
		utxos := w.filterAccountUtxo(inputUtxos)
		if err := w.saveUtxos(utxos, store); err != nil {
			log.WithFields(log.Fields{"module": logModule, "err": err}).Error("detachUtxos fail on batchSaveUtxos")
			return
		}
	}
}

func (w *Wallet) filterAccountUtxo(utxos []*account.UTXO) []*account.UTXO {
	outsByScript := make(map[string][]*account.UTXO, len(utxos))
	for _, utxo := range utxos {
		scriptStr := string(utxo.ControlProgram)
		outsByScript[scriptStr] = append(outsByScript[scriptStr], utxo)
	}

	result := make([]*account.UTXO, 0, len(utxos))
	for s := range outsByScript {
		if !segwit.IsP2WScript([]byte(s)) {
			for _, utxo := range outsByScript[s] {
				result = append(result, utxo)
			}
			continue
		}

		var hash [32]byte
		sha3pool.Sum256(hash[:], []byte(s))
		cp, err := w.AccountMgr.GetControlProgram(bc.NewHash(hash))
		if err != nil && err != account.ErrFindCtrlProgram {
			log.WithFields(log.Fields{"module": logModule, "err": err}).Error("filterAccountUtxo fail.")
			continue
		}
		if cp == nil {
			continue
		}

		for _, utxo := range outsByScript[s] {
			utxo.AccountID = cp.AccountID
			utxo.Address = cp.Address
			utxo.ControlProgramIndex = cp.KeyIndex
			utxo.Change = cp.Change
			result = append(result, utxo)
		}
	}
	return result
}

func (w *Wallet) saveUtxos(utxos []*account.UTXO, store WalletStore) error {
	for _, utxo := range utxos {
		if segwit.IsP2WScript(utxo.ControlProgram) {
			if err := w.AccountMgr.SetStandardUTXO(utxo.OutputID, utxo); err != nil {
				return err
			}
		} else {
			if err := store.SetContractUTXO(utxo.OutputID, utxo); err != nil {
				return err
			}
		}
	}
	return nil
}

func txInToUtxos(tx *types.Tx, statusFail bool) []*account.UTXO {
	utxos := []*account.UTXO{}
	for _, inpID := range tx.Tx.InputIDs {

		e, err := tx.Entry(inpID)
		if err != nil {
			continue
		}
		utxo := &account.UTXO{}
		switch inp := e.(type) {
		case *bc.Spend:
			resOut, err := tx.IntraChainOutput(*inp.SpentOutputId)
			if err != nil {
				log.WithFields(log.Fields{"module": logModule, "err": err}).Error("txInToUtxos fail on get resOut for spedn")
				continue
			}
			if statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
				continue
			}
			utxo = &account.UTXO{
				OutputID:       *inp.SpentOutputId,
				AssetID:        *resOut.Source.Value.AssetId,
				Amount:         resOut.Source.Value.Amount,
				ControlProgram: resOut.ControlProgram.Code,
				SourceID:       *resOut.Source.Ref,
				SourcePos:      resOut.Source.Position,
			}
		case *bc.VetoInput:
			resOut, err := tx.VoteOutput(*inp.SpentOutputId)
			if err != nil {
				log.WithFields(log.Fields{"module": logModule, "err": err}).Error("txInToUtxos fail on get resOut for vetoInput")
				continue
			}
			if statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
				continue
			}
			utxo = &account.UTXO{
				OutputID:       *inp.SpentOutputId,
				AssetID:        *resOut.Source.Value.AssetId,
				Amount:         resOut.Source.Value.Amount,
				ControlProgram: resOut.ControlProgram.Code,
				SourceID:       *resOut.Source.Ref,
				SourcePos:      resOut.Source.Position,
				Vote:           resOut.Vote,
			}
		default:
			continue
		}
		utxos = append(utxos, utxo)
	}
	return utxos
}

func txOutToUtxos(tx *types.Tx, statusFail bool, blockHeight uint64) []*account.UTXO {
	validHeight := uint64(0)
	if tx.Inputs[0].InputType() == types.CoinbaseInputType {
		validHeight = blockHeight + consensus.ActiveNetParams.CoinbasePendingBlockNumber
	}

	utxos := []*account.UTXO{}
	for i, out := range tx.Outputs {
		entryOutput, err := tx.Entry(*tx.ResultIds[i])
		if err != nil {
			log.WithFields(log.Fields{"module": logModule, "err": err}).Error("txOutToUtxos fail on get entryOutput")
			continue
		}

		utxo := &account.UTXO{}
		switch bcOut := entryOutput.(type) {
		case *bc.IntraChainOutput:
			if (statusFail && *out.AssetAmount().AssetId != *consensus.BTMAssetID) || out.AssetAmount().Amount == uint64(0) {
				continue
			}
			utxo = &account.UTXO{
				OutputID:       *tx.OutputID(i),
				AssetID:        *out.AssetAmount().AssetId,
				Amount:         out.AssetAmount().Amount,
				ControlProgram: out.ControlProgram(),
				SourceID:       *bcOut.Source.Ref,
				SourcePos:      bcOut.Source.Position,
				ValidHeight:    validHeight,
			}

		case *bc.VoteOutput:
			if statusFail && *out.AssetAmount().AssetId != *consensus.BTMAssetID {
				continue
			}

			voteValidHeight := blockHeight + consensus.ActiveNetParams.VotePendingBlockNumber
			if validHeight < voteValidHeight {
				validHeight = voteValidHeight
			}

			utxo = &account.UTXO{
				OutputID:       *tx.OutputID(i),
				AssetID:        *out.AssetAmount().AssetId,
				Amount:         out.AssetAmount().Amount,
				ControlProgram: out.ControlProgram(),
				SourceID:       *bcOut.Source.Ref,
				SourcePos:      bcOut.Source.Position,
				ValidHeight:    validHeight,
				Vote:           bcOut.Vote,
			}

		default:
			log.WithFields(log.Fields{"module": logModule}).Warn("txOutToUtxos fail on get bcOut")
			continue
		}

		utxos = append(utxos, utxo)
	}
	return utxos
}
