package sealing

import (
	"time"

	"golang.org/x/xerrors"

	"github.com/filecoin-project/go-state-types/exitcode"
	"github.com/filecoin-project/go-statemachine"

	"github.com/filecoin-project/lotus/api"
	"github.com/filecoin-project/lotus/build"
	"github.com/filecoin-project/lotus/chain/actors/policy"
	"github.com/filecoin-project/lotus/chain/types"
)

func (m *Sealing) handleFaulty(ctx statemachine.Context, sector SectorInfo) error {
	// TODO: noop because this is now handled by the PoSt scheduler. We can reuse
	//  this state for tracking faulty sectors, or remove it when that won't be
	//  a breaking change
	return nil
}

func (m *Sealing) handleFaultReported(ctx statemachine.Context, sector SectorInfo) error {
	if sector.FaultReportMsg == nil {
		return xerrors.Errorf("entered fault reported state without a FaultReportMsg cid")
	}

	mw, err := m.Api.StateWaitMsg(ctx.Context(), *sector.FaultReportMsg, build.MessageConfidence, api.LookbackNoLimit, true)
	if err != nil {
		return xerrors.Errorf("failed to wait for fault declaration: %w", err)
	}

	if mw.Receipt.ExitCode != 0 {
		log.Errorf("UNHANDLED: declaring sector fault failed (exit=%d, msg=%s) (id: %d)", mw.Receipt.ExitCode, *sector.FaultReportMsg, sector.SectorNumber)
		return xerrors.Errorf("UNHANDLED: submitting fault declaration failed (exit %d)", mw.Receipt.ExitCode)
	}

	return ctx.Send(SectorFaultedFinal{})
}

func (m *Sealing) handleTerminating(ctx statemachine.Context, sector SectorInfo) error {
	// First step of sector termination
	// * See if sector is live
	//  * If not, goto removing
	// * Add to termination queue
	// * Wait for message to land on-chain
	// * Check for correct termination
	// * wait for expiration (+winning lookback?)

	si, err := m.Api.StateSectorGetInfo(ctx.Context(), m.maddr, sector.SectorNumber, types.EmptyTSK)
	if err != nil {
		return ctx.Send(SectorTerminateFailed{xerrors.Errorf("getting sector info: %w", err)})
	}

	if si == nil {
		// either already terminated or not committed yet

		pci, err := m.Api.StateSectorPreCommitInfo(ctx.Context(), m.maddr, sector.SectorNumber, types.EmptyTSK)
		if err != nil {
			return ctx.Send(SectorTerminateFailed{xerrors.Errorf("checking precommit presence: %w", err)})
		}
		if pci != nil {
			return ctx.Send(SectorTerminateFailed{xerrors.Errorf("sector was precommitted but not proven, remove instead of terminating")})
		}

		return ctx.Send(SectorRemove{})
	}

	termCid, terminated, err := m.terminator.AddTermination(ctx.Context(), m.minerSectorID(sector.SectorNumber))
	if err != nil {
		return ctx.Send(SectorTerminateFailed{xerrors.Errorf("queueing termination: %w", err)})
	}

	if terminated {
		return ctx.Send(SectorTerminating{Message: nil})
	}

	return ctx.Send(SectorTerminating{Message: &termCid})
}

func (m *Sealing) handleTerminateWait(ctx statemachine.Context, sector SectorInfo) error {
	if sector.TerminateMessage == nil {
		return xerrors.New("entered TerminateWait with nil TerminateMessage")
	}

	mw, err := m.Api.StateWaitMsg(ctx.Context(), *sector.TerminateMessage, build.MessageConfidence, api.LookbackNoLimit, true)
	if err != nil {
		return ctx.Send(SectorTerminateFailed{xerrors.Errorf("waiting for terminate message to land on chain: %w", err)})
	}

	if mw.Receipt.ExitCode != exitcode.Ok {
		return ctx.Send(SectorTerminateFailed{xerrors.Errorf("terminate message failed to execute: exit %d: %w", mw.Receipt.ExitCode, err)})
	}

	return ctx.Send(SectorTerminated{TerminatedAt: mw.Height})
}

func (m *Sealing) handleTerminateFinality(ctx statemachine.Context, sector SectorInfo) error {
	for {
		ts, err := m.Api.ChainHead(ctx.Context())
		if err != nil {
			return ctx.Send(SectorTerminateFailed{xerrors.Errorf("getting chain head: %w", err)})
		}

		nv, err := m.Api.StateNetworkVersion(ctx.Context(), ts.Key())
		if err != nil {
			return ctx.Send(SectorTerminateFailed{xerrors.Errorf("getting network version: %w", err)})
		}

		if ts.Height() >= sector.TerminatedAt+policy.GetWinningPoStSectorSetLookback(nv) {
			return ctx.Send(SectorRemove{})
		}

		toWait := time.Duration(ts.Height()-sector.TerminatedAt+policy.GetWinningPoStSectorSetLookback(nv)) * time.Duration(build.BlockDelaySecs) * time.Second
		select {
		case <-time.After(toWait):
			continue
		case <-ctx.Context().Done():
			return ctx.Context().Err()
		}
	}
}

func (m *Sealing) handleRemoving(ctx statemachine.Context, sector SectorInfo) error {
	if err := m.sealer.Remove(ctx.Context(), m.minerSector(sector.SectorType, sector.SectorNumber)); err != nil {
		return ctx.Send(SectorRemoveFailed{err})
	}

	return ctx.Send(SectorRemoved{})
}

func (m *Sealing) handleProvingSector(ctx statemachine.Context, sector SectorInfo) error {
	// TODO: track sector health / expiration

	m.inputLk.Lock()
	// in case we revert into Proving without going into Available
	delete(m.available, m.minerSectorID(sector.SectorNumber))
	m.inputLk.Unlock()

	// guard against manual state updates from snap-deals states into Proving
	// note: normally snap deals should be aborted through the abort command, but
	// apparently sometimes some SPs would use update-state to force the sector back
	// into the Proving state, breaking the deal input pipeline in the process.
	m.cleanupAssignedDeals(sector)

	// TODO: Watch termination
	// TODO: Auto-extend if set

	return nil
}

func (m *Sealing) handleAvailableSector(ctx statemachine.Context, sector SectorInfo) error {
	m.inputLk.Lock()
	m.available[m.minerSectorID(sector.SectorNumber)] = struct{}{}
	m.inputLk.Unlock()
	// TODO: Watch termination
	// TODO: Auto-extend if set

	return nil
}
