package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/csv"
	"fmt"
	"io"
	"net/http"
	_ "net/http/pprof"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/ipfs/go-cid"
	logging "github.com/ipfs/go-log/v2"
	"github.com/mitchellh/go-homedir"
	"github.com/urfave/cli/v2"
	"golang.org/x/xerrors"

	"github.com/filecoin-project/go-address"
	"github.com/filecoin-project/go-bitfield"
	"github.com/filecoin-project/go-state-types/abi"
	"github.com/filecoin-project/go-state-types/big"
	"github.com/filecoin-project/go-state-types/builtin"
	minertypes "github.com/filecoin-project/go-state-types/builtin/v9/miner"
	"github.com/filecoin-project/go-state-types/exitcode"
	"github.com/filecoin-project/go-state-types/network"
	miner2 "github.com/filecoin-project/specs-actors/v2/actors/builtin/miner"

	"github.com/filecoin-project/lotus/api"
	"github.com/filecoin-project/lotus/build"
	lbuiltin "github.com/filecoin-project/lotus/chain/actors/builtin"
	"github.com/filecoin-project/lotus/chain/actors/builtin/market"
	"github.com/filecoin-project/lotus/chain/actors/builtin/miner"
	"github.com/filecoin-project/lotus/chain/types"
	lcli "github.com/filecoin-project/lotus/cli"
	"github.com/filecoin-project/lotus/tools/stats/sync"
)

var log = logging.Logger("main")

func main() {
	local := []*cli.Command{
		runCmd,
		recoverMinersCmd,
		findMinersCmd,
		versionCmd,
	}

	app := &cli.App{
		Name:  "lotus-pcr",
		Usage: "Refunds precommit initial pledge for all miners",
		Description: `Lotus PCR will attempt to reimbursement the initial pledge collateral of the PreCommitSector
   miner actor method for all miners on the network.

   The refund is sent directly to the miner actor, and not to the worker.

   The value refunded to the miner actor is not the value in the message itself, but calculated
   using StateMinerInitialPledgeCollateral of the PreCommitSector message params. This is to reduce
   abuse by over send in the PreCommitSector message and receiving more funds than was actually
   consumed by pledging the sector.

   No gas charges are refunded as part of this process, but a small 3% (by default) additional
   funds are provided.

   A single message will be produced per miner totaling their refund for all PreCommitSector messages
   in a tipset.
`,
		Version: build.UserVersion(),
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:    "lotus-path",
				EnvVars: []string{"LOTUS_PATH"},
				Value:   "~/.lotus", // TODO: Consider XDG_DATA_HOME
			},
			&cli.StringFlag{
				Name:    "repo",
				EnvVars: []string{"LOTUS_PCR_PATH"},
				Value:   "~/.lotuspcr", // TODO: Consider XDG_DATA_HOME
			},
			&cli.StringFlag{
				Name:    "log-level",
				EnvVars: []string{"LOTUS_PCR_LOG_LEVEL"},
				Hidden:  true,
				Value:   "info",
			},
		},
		Before: func(cctx *cli.Context) error {
			return logging.SetLogLevel("main", cctx.String("log-level"))
		},
		Commands: local,
	}

	if err := app.Run(os.Args); err != nil {
		log.Errorw("exit in error", "err", err)
		os.Exit(1)
		return
	}
}

var versionCmd = &cli.Command{
	Name:  "version",
	Usage: "Print version",
	Action: func(cctx *cli.Context) error {
		cli.VersionPrinter(cctx)
		return nil
	},
}

var findMinersCmd = &cli.Command{
	Name:  "find-miners",
	Usage: "find miners with a desired minimum balance",
	Description: `Find miners returns a list of miners and their balances that are below a
   threhold value. By default only the miner actor available balance is considered but other
   account balances can be included by enabling them through the flags.

   Examples
   Find all miners with an available balance below 100 FIL

     lotus-pcr find-miners --threshold 100

   Find all miners with a balance below zero, which includes the owner and worker balances

     lotus-pcr find-miners --threshold 0 --owner --worker
`,
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "no-sync",
			EnvVars: []string{"LOTUS_PCR_NO_SYNC"},
			Usage:   "do not wait for chain sync to complete",
		},
		&cli.IntFlag{
			Name:    "threshold",
			EnvVars: []string{"LOTUS_PCR_THRESHOLD"},
			Usage:   "balance below this limit will be printed",
			Value:   0,
		},
		&cli.BoolFlag{
			Name:  "owner",
			Usage: "include owner balance",
			Value: false,
		},
		&cli.BoolFlag{
			Name:  "worker",
			Usage: "include worker balance",
			Value: false,
		},
		&cli.BoolFlag{
			Name:  "control",
			Usage: "include control balance",
			Value: false,
		},
	},
	Action: func(cctx *cli.Context) error {
		ctx := context.Background()
		api, closer, err := lcli.GetFullNodeAPI(cctx)
		if err != nil {
			return err
		}
		defer closer()

		if !cctx.Bool("no-sync") {
			if err := sync.SyncWait(ctx, api); err != nil {
				return err
			}
		}

		owner := cctx.Bool("owner")
		worker := cctx.Bool("worker")
		control := cctx.Bool("control")
		threshold := uint64(cctx.Int("threshold"))

		rf := &refunder{
			api:       api,
			threshold: types.FromFil(threshold),
		}

		refundTipset, err := api.ChainHead(ctx)
		if err != nil {
			return err
		}

		balanceRefund, err := rf.FindMiners(ctx, refundTipset, NewMinersRefund(), owner, worker, control)
		if err != nil {
			return err
		}

		for _, maddr := range balanceRefund.Miners() {
			fmt.Printf("%s\t%s\n", maddr, types.FIL(balanceRefund.GetRefund(maddr)))
		}

		return nil
	},
}

