package stmgr

import (
	"context"
	"fmt"

	"github.com/ipfs/go-cid"
	"go.opencensus.io/trace"

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

func (sm *StateManager) TipSetState(ctx context.Context, ts *types.TipSet) (st cid.Cid, rec cid.Cid, err error) {
	ctx, span := trace.StartSpan(ctx, "tipSetState")
	defer span.End()
	if span.IsRecordingEvents() {
		span.AddAttributes(trace.StringAttribute("tipset", fmt.Sprint(ts.Cids())))
	}

	ck := cidsToKey(ts.Cids())
	sm.stlk.Lock()
	cw, cwok := sm.compWait[ck]
	if cwok {
		sm.stlk.Unlock()
		span.AddAttributes(trace.BoolAttribute("waited", true))
		select {
		case <-cw:
			sm.stlk.Lock()
		case <-ctx.Done():
			return cid.Undef, cid.Undef, ctx.Err()
		}
	}
	cached, ok := sm.stCache[ck]
	if ok {
		sm.stlk.Unlock()
		span.AddAttributes(trace.BoolAttribute("cache", true))
		return cached[0], cached[1], nil
	}
	ch := make(chan struct{})
	sm.compWait[ck] = ch

	defer func() {
		sm.stlk.Lock()
		delete(sm.compWait, ck)
		if st != cid.Undef {
			sm.stCache[ck] = []cid.Cid{st, rec}
		}
		sm.stlk.Unlock()
		close(ch)
	}()

	sm.stlk.Unlock()

	if ts.Height() == 0 {
		// NB: This is here because the process that executes blocks requires that the
		// block miner reference a valid miner in the state tree. Unless we create some
		// magical genesis miner, this won't work properly, so we short circuit here
		// This avoids the question of 'who gets paid the genesis block reward'.
		// This also makes us not attempt to lookup the tipset state with
		// tryLookupTipsetState, which would cause a very long, very slow walk.
		return ts.Blocks()[0].ParentStateRoot, ts.Blocks()[0].ParentMessageReceipts, nil
	}

	// First, try to find the tipset in the current chain. If found, we can avoid re-executing
	// it.
	if st, rec, found := tryLookupTipsetState(ctx, sm.cs, ts); found {
		return st, rec, nil
	}

	st, rec, err = sm.tsExec.ExecuteTipSet(ctx, sm, ts, sm.tsExecMonitor, false)
	if err != nil {
		return cid.Undef, cid.Undef, err
	}

	return st, rec, nil
}

// Try to lookup a state & receipt CID for a given tipset by walking the chain instead of executing
// it. This will only successfully return the state/receipt CIDs if they're found in the state
// store.
//
// NOTE: This _won't_ recursively walk the receipt/state trees. It assumes that having the root
// implies having the rest of the tree. However, lotus generally makes that assumption anyways.
func tryLookupTipsetState(ctx context.Context, cs *store.ChainStore, ts *types.TipSet) (cid.Cid, cid.Cid, bool) {
	nextTs, err := cs.GetTipsetByHeight(ctx, ts.Height()+1, nil, false)
	if err != nil {
		// Nothing to see here. The requested height may be beyond the current head.
		return cid.Undef, cid.Undef, false
	}

	// Make sure we're on the correct fork.
	if nextTs.Parents() != ts.Key() {
		// Also nothing to see here. This just means that the requested tipset is on a
		// different fork.
		return cid.Undef, cid.Undef, false
	}

	stateCid := nextTs.ParentState()
	receiptCid := nextTs.ParentMessageReceipts()

	// Make sure we have the parent state.
	if hasState, err := cs.StateBlockstore().Has(ctx, stateCid); err != nil {
		log.Errorw("failed to lookup state-root in blockstore", "cid", stateCid, "error", err)
		return cid.Undef, cid.Undef, false
	} else if !hasState {
		// We have the chain but don't have the state. It looks like we need to try
		// executing?
		return cid.Undef, cid.Undef, false
	}

	// Make sure we have the receipts.
	if hasReceipts, err := cs.ChainBlockstore().Has(ctx, receiptCid); err != nil {
		log.Errorw("failed to lookup receipts in blockstore", "cid", receiptCid, "error", err)
		return cid.Undef, cid.Undef, false
	} else if !hasReceipts {
		// If we don't have the receipts, re-execute and try again.
		return cid.Undef, cid.Undef, false
	}

	return stateCid, receiptCid, true
}

func (sm *StateManager) ExecutionTraceWithMonitor(ctx context.Context, ts *types.TipSet, em ExecMonitor) (cid.Cid, error) {
	st, _, err := sm.tsExec.ExecuteTipSet(ctx, sm, ts, em, true)
	return st, err
}

func (sm *StateManager) ExecutionTrace(ctx context.Context, ts *types.TipSet) (cid.Cid, []*api.InvocResult, error) {
	tsKey := ts.Key()

	// check if we have the trace for this tipset in the cache
	if execTraceCacheSize > 0 {
		sm.execTraceCacheLock.Lock()
		if entry, ok := sm.execTraceCache.Get(tsKey); ok {
			// we have to make a deep copy since caller can modify the invocTrace
			// and we don't want that to change what we store in cache
			invocTraceCopy := makeDeepCopy(entry.invocTrace)
			sm.execTraceCacheLock.Unlock()
			return entry.postStateRoot, invocTraceCopy, nil
		}
		sm.execTraceCacheLock.Unlock()
	}

	var invocTrace []*api.InvocResult
	st, err := sm.ExecutionTraceWithMonitor(ctx, ts, &InvocationTracer{trace: &invocTrace})
	if err != nil {
		return cid.Undef, nil, err
	}

	if execTraceCacheSize > 0 {
		invocTraceCopy := makeDeepCopy(invocTrace)

		sm.execTraceCacheLock.Lock()
		sm.execTraceCache.Add(tsKey, tipSetCacheEntry{st, invocTraceCopy})
		sm.execTraceCacheLock.Unlock()
	}

	return st, invocTrace, nil
}

func makeDeepCopy(invocTrace []*api.InvocResult) []*api.InvocResult {
	c := make([]*api.InvocResult, len(invocTrace))
	for i, ir := range invocTrace {
		if ir == nil {
			continue
		}
		tmp := *ir
		c[i] = &tmp
	}

	return c
}