package deals

import (
	"bytes"
	"context"

	"golang.org/x/xerrors"

	"github.com/filecoin-project/lotus/api"
	"github.com/filecoin-project/lotus/build"
	"github.com/filecoin-project/lotus/chain/actors"
	"github.com/filecoin-project/lotus/chain/stmgr"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/lotus/lib/cborutil"
)

type clientHandlerFunc func(ctx context.Context, deal ClientDeal) (func(*ClientDeal), error)

func (c *Client) handle(ctx context.Context, deal ClientDeal, cb clientHandlerFunc, next api.DealState) {
	go func() {
		mut, err := cb(ctx, deal)
		if err != nil {
			next = api.DealError
		}

		if err == nil && next == api.DealNoUpdate {
			return
		}

		select {
		case c.updated <- clientDealUpdate{
			newState: next,
			id:       deal.ProposalCid,
			err:      err,
			mut:      mut,
		}:
		case <-c.stop:
		}
	}()
}

func (c *Client) new(ctx context.Context, deal ClientDeal) (func(*ClientDeal), error) {
	resp, err := c.readStorageDealResp(deal)
	if err != nil {
		return nil, err
	}
	if err := c.disconnect(deal); err != nil {
		return nil, err
	}

	/* data transfer happens */
	if resp.State != api.DealAccepted {
		return nil, xerrors.Errorf("deal wasn't accepted (State=%d)", resp.State)
	}

	return func(info *ClientDeal) {
		info.PublishMessage = resp.PublishMessage
	}, nil
}

func (c *Client) accepted(ctx context.Context, deal ClientDeal) (func(*ClientDeal), error) {
	log.Infow("DEAL ACCEPTED!")

	pubmsg, err := c.chain.GetMessage(*deal.PublishMessage)
	if err != nil {
		return nil, xerrors.Errorf("getting deal pubsish message: %w", err)
	}

	pw, err := stmgr.GetMinerWorker(ctx, c.sm, nil, deal.Proposal.Provider)
	if err != nil {
		return nil, xerrors.Errorf("getting miner worker failed: %w", err)
	}

	if pubmsg.From != pw {
		return nil, xerrors.Errorf("deal wasn't published by storage provider: from=%s, provider=%s", pubmsg.From, deal.Proposal.Provider)
	}

	if pubmsg.To != actors.StorageMarketAddress {
		return nil, xerrors.Errorf("deal publish message wasn't set to StorageMarket actor (to=%s)", pubmsg.To)
	}

	if pubmsg.Method != actors.SMAMethods.PublishStorageDeals {
		return nil, xerrors.Errorf("deal publish message called incorrect method (method=%s)", pubmsg.Method)
	}

	var params actors.PublishStorageDealsParams
	if err := params.UnmarshalCBOR(bytes.NewReader(pubmsg.Params)); err != nil {
		return nil, err
	}

	dealIdx := -1
	for i, storageDeal := range params.Deals {
		// TODO: make it less hacky
		eq, err := cborutil.Equals(&deal.Proposal, &storageDeal.Proposal)
		if err != nil {
			return nil, err
		}
		if eq {
			dealIdx = i
			break
		}
	}

	if dealIdx == -1 {
		return nil, xerrors.Errorf("deal publish didn't contain our deal (message cid: %s)", deal.PublishMessage)
	}

	// TODO: timeout
	_, ret, err := c.sm.WaitForMessage(ctx, *deal.PublishMessage)
	if err != nil {
		return nil, xerrors.Errorf("waiting for deal publish message: %w", err)
	}
	if ret.ExitCode != 0 {
		return nil, xerrors.Errorf("deal publish failed: exit=%d", ret.ExitCode)
	}

	var res actors.PublishStorageDealResponse
	if err := res.UnmarshalCBOR(bytes.NewReader(ret.Return)); err != nil {
		return nil, err
	}

	return func(info *ClientDeal) {
		info.DealID = res.DealIDs[dealIdx]
	}, nil
}

func (c *Client) staged(ctx context.Context, deal ClientDeal) (func(*ClientDeal), error) {
	// TODO: Maybe wait for pre-commit

	return nil, nil
}

func (c *Client) sealing(ctx context.Context, deal ClientDeal) (func(*ClientDeal), error) {
	checkFunc := func(ts *types.TipSet) (done bool, more bool, err error) {
		sd, err := stmgr.GetStorageDeal(ctx, c.sm, deal.DealID, ts)
		if err != nil {
			// TODO: This may be fine for some errors
			return false, false, xerrors.Errorf("failed to look up deal on chain: %w", err)
		}

		if sd.ActivationEpoch > 0 {
			select {
			case c.updated <- clientDealUpdate{
				newState: api.DealComplete,
				id:       deal.ProposalCid,
			}:
			case <-c.stop:
			}

			return true, false, nil
		}

		return false, true, nil
	}

	called := func(msg *types.Message, ts *types.TipSet, curH uint64) (more bool, err error) {
		defer func() {
			if err != nil {
				select {
				case c.updated <- clientDealUpdate{
					newState: api.DealComplete,
					id:       deal.ProposalCid,
					err:      xerrors.Errorf("handling applied event: %w", err),
				}:
				case <-c.stop:
				}
			}
		}()

		if msg == nil {
			log.Error("timed out waiting for deal activation... what now?")
			return false, nil
		}

		var params actors.SectorProveCommitInfo
		if err := params.UnmarshalCBOR(bytes.NewReader(msg.Params)); err != nil {
			return false, err
		}

		var found bool
		for _, dealID := range params.DealIDs {
			if dealID == deal.DealID {
				found = true
				break
			}
		}

		if !found {
			return true, nil
		}

		sd, err := stmgr.GetStorageDeal(ctx, c.sm, deal.DealID, ts)
		if err != nil {
			return false, xerrors.Errorf("failed to look up deal on chain: %w", err)
		}

		if sd.ActivationEpoch == 0 {
			return false, xerrors.Errorf("deal wasn't active: deal=%d, parentState=%s, h=%d", deal.DealID, ts.ParentState(), ts.Height())
		}

		log.Infof("Storage deal %d activated at epoch %d", deal.DealID, sd.ActivationEpoch)

		select {
		case c.updated <- clientDealUpdate{
			newState: api.DealComplete,
			id:       deal.ProposalCid,
		}:
		case <-c.stop:
		}

		return false, nil
	}

	revert := func(ctx context.Context, ts *types.TipSet) error {
		log.Warn("deal activation reverted; TODO: actually handle this!")
		// TODO: Just go back to DealSealing?
		return nil
	}

	if err := c.events.Called(checkFunc, called, revert, 3, build.SealRandomnessLookbackLimit, deal.Proposal.Provider, actors.MAMethods.ProveCommitSector); err != nil {
		return nil, xerrors.Errorf("failed to set up called handler")
	}

	return nil, nil
}