var recoverMinersCmd = &cli.Command{
	Name:  "recover-miners",
	Usage: "Ensure all miners with a negative available balance have a FIL surplus across accounts",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:    "from",
			EnvVars: []string{"LOTUS_PCR_FROM"},
			Usage:   "wallet address to send refund from",
		},
		&cli.BoolFlag{
			Name:    "no-sync",
			EnvVars: []string{"LOTUS_PCR_NO_SYNC"},
			Usage:   "do not wait for chain sync to complete",
		},
		&cli.BoolFlag{
			Name:    "dry-run",
			EnvVars: []string{"LOTUS_PCR_DRY_RUN"},
			Usage:   "do not send any messages",
			Value:   false,
		},
		&cli.StringFlag{
			Name:  "output",
			Usage: "dump data as a csv format to this file",
		},
		&cli.IntFlag{
			Name:    "miner-recovery-cutoff",
			EnvVars: []string{"LOTUS_PCR_MINER_RECOVERY_CUTOFF"},
			Usage:   "maximum amount of FIL that can be sent to any one miner before refund percent is applied",
			Value:   3000,
		},
		&cli.IntFlag{
			Name:    "miner-recovery-bonus",
			EnvVars: []string{"LOTUS_PCR_MINER_RECOVERY_BONUS"},
			Usage:   "additional FIL to send to each miner",
			Value:   5,
		},
		&cli.IntFlag{
			Name:    "miner-recovery-refund-percent",
			EnvVars: []string{"LOTUS_PCR_MINER_RECOVERY_REFUND_PERCENT"},
			Usage:   "percent of refund to issue",
			Value:   110,
		},
	},
	Action: func(cctx *cli.Context) error {
		ctx := context.Background()
		api, closer, err := lcli.GetFullNodeAPI(cctx)
		if err != nil {
			log.Fatal(err)
		}
		defer closer()

		r, err := NewRepo(cctx.String("repo"))
		if err != nil {
			return err
		}

		if err := r.Open(); err != nil {
			return err
		}

		from, err := address.NewFromString(cctx.String("from"))
		if err != nil {
			return xerrors.Errorf("parsing source address (provide correct --from flag!): %w", err)
		}

		if !cctx.Bool("no-sync") {
			if err := sync.SyncWait(ctx, api); err != nil {
				return err
			}
		}

		dryRun := cctx.Bool("dry-run")
		minerRecoveryRefundPercent := cctx.Int("miner-recovery-refund-percent")
		minerRecoveryCutoff := uint64(cctx.Int("miner-recovery-cutoff"))
		minerRecoveryBonus := uint64(cctx.Int("miner-recovery-bonus"))

		blockmap := make(map[address.Address]struct{})

		for _, addr := range r.Blocklist() {
			blockmap[addr] = struct{}{}
		}

		rf := &refunder{
			api:                        api,
			wallet:                     from,
			dryRun:                     dryRun,
			minerRecoveryRefundPercent: minerRecoveryRefundPercent,
			minerRecoveryCutoff:        types.FromFil(minerRecoveryCutoff),
			minerRecoveryBonus:         types.FromFil(minerRecoveryBonus),
			blockmap:                   blockmap,
		}

		refundTipset, err := api.ChainHead(ctx)
		if err != nil {
			return err
		}

		balanceRefund, err := rf.EnsureMinerMinimums(ctx, refundTipset, NewMinersRefund(), cctx.String("output"))
		if err != nil {
			return err
		}

		if err := rf.Refund(ctx, "refund to recover miner", refundTipset, balanceRefund, 0); err != nil {
			return err
		}

		return nil
	},
}

