package commands

import (
	"bytes"
	"context"
	"sort"

	"github.com/RoaringBitmap/roaring/roaring64"
	"github.com/ledgerwatch/erigon-lib/kv"
	"github.com/ledgerwatch/erigon/common"
	"github.com/ledgerwatch/erigon/common/changeset"
	"github.com/ledgerwatch/erigon/core/rawdb"
	"github.com/ledgerwatch/erigon/core/types/accounts"
)

func (api *OtterscanAPIImpl) GetTransactionBySenderAndNonce(ctx context.Context, addr common.Address, nonce uint64) (*common.Hash, error) {
	tx, err := api.db.BeginRo(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	accHistoryC, err := tx.Cursor(kv.AccountsHistory)
	if err != nil {
		return nil, err
	}
	defer accHistoryC.Close()

	accChangesC, err := tx.CursorDupSort(kv.AccountChangeSet)
	if err != nil {
		return nil, err
	}
	defer accChangesC.Close()

	// Locate the chunk where the nonce happens
	acs := changeset.Mapper[kv.AccountChangeSet]
	k, v, err := accHistoryC.Seek(acs.IndexChunkKey(addr.Bytes(), 0))
	if err != nil {
		return nil, err
	}

	bitmap := roaring64.New()
	maxBlPrevChunk := uint64(0)
	var acc accounts.Account

	for {
		if k == nil || !bytes.HasPrefix(k, addr.Bytes()) {
			// Check plain state
			data, err := tx.GetOne(kv.PlainState, addr.Bytes())
			if err != nil {
				return nil, err
			}
			if err := acc.DecodeForStorage(data); err != nil {
				return nil, err
			}

			// Nonce changed in plain state, so it means the last block of last chunk
			// contains the actual nonce change
			if acc.Nonce > nonce {
				break
			}

			// Not found; asked for nonce still not used
			return nil, nil
		}

		// Inspect block changeset
		if _, err := bitmap.ReadFrom(bytes.NewReader(v)); err != nil {
			return nil, err
		}
		maxBl := bitmap.Maximum()
		data, err := acs.Find(accChangesC, maxBl, addr.Bytes())
		if err != nil {
			return nil, err
		}
		if err := acc.DecodeForStorage(data); err != nil {
			return nil, err
		}

		// Desired nonce was found in this chunk
		if acc.Nonce > nonce {
			break
		}

		maxBlPrevChunk = maxBl
		k, v, err = accHistoryC.Next()
		if err != nil {
			return nil, err
		}
	}

	// Locate the exact block inside chunk when the nonce changed
	blocks := bitmap.ToArray()
	var errSearch error = nil
	idx := sort.Search(len(blocks), func(i int) bool {
		if errSearch != nil {
			return false
		}

		// Locate the block changeset
		data, err := acs.Find(accChangesC, blocks[i], addr.Bytes())
		if err != nil {
			errSearch = err
			return false
		}

		if err := acc.DecodeForStorage(data); err != nil {
			errSearch = err
			return false
		}

		// Since the state contains the nonce BEFORE the block changes, we look for
		// the block when the nonce changed to be > the desired once, which means the
		// previous history block contains the actual change; it may contain multiple
		// nonce changes.
		return acc.Nonce > nonce
	})
	if errSearch != nil {
		return nil, errSearch
	}

	// Since the changeset contains the state BEFORE the change, we inspect
	// the block before the one we found; if it is the first block inside the chunk,
	// we use the last block from prev chunk
	nonceBlock := maxBlPrevChunk
	if idx > 0 {
		nonceBlock = blocks[idx-1]
	}
	found, txHash, err := api.findNonce(ctx, tx, addr, nonce, nonceBlock)
	if err != nil {
		return nil, err
	}
	if !found {
		return nil, nil
	}

	return &txHash, nil
}

func (api *OtterscanAPIImpl) findNonce(ctx context.Context, tx kv.Tx, addr common.Address, nonce uint64, blockNum uint64) (bool, common.Hash, error) {
	hash, err := rawdb.ReadCanonicalHash(tx, blockNum)
	if err != nil {
		return false, common.Hash{}, err
	}
	block, senders, err := api._blockReader.BlockWithSenders(ctx, tx, hash, blockNum)
	if err != nil {
		return false, common.Hash{}, err
	}

	txs := block.Transactions()
	for i, s := range senders {
		if s != addr {
			continue
		}

		t := txs[i]
		if t.GetNonce() == nonce {
			return true, t.Hash(), nil
		}
	}

	return false, common.Hash{}, nil
}
