package slashfilter

import (
	"fmt"

	"github.com/filecoin-project/lotus/build"

	"golang.org/x/xerrors"

	"github.com/ipfs/go-cid"
	ds "github.com/ipfs/go-datastore"
	"github.com/ipfs/go-datastore/namespace"

	"github.com/filecoin-project/go-state-types/abi"
	"github.com/filecoin-project/lotus/chain/types"
)

type SlashFilter struct {
	byEpoch   ds.Datastore // double-fork mining faults, parent-grinding fault
	byParents ds.Datastore // time-offset mining faults
}

func New(dstore ds.Batching) *SlashFilter {
	return &SlashFilter{
		byEpoch:   namespace.Wrap(dstore, ds.NewKey("/slashfilter/epoch")),
		byParents: namespace.Wrap(dstore, ds.NewKey("/slashfilter/parents")),
	}
}

func (f *SlashFilter) MinedBlock(bh *types.BlockHeader, parentEpoch abi.ChainEpoch) error {
	if build.IsNearUpgrade(bh.Height, build.UpgradeOrangeHeight) {
		return nil
	}

	epochKey := ds.NewKey(fmt.Sprintf("/%s/%d", bh.Miner, bh.Height))
	{
		// double-fork mining (2 blocks at one epoch)
		if err := checkFault(f.byEpoch, epochKey, bh, "double-fork mining faults"); err != nil {
			return err
		}
	}

	parentsKey := ds.NewKey(fmt.Sprintf("/%s/%x", bh.Miner, types.NewTipSetKey(bh.Parents...).Bytes()))
	{
		// time-offset mining faults (2 blocks with the same parents)
		if err := checkFault(f.byParents, parentsKey, bh, "time-offset mining faults"); err != nil {
			return err
		}
	}

	{
		// parent-grinding fault (didn't mine on top of our own block)

		// First check if we have mined a block on the parent epoch
		parentEpochKey := ds.NewKey(fmt.Sprintf("/%s/%d", bh.Miner, parentEpoch))
		have, err := f.byEpoch.Has(parentEpochKey)
		if err != nil {
			return err
		}

		if have {
			// If we had, make sure it's in our parent tipset
			cidb, err := f.byEpoch.Get(parentEpochKey)
			if err != nil {
				return xerrors.Errorf("getting other block cid: %w", err)
			}

			_, parent, err := cid.CidFromBytes(cidb)
			if err != nil {
				return err
			}

			var found bool
			for _, c := range bh.Parents {
				if c.Equals(parent) {
					found = true
				}
			}

			if !found {
				return xerrors.Errorf("produced block would trigger 'parent-grinding fault' consensus fault; miner: %s; bh: %s, expected parent: %s", bh.Miner, bh.Cid(), parent)
			}
		}
	}

	if err := f.byParents.Put(parentsKey, bh.Cid().Bytes()); err != nil {
		return xerrors.Errorf("putting byEpoch entry: %w", err)
	}

	if err := f.byEpoch.Put(epochKey, bh.Cid().Bytes()); err != nil {
		return xerrors.Errorf("putting byEpoch entry: %w", err)
	}

	return nil
}

func checkFault(t ds.Datastore, key ds.Key, bh *types.BlockHeader, faultType string) error {
	fault, err := t.Has(key)
	if err != nil {
		return err
	}

	if fault {
		cidb, err := t.Get(key)
		if err != nil {
			return xerrors.Errorf("getting other block cid: %w", err)
		}

		_, other, err := cid.CidFromBytes(cidb)
		if err != nil {
			return err
		}

		if other == bh.Cid() {
			return nil
		}

		return xerrors.Errorf("produced block would trigger '%s' consensus fault; miner: %s; bh: %s, other: %s", faultType, bh.Miner, bh.Cid(), other)
	}

	return nil
}