var runCmd = &cli.Command{
	Name:  "run",
	Usage: "Start message reimpursement",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:    "from",
			EnvVars: []string{"LOTUS_PCR_FROM"},
			Usage:   "wallet address to send refund from",
		},
		&cli.BoolFlag{
			Name:    "no-sync",
			EnvVars: []string{"LOTUS_PCR_NO_SYNC"},
			Usage:   "do not wait for chain sync to complete",
		},
		&cli.IntFlag{
			Name:    "refund-percent",
			EnvVars: []string{"LOTUS_PCR_REFUND_PERCENT"},
			Usage:   "percent of refund to issue",
			Value:   103,
		},
		&cli.IntFlag{
			Name:    "max-message-queue",
			EnvVars: []string{"LOTUS_PCR_MAX_MESSAGE_QUEUE"},
			Usage:   "set the maximum number of messages that can be queue in the mpool",
			Value:   300,
		},
		&cli.IntFlag{
			Name:    "aggregate-tipsets",
			EnvVars: []string{"LOTUS_PCR_AGGREGATE_TIPSETS"},
			Usage:   "number of tipsets to process before sending messages",
			Value:   1,
		},
		&cli.BoolFlag{
			Name:    "dry-run",
			EnvVars: []string{"LOTUS_PCR_DRY_RUN"},
			Usage:   "do not send any messages",
			Value:   false,
		},
		&cli.BoolFlag{
			Name:    "pre-commit",
			EnvVars: []string{"LOTUS_PCR_PRE_COMMIT"},
			Usage:   "process PreCommitSector messages",
			Value:   true,
		},
		&cli.BoolFlag{
			Name:    "prove-commit",
			EnvVars: []string{"LOTUS_PCR_PROVE_COMMIT"},
			Usage:   "process ProveCommitSector messages",
			Value:   true,
		},
		&cli.BoolFlag{
			Name:    "windowed-post",
			EnvVars: []string{"LOTUS_PCR_WINDOWED_POST"},
			Usage:   "process SubmitWindowedPoSt messages and refund gas fees",
			Value:   false,
		},
		&cli.BoolFlag{
			Name:    "storage-deals",
			EnvVars: []string{"LOTUS_PCR_STORAGE_DEALS"},
			Usage:   "process PublishStorageDeals messages and refund gas fees",
			Value:   false,
		},
		&cli.IntFlag{
			Name:    "head-delay",
			EnvVars: []string{"LOTUS_PCR_HEAD_DELAY"},
			Usage:   "the number of tipsets to delay message processing to smooth chain reorgs",
			Value:   int(build.MessageConfidence),
		},
		&cli.BoolFlag{
			Name:    "miner-recovery",
			EnvVars: []string{"LOTUS_PCR_MINER_RECOVERY"},
			Usage:   "run the miner recovery job",
			Value:   false,
		},
		&cli.IntFlag{
			Name:    "miner-recovery-period",
			EnvVars: []string{"LOTUS_PCR_MINER_RECOVERY_PERIOD"},
			Usage:   "interval between running miner recovery",
			Value:   2880,
		},
		&cli.IntFlag{
			Name:    "miner-recovery-cutoff",
			EnvVars: []string{"LOTUS_PCR_MINER_RECOVERY_CUTOFF"},
			Usage:   "maximum amount of FIL that can be sent to any one miner before refund percent is applied",
			Value:   3000,
		},
		&cli.IntFlag{
			Name:    "miner-recovery-bonus",
			EnvVars: []string{"LOTUS_PCR_MINER_RECOVERY_BONUS"},
			Usage:   "additional FIL to send to each miner",
			Value:   5,
		},
		&cli.IntFlag{
			Name:    "miner-recovery-refund-percent",
			EnvVars: []string{"LOTUS_PCR_MINER_RECOVERY_REFUND_PERCENT"},
			Usage:   "percent of refund to issue",
			Value:   110,
		},
		&cli.StringFlag{
			Name:    "pre-fee-cap-max",
			EnvVars: []string{"LOTUS_PCR_PRE_FEE_CAP_MAX"},
			Usage:   "messages with a fee cap larger than this will be skipped when processing pre commit messages",
			Value:   "0.000000001",
		},
		&cli.StringFlag{
			Name:    "prove-fee-cap-max",
			EnvVars: []string{"LOTUS_PCR_PROVE_FEE_CAP_MAX"},
			Usage:   "messages with a prove cap larger than this will be skipped when processing pre commit messages",
			Value:   "0.000000001",
		},
		&cli.StringFlag{
			Name:  "http-server-timeout",
			Value: "30s",
		},
	},
	Action: func(cctx *cli.Context) error {
		timeout, err := time.ParseDuration(cctx.String("http-timeout"))
		if err != nil {
			return xerrors.Errorf("invalid time string %s: %x", cctx.String("http-timeout"), err)
		}

		go func() {
			server := &http.Server{
				Addr:              ":6060",
				ReadHeaderTimeout: timeout,
			}

			_ = server.ListenAndServe()
		}()

		ctx := context.Background()
		api, closer, err := lcli.GetFullNodeAPI(cctx)
		if err != nil {
			log.Fatal(err)
		}
		defer closer()

		r, err := NewRepo(cctx.String("repo"))
		if err != nil {
			return err
		}

		if err := r.Open(); err != nil {
			return err
		}

		from, err := address.NewFromString(cctx.String("from"))
		if err != nil {
			return xerrors.Errorf("parsing source address (provide correct --from flag!): %w", err)
		}

		if !cctx.Bool("no-sync") {
			if err := sync.SyncWait(ctx, api); err != nil {
				return err
			}
		}

		tipsetsCh, err := sync.BufferedTipsetChannel(ctx, api, r.Height(), cctx.Int("head-delay"))
		if err != nil {
			log.Fatal(err)
		}

		refundPercent := cctx.Int("refund-percent")
		maxMessageQueue := cctx.Int("max-message-queue")
		dryRun := cctx.Bool("dry-run")
		preCommitEnabled := cctx.Bool("pre-commit")
		proveCommitEnabled := cctx.Bool("prove-commit")
		windowedPoStEnabled := cctx.Bool("windowed-post")
		publishStorageDealsEnabled := cctx.Bool("storage-deals")
		aggregateTipsets := cctx.Int("aggregate-tipsets")
		minerRecoveryEnabled := cctx.Bool("miner-recovery")
		minerRecoveryPeriod := abi.ChainEpoch(int64(cctx.Int("miner-recovery-period")))
		minerRecoveryRefundPercent := cctx.Int("miner-recovery-refund-percent")
		minerRecoveryCutoff := uint64(cctx.Int("miner-recovery-cutoff"))
		minerRecoveryBonus := uint64(cctx.Int("miner-recovery-bonus"))

		preFeeCapMax, err := types.ParseFIL(cctx.String("pre-fee-cap-max"))
		if err != nil {
			return err
		}

		proveFeeCapMax, err := types.ParseFIL(cctx.String("prove-fee-cap-max"))
		if err != nil {
			return err
		}

		blockmap := make(map[address.Address]struct{})

		for _, addr := range r.Blocklist() {
			blockmap[addr] = struct{}{}
		}

		rf := &refunder{
			api:                        api,
			wallet:                     from,
			refundPercent:              refundPercent,
			minerRecoveryRefundPercent: minerRecoveryRefundPercent,
			minerRecoveryCutoff:        types.FromFil(minerRecoveryCutoff),
			minerRecoveryBonus:         types.FromFil(minerRecoveryBonus),
			dryRun:                     dryRun,
			preCommitEnabled:           preCommitEnabled,
			proveCommitEnabled:         proveCommitEnabled,
			windowedPoStEnabled:        windowedPoStEnabled,
			publishStorageDealsEnabled: publishStorageDealsEnabled,
			preFeeCapMax:               types.BigInt(preFeeCapMax),
			proveFeeCapMax:             types.BigInt(proveFeeCapMax),
			blockmap:                   blockmap,
		}

		var refunds = NewMinersRefund()
		var rounds = 0
		nextMinerRecovery := r.MinerRecoveryHeight() + minerRecoveryPeriod

		for tipset := range tipsetsCh {
			for k := range rf.blockmap {
				fmt.Printf("%s\n", k)
			}

			refunds, err = rf.ProcessTipset(ctx, tipset, refunds)
			if err != nil {
				return err
			}

			refundTipset, err := api.ChainHead(ctx)
			if err != nil {
				return err
			}

			if minerRecoveryEnabled && refundTipset.Height() >= nextMinerRecovery {
				recoveryRefund, err := rf.EnsureMinerMinimums(ctx, refundTipset, NewMinersRefund(), "")
				if err != nil {
					return err
				}

				if err := rf.Refund(ctx, "refund to recover miners", refundTipset, recoveryRefund, 0); err != nil {
					return err
				}

				if err := r.SetMinerRecoveryHeight(tipset.Height()); err != nil {
					return err
				}

				nextMinerRecovery = r.MinerRecoveryHeight() + minerRecoveryPeriod
			}

			rounds = rounds + 1
			if rounds < aggregateTipsets {
				continue
			}

			if err := rf.Refund(ctx, "refund stats", refundTipset, refunds, rounds); err != nil {
				return err
			}

			rounds = 0
			refunds = NewMinersRefund()

			if err := r.SetHeight(tipset.Height()); err != nil {
				return err
			}

			for {
				msgs, err := api.MpoolPending(ctx, types.EmptyTSK)
				if err != nil {
					log.Warnw("failed to fetch pending messages", "err", err)
					time.Sleep(time.Duration(int64(time.Second) * int64(build.BlockDelaySecs)))
					continue
				}

				count := 0
				for _, msg := range msgs {
					if msg.Message.From == from {
						count = count + 1
					}
				}

				if count < maxMessageQueue {
					break
				}

				log.Warnw("messages in mpool over max message queue", "message_count", count, "max_message_queue", maxMessageQueue)
				time.Sleep(time.Duration(int64(time.Second) * int64(build.BlockDelaySecs)))
			}
		}

		return nil
	},
}

