package paychmgr

import (
	"context"

	"github.com/filecoin-project/specs-actors/actors/abi/big"

	"github.com/filecoin-project/specs-actors/actors/builtin/account"

	"github.com/filecoin-project/go-address"
	"github.com/filecoin-project/specs-actors/actors/builtin/paych"
	xerrors "golang.org/x/xerrors"

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

func (pm *Manager) loadPaychState(ctx context.Context, ch address.Address) (*types.Actor, *paych.State, error) {
	var pcast paych.State
	act, err := pm.sm.LoadActorState(ctx, ch, &pcast, nil)
	if err != nil {
		return nil, nil, err
	}

	return act, &pcast, nil
}

func (pm *Manager) loadStateChannelInfo(ctx context.Context, ch address.Address, dir uint64) (*ChannelInfo, error) {
	_, st, err := pm.loadPaychState(ctx, ch)
	if err != nil {
		return nil, err
	}

	var account account.State
	_, err = pm.sm.LoadActorState(ctx, st.From, &account, nil)
	if err != nil {
		return nil, err
	}
	from := account.Address
	_, err = pm.sm.LoadActorState(ctx, st.To, &account, nil)
	if err != nil {
		return nil, err
	}
	to := account.Address

	ci := &ChannelInfo{
		Channel:   ch,
		Direction: dir,
		NextLane:  nextLaneFromState(st),
	}

	if dir == DirOutbound {
		ci.Control = from
		ci.Target = to
	} else {
		ci.Control = to
		ci.Target = from
	}

	return ci, nil
}

func nextLaneFromState(st *paych.State) uint64 {
	if len(st.LaneStates) == 0 {
		return 0
	}

	maxLane := st.LaneStates[0].ID
	for _, state := range st.LaneStates {
		if state.ID > maxLane {
			maxLane = state.ID
		}
	}
	return maxLane + 1
}

// laneState gets the LaneStates from chain, then applies all vouchers in
// the data store over the chain state
func (pm *Manager) laneState(state *paych.State, ch address.Address) (map[uint64]*paych.LaneState, error) {
	// TODO: we probably want to call UpdateChannelState with all vouchers to be fully correct
	//  (but technically dont't need to)
	laneStates := make(map[uint64]*paych.LaneState, len(state.LaneStates))

	// Get the lane state from the chain
	for _, laneState := range state.LaneStates {
		laneStates[laneState.ID] = laneState
	}

	// Apply locally stored vouchers
	vouchers, err := pm.store.VouchersForPaych(ch)
	if err != nil && err != ErrChannelNotTracked {
		return nil, err
	}

	for _, v := range vouchers {
		for range v.Voucher.Merges {
			return nil, xerrors.Errorf("paych merges not handled yet")
		}

		// If there's a voucher for a lane that isn't in chain state just
		// create it
		ls, ok := laneStates[v.Voucher.Lane]
		if !ok {
			ls = &paych.LaneState{
				ID:       v.Voucher.Lane,
				Redeemed: types.NewInt(0),
				Nonce:    0,
			}
			laneStates[v.Voucher.Lane] = ls
		}

		if v.Voucher.Nonce < ls.Nonce {
			continue
		}

		ls.Nonce = v.Voucher.Nonce
		ls.Redeemed = v.Voucher.Amount
	}

	return laneStates, nil
}

// Get the total redeemed amount across all lanes, after applying the voucher
func (pm *Manager) totalRedeemedWithVoucher(laneStates map[uint64]*paych.LaneState, sv *paych.SignedVoucher) (big.Int, error) {
	// TODO: merges
	if len(sv.Merges) != 0 {
		return big.Int{}, xerrors.Errorf("dont currently support paych lane merges")
	}

	total := big.NewInt(0)
	for _, ls := range laneStates {
		total = big.Add(total, ls.Redeemed)
	}

	lane, ok := laneStates[sv.Lane]
	if ok {
		// If the voucher is for an existing lane, and the voucher nonce
		// and is higher than the lane nonce
		if sv.Nonce > lane.Nonce {
			// Add the delta between the redeemed amount and the voucher
			// amount to the total
			delta := big.Sub(sv.Amount, lane.Redeemed)
			total = big.Add(total, delta)
		}
	} else {
		// If the voucher is *not* for an existing lane, just add its
		// value (implicitly a new lane will be created for the voucher)
		total = big.Add(total, sv.Amount)
	}

	return total, nil
}