type MinersRefund struct {
	refunds      map[address.Address]types.BigInt
	count        int
	totalRefunds types.BigInt
}

func NewMinersRefund() *MinersRefund {
	return &MinersRefund{
		refunds:      make(map[address.Address]types.BigInt),
		totalRefunds: types.NewInt(0),
	}
}

func (m *MinersRefund) Track(addr address.Address, value types.BigInt) {
	if _, ok := m.refunds[addr]; !ok {
		m.refunds[addr] = types.NewInt(0)
	}

	m.count = m.count + 1
	m.totalRefunds = types.BigAdd(m.totalRefunds, value)
	m.refunds[addr] = types.BigAdd(m.refunds[addr], value)
}

func (m *MinersRefund) Count() int {
	return m.count
}

func (m *MinersRefund) TotalRefunds() types.BigInt {
	return m.totalRefunds
}

func (m *MinersRefund) Miners() []address.Address {
	miners := make([]address.Address, 0, len(m.refunds))
	for addr := range m.refunds {
		miners = append(miners, addr)
	}

	return miners
}

func (m *MinersRefund) GetRefund(addr address.Address) types.BigInt {
	return m.refunds[addr]
}

type refunderNodeApi interface {
	ChainGetParentMessages(ctx context.Context, blockCid cid.Cid) ([]api.Message, error)
	ChainGetParentReceipts(ctx context.Context, blockCid cid.Cid) ([]*types.MessageReceipt, error)
	ChainGetTipSetByHeight(ctx context.Context, epoch abi.ChainEpoch, tsk types.TipSetKey) (*types.TipSet, error)
	ChainReadObj(context.Context, cid.Cid) ([]byte, error)
	StateMinerInitialPledgeCollateral(ctx context.Context, addr address.Address, precommitInfo minertypes.SectorPreCommitInfo, tsk types.TipSetKey) (types.BigInt, error)
	StateMinerInfo(context.Context, address.Address, types.TipSetKey) (api.MinerInfo, error)
	StateSectorPreCommitInfo(ctx context.Context, addr address.Address, sector abi.SectorNumber, tsk types.TipSetKey) (minertypes.SectorPreCommitOnChainInfo, error)
	StateMinerAvailableBalance(context.Context, address.Address, types.TipSetKey) (types.BigInt, error)
	StateMinerSectors(ctx context.Context, addr address.Address, filter *bitfield.BitField, tsk types.TipSetKey) ([]*miner.SectorOnChainInfo, error)
	StateMinerFaults(ctx context.Context, addr address.Address, tsk types.TipSetKey) (bitfield.BitField, error)
	StateListMiners(context.Context, types.TipSetKey) ([]address.Address, error)
	StateGetActor(ctx context.Context, actor address.Address, tsk types.TipSetKey) (*types.Actor, error)
	StateNetworkVersion(ctx context.Context, tsk types.TipSetKey) (network.Version, error)
	MpoolPushMessage(ctx context.Context, msg *types.Message, spec *api.MessageSendSpec) (*types.SignedMessage, error)
	GasEstimateGasPremium(ctx context.Context, nblocksincl uint64, sender address.Address, gaslimit int64, tsk types.TipSetKey) (types.BigInt, error)
	WalletBalance(ctx context.Context, addr address.Address) (types.BigInt, error)
}

type refunder struct {
	api                        refunderNodeApi
	wallet                     address.Address
	refundPercent              int
	minerRecoveryRefundPercent int
	minerRecoveryCutoff        big.Int
	minerRecoveryBonus         big.Int
	dryRun                     bool
	preCommitEnabled           bool
	proveCommitEnabled         bool
	windowedPoStEnabled        bool
	publishStorageDealsEnabled bool
	threshold                  big.Int
	blockmap                   map[address.Address]struct{}

	preFeeCapMax   big.Int
	proveFeeCapMax big.Int
}

func (r *refunder) FindMiners(ctx context.Context, tipset *types.TipSet, refunds *MinersRefund, owner, worker, control bool) (*MinersRefund, error) {
	miners, err := r.api.StateListMiners(ctx, tipset.Key())
	if err != nil {
		return nil, err
	}

	for _, maddr := range miners {
		mact, err := r.api.StateGetActor(ctx, maddr, types.EmptyTSK)
		if err != nil {
			log.Errorw("failed", "err", err, "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
			continue
		}

		if !mact.Balance.GreaterThan(big.Zero()) {
			continue
		}

		minerAvailableBalance, err := r.api.StateMinerAvailableBalance(ctx, maddr, tipset.Key())
		if err != nil {
			log.Errorw("failed", "err", err, "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
			continue
		}

		// Look up and find all addresses associated with the miner
		minerInfo, err := r.api.StateMinerInfo(ctx, maddr, tipset.Key())
		if err != nil {
			log.Errorw("failed", "err", err, "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
			continue
		}

		allAddresses := []address.Address{}

		if worker {
			allAddresses = append(allAddresses, minerInfo.Worker)
		}

		if owner {
			allAddresses = append(allAddresses, minerInfo.Owner)
		}

		if control {
			allAddresses = append(allAddresses, minerInfo.ControlAddresses...)
		}

		// Sum the balancer of all the addresses
		addrSum := big.Zero()
		addrCheck := make(map[address.Address]struct{}, len(allAddresses))
		for _, addr := range allAddresses {
			if _, found := addrCheck[addr]; !found {
				balance, err := r.api.WalletBalance(ctx, addr)
				if err != nil {
					log.Errorw("failed", "err", err, "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
					continue
				}

				addrSum = big.Add(addrSum, balance)
				addrCheck[addr] = struct{}{}
			}
		}

		totalAvailableBalance := big.Add(addrSum, minerAvailableBalance)

		if totalAvailableBalance.GreaterThanEqual(r.threshold) {
			continue
		}

		refunds.Track(maddr, totalAvailableBalance)

		log.Debugw("processing miner", "miner", maddr, "sectors", "available_balance", totalAvailableBalance)
	}

	return refunds, nil
}

func (r *refunder) EnsureMinerMinimums(ctx context.Context, tipset *types.TipSet, refunds *MinersRefund, output string) (*MinersRefund, error) {
	miners, err := r.api.StateListMiners(ctx, tipset.Key())
	if err != nil {
		return nil, err
	}

	w := io.Discard
	if len(output) != 0 {
		f, err := os.Create(output)
		if err != nil {
			return nil, err
		}

		defer f.Close() // nolint:errcheck

		w = bufio.NewWriter(f)
	}

	csvOut := csv.NewWriter(w)
	defer csvOut.Flush()
	if err := csvOut.Write([]string{"MinerID", "FaultedSectors", "AvailableBalance", "ProposedRefund"}); err != nil {
		return nil, err
	}

	for _, maddr := range miners {
		if _, found := r.blockmap[maddr]; found {
			log.Debugw("skipping blocked miner", "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
			continue
		}

		mact, err := r.api.StateGetActor(ctx, maddr, types.EmptyTSK)
		if err != nil {
			log.Errorw("failed", "err", err, "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
			continue
		}

		if !mact.Balance.GreaterThan(big.Zero()) {
			continue
		}

		minerAvailableBalance, err := r.api.StateMinerAvailableBalance(ctx, maddr, tipset.Key())
		if err != nil {
			log.Errorw("failed", "err", err, "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
			continue
		}

		// Look up and find all addresses associated with the miner
		minerInfo, err := r.api.StateMinerInfo(ctx, maddr, tipset.Key())
		if err != nil {
			log.Errorw("failed", "err", err, "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
			continue
		}

		allAddresses := []address.Address{minerInfo.Worker, minerInfo.Owner}
		allAddresses = append(allAddresses, minerInfo.ControlAddresses...)

		// Sum the balancer of all the addresses
		addrSum := big.Zero()
		addrCheck := make(map[address.Address]struct{}, len(allAddresses))
		for _, addr := range allAddresses {
			if _, found := addrCheck[addr]; !found {
				balance, err := r.api.WalletBalance(ctx, addr)
				if err != nil {
					log.Errorw("failed", "err", err, "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
					continue
				}

				addrSum = big.Add(addrSum, balance)
				addrCheck[addr] = struct{}{}
			}
		}

		faults, err := r.api.StateMinerFaults(ctx, maddr, tipset.Key())
		if err != nil {
			log.Errorw("failed to look up miner faults", "err", err, "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
			continue
		}

		faultsCount, err := faults.Count()
		if err != nil {
			log.Errorw("failed to get count of faults", "err", err, "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
			continue
		}

		if faultsCount == 0 {
			log.Debugw("skipping miner with zero faults", "height", tipset.Height(), "key", tipset.Key(), "miner", maddr)
			continue
		}

		totalAvailableBalance := big.Add(addrSum, minerAvailableBalance)
		balanceCutoff := big.Mul(big.Div(big.NewIntUnsigned(faultsCount), big.NewInt(10)), big.NewIntUnsigned(build.FilecoinPrecision))

		if totalAvailableBalance.GreaterThan(balanceCutoff) {
			log.Debugw(
				"skipping over miner with total available balance larger than refund",
				"height", tipset.Height(),
				"key", tipset.Key(),
				"miner", maddr,
				"available_balance", totalAvailableBalance,
				"balance_cutoff", balanceCutoff,
				"faults_count", faultsCount,
				"available_balance_fil", big.Div(totalAvailableBalance, big.NewIntUnsigned(build.FilecoinPrecision)).Int64(),
				"balance_cutoff_fil", big.Div(balanceCutoff, big.NewIntUnsigned(build.FilecoinPrecision)).Int64(),
			)
			continue
		}

		refundValue := big.Sub(balanceCutoff, totalAvailableBalance)
		if r.minerRecoveryRefundPercent > 0 {
			refundValue = types.BigMul(types.BigDiv(refundValue, types.NewInt(100)), types.NewInt(uint64(r.minerRecoveryRefundPercent)))
		}

		refundValue = big.Add(refundValue, r.minerRecoveryBonus)

		if refundValue.GreaterThan(r.minerRecoveryCutoff) {
			log.Infow(
				"skipping over miner with refund greater than refund cutoff",
				"height", tipset.Height(),
				"key", tipset.Key(),
				"miner", maddr,
				"available_balance", totalAvailableBalance,
				"balance_cutoff", balanceCutoff,
				"faults_count", faultsCount,
				"refund", refundValue,
				"available_balance_fil", big.Div(totalAvailableBalance, big.NewIntUnsigned(build.FilecoinPrecision)).Int64(),
				"balance_cutoff_fil", big.Div(balanceCutoff, big.NewIntUnsigned(build.FilecoinPrecision)).Int64(),
				"refund_fil", big.Div(refundValue, big.NewIntUnsigned(build.FilecoinPrecision)).Int64(),
			)
			continue
		}

		refunds.Track(maddr, refundValue)
		record := []string{
			maddr.String(),
			fmt.Sprintf("%d", faultsCount),
			big.Div(totalAvailableBalance, big.NewIntUnsigned(build.FilecoinPrecision)).String(),
			big.Div(refundValue, big.NewIntUnsigned(build.FilecoinPrecision)).String(),
		}
		if err := csvOut.Write(record); err != nil {
			return nil, err
		}

		log.Debugw(
			"processing miner",
			"miner", maddr,
			"faults_count", faultsCount,
			"available_balance", totalAvailableBalance,
			"refund", refundValue,
			"available_balance_fil", big.Div(totalAvailableBalance, big.NewIntUnsigned(build.FilecoinPrecision)).Int64(),
			"refund_fil", big.Div(refundValue, big.NewIntUnsigned(build.FilecoinPrecision)).Int64(),
		)
	}

	return refunds, nil
}

func (r *refunder) processTipsetStorageMarketActor(ctx context.Context, tipset *types.TipSet, msg api.Message, recp *types.MessageReceipt) (bool, string, types.BigInt, error) {

	m := msg.Message
	var refundValue types.BigInt
	var messageMethod string

	switch m.Method {
	case market.Methods.PublishStorageDeals:
		if !r.publishStorageDealsEnabled {
			return false, messageMethod, types.NewInt(0), nil
		}

		messageMethod = "PublishStorageDeals"

		if recp.ExitCode != exitcode.Ok {
			log.Debugw("skipping non-ok exitcode message", "method", messageMethod, "cid", msg.Cid, "miner", m.To, "exitcode", recp.ExitCode)
			return false, messageMethod, types.NewInt(0), nil
		}

		refundValue = types.BigMul(types.NewInt(uint64(recp.GasUsed)), tipset.Blocks()[0].ParentBaseFee)
	default:
		return false, messageMethod, types.NewInt(0), nil
	}

	return true, messageMethod, refundValue, nil
}

func (r *refunder) processTipsetStorageMinerActor(ctx context.Context, tipset *types.TipSet, msg api.Message, recp *types.MessageReceipt) (bool, string, types.BigInt, error) {

	m := msg.Message
	var refundValue types.BigInt
	var messageMethod string

	if _, found := r.blockmap[m.To]; found {
		log.Debugw("skipping blocked miner", "height", tipset.Height(), "key", tipset.Key(), "miner", m.To)
		return false, messageMethod, types.NewInt(0), nil
	}

	switch m.Method {
	case builtin.MethodsMiner.SubmitWindowedPoSt:
		if !r.windowedPoStEnabled {
			return false, messageMethod, types.NewInt(0), nil
		}

		messageMethod = "SubmitWindowedPoSt"

		if recp.ExitCode != exitcode.Ok {
			log.Debugw("skipping non-ok exitcode message", "method", messageMethod, "cid", msg.Cid, "miner", m.To, "exitcode", recp.ExitCode)
			return false, messageMethod, types.NewInt(0), nil
		}

		refundValue = types.BigMul(types.NewInt(uint64(recp.GasUsed)), tipset.Blocks()[0].ParentBaseFee)
	case builtin.MethodsMiner.ProveCommitSector:
		if !r.proveCommitEnabled {
			return false, messageMethod, types.NewInt(0), nil
		}

		messageMethod = "ProveCommitSector"

		if recp.ExitCode != exitcode.Ok {
			log.Debugw("skipping non-ok exitcode message", "method", messageMethod, "cid", msg.Cid, "miner", m.To, "exitcode", recp.ExitCode)
			return false, messageMethod, types.NewInt(0), nil
		}

		if m.GasFeeCap.GreaterThan(r.proveFeeCapMax) {
			log.Debugw("skipping high fee cap message", "method", messageMethod, "cid", msg.Cid, "miner", m.To, "gas_fee_cap", m.GasFeeCap, "fee_cap_max", r.proveFeeCapMax)
			return false, messageMethod, types.NewInt(0), nil
		}

		if tipset.Blocks()[0].ParentBaseFee.GreaterThan(r.proveFeeCapMax) {
			log.Debugw("skipping high base fee message", "method", messageMethod, "cid", msg.Cid, "miner", m.To, "basefee", tipset.Blocks()[0].ParentBaseFee, "fee_cap_max", r.proveFeeCapMax)
			return false, messageMethod, types.NewInt(0), nil
		}

		var sn abi.SectorNumber

		var proveCommitSector miner2.ProveCommitSectorParams
		if err := proveCommitSector.UnmarshalCBOR(bytes.NewBuffer(m.Params)); err != nil {
			log.Warnw("failed to decode provecommit params", "err", err, "method", messageMethod, "cid", msg.Cid, "miner", m.To)
			return false, messageMethod, types.NewInt(0), nil
		}

		sn = proveCommitSector.SectorNumber

		// We use the parent tipset key because precommit information is removed when ProveCommitSector is executed
		precommitChainInfo, err := r.api.StateSectorPreCommitInfo(ctx, m.To, sn, tipset.Parents())
		if err != nil {
			log.Warnw("failed to get precommit info for sector", "err", err, "method", messageMethod, "cid", msg.Cid, "miner", m.To, "sector_number", sn)
			return false, messageMethod, types.NewInt(0), nil
		}

		precommitTipset, err := r.api.ChainGetTipSetByHeight(ctx, precommitChainInfo.PreCommitEpoch, tipset.Key())
		if err != nil {
			log.Warnf("failed to lookup precommit epoch", "err", err, "method", messageMethod, "cid", msg.Cid, "miner", m.To, "sector_number", sn)
			return false, messageMethod, types.NewInt(0), nil
		}

		collateral, err := r.api.StateMinerInitialPledgeCollateral(ctx, m.To, precommitChainInfo.Info, precommitTipset.Key())
		if err != nil {
			log.Warnw("failed to get initial pledge collateral", "err", err, "method", messageMethod, "cid", msg.Cid, "miner", m.To, "sector_number", sn)
			return false, messageMethod, types.NewInt(0), nil
		}

		collateral = big.Sub(collateral, precommitChainInfo.PreCommitDeposit)
		if collateral.LessThan(big.Zero()) {
			log.Debugw("skipping zero pledge collateral difference", "method", messageMethod, "cid", msg.Cid, "miner", m.To, "sector_number", sn)
			return false, messageMethod, types.NewInt(0), nil
		}

		refundValue = collateral
		if r.refundPercent > 0 {
			refundValue = types.BigMul(types.BigDiv(refundValue, types.NewInt(100)), types.NewInt(uint64(r.refundPercent)))
		}
	case builtin.MethodsMiner.PreCommitSector:
		if !r.preCommitEnabled {
			return false, messageMethod, types.NewInt(0), nil
		}

		messageMethod = "PreCommitSector"

		if recp.ExitCode != exitcode.Ok {
			log.Debugw("skipping non-ok exitcode message", "method", messageMethod, "cid", msg.Cid, "miner", m.To, "exitcode", recp.ExitCode)
			return false, messageMethod, types.NewInt(0), nil
		}

		if m.GasFeeCap.GreaterThan(r.preFeeCapMax) {
			log.Debugw("skipping high fee cap message", "method", messageMethod, "cid", msg.Cid, "miner", m.To, "gas_fee_cap", m.GasFeeCap, "fee_cap_max", r.preFeeCapMax)
			return false, messageMethod, types.NewInt(0), nil
		}

		if tipset.Blocks()[0].ParentBaseFee.GreaterThan(r.preFeeCapMax) {
			log.Debugw("skipping high base fee message", "method", messageMethod, "cid", msg.Cid, "miner", m.To, "basefee", tipset.Blocks()[0].ParentBaseFee, "fee_cap_max", r.preFeeCapMax)
			return false, messageMethod, types.NewInt(0), nil
		}

		var precommitInfo minertypes.SectorPreCommitInfo
		if err := precommitInfo.UnmarshalCBOR(bytes.NewBuffer(m.Params)); err != nil {
			log.Warnw("failed to decode precommit params", "err", err, "method", messageMethod, "cid", msg.Cid, "miner", m.To)
			return false, messageMethod, types.NewInt(0), nil
		}

		collateral, err := r.api.StateMinerInitialPledgeCollateral(ctx, m.To, precommitInfo, tipset.Key())
		if err != nil {
			log.Warnw("failed to calculate initial pledge collateral", "err", err, "method", messageMethod, "cid", msg.Cid, "miner", m.To, "sector_number", precommitInfo.SectorNumber)
			return false, messageMethod, types.NewInt(0), nil
		}

		refundValue = collateral
		if r.refundPercent > 0 {
			refundValue = types.BigMul(types.BigDiv(refundValue, types.NewInt(100)), types.NewInt(uint64(r.refundPercent)))
		}
	default:
		return false, messageMethod, types.NewInt(0), nil
	}

	return true, messageMethod, refundValue, nil
}

func (r *refunder) ProcessTipset(ctx context.Context, tipset *types.TipSet, refunds *MinersRefund) (*MinersRefund, error) {
	cids := tipset.Cids()
	if len(cids) == 0 {
		log.Errorw("no cids in tipset", "height", tipset.Height(), "key", tipset.Key())
		return nil, fmt.Errorf("no cids in tipset")
	}

	msgs, err := r.api.ChainGetParentMessages(ctx, cids[0])
	if err != nil {
		log.Errorw("failed to get tipset parent messages", "err", err, "height", tipset.Height(), "key", tipset.Key())
		return nil, nil
	}

	recps, err := r.api.ChainGetParentReceipts(ctx, cids[0])
	if err != nil {
		log.Errorw("failed to get tipset parent receipts", "err", err, "height", tipset.Height(), "key", tipset.Key())
		return nil, nil
	}

	if len(msgs) != len(recps) {
		log.Errorw("message length does not match receipts length", "height", tipset.Height(), "key", tipset.Key(), "messages", len(msgs), "receipts", len(recps))
		return nil, nil
	}

	tipsetRefunds := NewMinersRefund()
	for i, msg := range msgs {
		refundValue := types.NewInt(0)
		m := msg.Message

		a, err := r.api.StateGetActor(ctx, m.To, tipset.Key())
		if err != nil {
			log.Warnw("failed to look up state actor", "height", tipset.Height(), "key", tipset.Key(), "actor", m.To)
			continue
		}

		var messageMethod string
		var processed bool

		if m.To == market.Address {
			processed, messageMethod, refundValue, err = r.processTipsetStorageMarketActor(ctx, tipset, msg, recps[i])
		}

		if lbuiltin.IsStorageMinerActor(a.Code) {
			processed, messageMethod, refundValue, err = r.processTipsetStorageMinerActor(ctx, tipset, msg, recps[i])
		}

		if err != nil {
			log.Errorw("error while processing message", "cid", msg.Cid)
			continue
		}
		if !processed {
			continue
		}

		log.Debugw(
			"processing message",
			"method", messageMethod,
			"cid", msg.Cid,
			"from", m.From,
			"to", m.To,
			"value", m.Value,
			"gas_fee_cap", m.GasFeeCap,
			"gas_premium", m.GasPremium,
			"gas_used", recps[i].GasUsed,
			"refund", refundValue,
			"refund_fil", big.Div(refundValue, big.NewIntUnsigned(build.FilecoinPrecision)).Int64(),
		)

		refunds.Track(m.From, refundValue)
		tipsetRefunds.Track(m.From, refundValue)
	}

	log.Infow(
		"tipset stats",
		"height", tipset.Height(),
		"key", tipset.Key(),
		"total_refunds", tipsetRefunds.TotalRefunds(),
		"total_refunds_fil", big.Div(tipsetRefunds.TotalRefunds(), big.NewIntUnsigned(build.FilecoinPrecision)).Int64(),
		"messages_processed", tipsetRefunds.Count(),
	)

	return refunds, nil
}

func (r *refunder) Refund(ctx context.Context, name string, tipset *types.TipSet, refunds *MinersRefund, rounds int) error {
	if refunds.Count() == 0 {
		log.Debugw("no messages to refund in tipset", "height", tipset.Height(), "key", tipset.Key())
		return nil
	}

	var messages []*types.Message
	refundSum := types.NewInt(0)

	for _, maddr := range refunds.Miners() {
		refundValue := refunds.GetRefund(maddr)

		// We want to try and ensure these messages get mined quickly
		gasPremium, err := r.api.GasEstimateGasPremium(ctx, 0, r.wallet, 0, tipset.Key())
		if err != nil {
			log.Warnw("failed to estimate gas premium", "err", err, "height", tipset.Height(), "key", tipset.Key())
			continue
		}

		msg := &types.Message{
			Value: refundValue,
			From:  r.wallet,
			To:    maddr,

			GasPremium: gasPremium,
		}

		refundSum = types.BigAdd(refundSum, msg.Value)
		messages = append(messages, msg)
	}

	balance, err := r.api.WalletBalance(ctx, r.wallet)
	if err != nil {
		log.Errorw("failed to get wallet balance", "err", err, "height", tipset.Height(), "key", tipset.Key())
		return xerrors.Errorf("failed to get wallet balance :%w", err)
	}

	// Calculate the minimum balance as the total refund we need to issue plus 5% to cover fees
	minBalance := types.BigAdd(refundSum, types.BigDiv(refundSum, types.NewInt(500)))
	if balance.LessThan(minBalance) {
		log.Errorw("not sufficient funds to cover refunds", "balance", balance, "refund_sum", refundSum, "minimum_required", minBalance)
		return xerrors.Errorf("wallet does not have enough balance to cover refund")
	}

	failures := 0
	refundSum.SetUint64(0)
	for _, msg := range messages {
		if !r.dryRun {
			if _, err = r.api.MpoolPushMessage(ctx, msg, nil); err != nil {
				log.Errorw("failed to MpoolPushMessage", "err", err, "msg", msg)
				failures = failures + 1
				continue
			}
		}

		refundSum = types.BigAdd(refundSum, msg.Value)
	}

	log.Infow(
		name,
		"tipsets_processed", rounds,
		"height", tipset.Height(),
		"key", tipset.Key(),
		"messages_sent", len(messages)-failures,
		"refund_sum", refundSum,
		"refund_sum_fil", big.Div(refundSum, big.NewIntUnsigned(build.FilecoinPrecision)).Int64(),
		"messages_failures", failures,
		"messages_processed", refunds.Count(),
	)
	return nil
}

type Repo struct {
	lastHeight              abi.ChainEpoch
	lastMinerRecoveryHeight abi.ChainEpoch
	path                    string
	blocklist               []address.Address
}

func NewRepo(path string) (*Repo, error) {
	path, err := homedir.Expand(path)
	if err != nil {
		return nil, err
	}

	return &Repo{
		lastHeight:              0,
		lastMinerRecoveryHeight: 0,
		path:                    path,
	}, nil
}

func (r *Repo) exists() (bool, error) {
	_, err := os.Stat(r.path)
	notexist := os.IsNotExist(err)
	if notexist {
		err = nil
	}
	return !notexist, err

}

func (r *Repo) init() error {
	exist, err := r.exists()
	if err != nil {
		return err
	}
	if exist {
		return nil
	}

	err = os.Mkdir(r.path, 0755) //nolint: gosec
	if err != nil && !os.IsExist(err) {
		return err
	}

	return nil
}

func (r *Repo) Open() error {
	if err := r.init(); err != nil {
		return err
	}

	if err := r.loadHeight(); err != nil {
		return err
	}

	if err := r.loadMinerRecoveryHeight(); err != nil {
		return err
	}

	if err := r.loadBlockList(); err != nil {
		return err
	}

	return nil
}

func loadChainEpoch(fn string) (abi.ChainEpoch, error) {
	f, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0644)
	if err != nil {
		return 0, err
	}
	defer func() {
		err = f.Close()
	}()

	raw, err := io.ReadAll(f)
	if err != nil {
		return 0, err
	}

	height, err := strconv.Atoi(string(bytes.TrimSpace(raw)))
	if err != nil {
		return 0, err
	}

	return abi.ChainEpoch(height), nil
}

func (r *Repo) loadBlockList() error {
	var err error
	fpath := filepath.Join(r.path, "blocklist")
	f, err := os.OpenFile(fpath, os.O_RDWR|os.O_CREATE, 0644)
	if err != nil {
		return err
	}
	defer func() {
		err = f.Close()
	}()

	blocklist := []address.Address{}
	input := bufio.NewReader(f)
	for {
		stra, errR := input.ReadString('\n')
		stra = strings.TrimSpace(stra)

		if len(stra) == 0 {
			if errR == io.EOF {
				break
			}
			continue
		}

		addr, err := address.NewFromString(stra)
		if err != nil {
			return err
		}

		blocklist = append(blocklist, addr)

		if errR != nil && errR != io.EOF {
			return err
		}

		if errR == io.EOF {
			break
		}
	}

	r.blocklist = blocklist

	return nil
}

func (r *Repo) loadHeight() error {
	var err error
	r.lastHeight, err = loadChainEpoch(filepath.Join(r.path, "height"))
	return err
}

func (r *Repo) loadMinerRecoveryHeight() error {
	var err error
	r.lastMinerRecoveryHeight, err = loadChainEpoch(filepath.Join(r.path, "miner_recovery_height"))
	return err
}

func (r *Repo) Blocklist() []address.Address {
	return r.blocklist
}

func (r *Repo) Height() abi.ChainEpoch {
	return r.lastHeight
}

func (r *Repo) MinerRecoveryHeight() abi.ChainEpoch {
	return r.lastMinerRecoveryHeight
}

func (r *Repo) SetHeight(last abi.ChainEpoch) (err error) {
	r.lastHeight = last
	var f *os.File
	f, err = os.OpenFile(filepath.Join(r.path, "height"), os.O_RDWR, 0644)
	if err != nil {
		return
	}

	defer func() {
		err = f.Close()
	}()

	if _, err = fmt.Fprintf(f, "%d", r.lastHeight); err != nil {
		return
	}

	return
}

func (r *Repo) SetMinerRecoveryHeight(last abi.ChainEpoch) (err error) {
	r.lastMinerRecoveryHeight = last
	var f *os.File
	f, err = os.OpenFile(filepath.Join(r.path, "miner_recovery_height"), os.O_RDWR, 0644)
	if err != nil {
		return
	}

	defer func() {
		err = f.Close()
	}()

	if _, err = fmt.Fprintf(f, "%d", r.lastMinerRecoveryHeight); err != nil {
		return
	}

	return
}
