package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"sort"
	"strconv"

	"github.com/ipfs/go-cid"
	cbor "github.com/ipfs/go-ipld-cbor"
	"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-state-types/abi"
	"github.com/filecoin-project/go-state-types/big"

	"github.com/filecoin-project/lotus/chain/actors/adt"
	"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/actors/builtin/multisig"
	"github.com/filecoin-project/lotus/chain/actors/builtin/power"
	"github.com/filecoin-project/lotus/chain/consensus/filcns"
	"github.com/filecoin-project/lotus/chain/state"
	"github.com/filecoin-project/lotus/chain/store"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/lotus/node/repo"
)

type Option uint64

const (
	Approve Option = 49
	Reject  Option = 50
)

type Vote struct {
	ID            uint64
	OptionID      Option
	SignerAddress address.Address
}

type msigVote struct {
	Multisig     msigBriefInfo
	ApproveCount uint64
	RejectCount  uint64
}

// https://filpoll.io/poll/16
// snapshot height: 2162760
// state root: bafy2bzacebdnzh43hw66bmvguk65wiwr5ssaejlq44fpdei2ysfh3eefpdlqs
var fip36PollCmd = &cli.Command{
	Name:      "fip36poll",
	Usage:     "Process the FIP0036 FilPoll result",
	ArgsUsage: "[state root, votes]",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "repo",
			Value: "~/.lotus",
		},
	},
	Subcommands: []*cli.Command{
		finalResultCmd,
	},
}

var finalResultCmd = &cli.Command{
	Name:      "results",
	Usage:     "get poll results",
	ArgsUsage: "[state root] [height] [votes json]",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "repo",
			Value: "~/.lotus",
		},
	},

	Action: func(cctx *cli.Context) error {
		if cctx.NArg() != 3 {
			return xerrors.New("filpoll0036 results [state root] [height] [votes.json]")
		}

		ctx := context.TODO()
		if !cctx.Args().Present() {
			return fmt.Errorf("must pass state root")
		}

		sroot, err := cid.Decode(cctx.Args().First())
		if err != nil {
			return fmt.Errorf("failed to parse input: %w", err)
		}

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

		lkrepo, err := fsrepo.Lock(repo.FullNode)
		if err != nil {
			return err
		}

		defer lkrepo.Close() //nolint:errcheck

		bs, err := lkrepo.Blockstore(ctx, repo.UniversalBlockstore)
		if err != nil {
			return fmt.Errorf("failed to open blockstore: %w", err)
		}

		defer func() {
			if c, ok := bs.(io.Closer); ok {
				if err := c.Close(); err != nil {
					log.Warnf("failed to close blockstore: %s", err)
				}
			}
		}()

		mds, err := lkrepo.Datastore(context.Background(), "/metadata")
		if err != nil {
			return err
		}

		cs := store.NewChainStore(bs, bs, mds, filcns.Weight, nil)
		defer cs.Close() //nolint:errcheck

		cst := cbor.NewCborStore(bs)
		store := adt.WrapStore(ctx, cst)

		st, err := state.LoadStateTree(cst, sroot)
		if err != nil {
			return err
		}

		height, err := strconv.Atoi(cctx.Args().Get(1))
		if err != nil {
			return err
		}

		//get all the votes' signer ID address && their vote
		vj, err := homedir.Expand(cctx.Args().Get(2))
		if err != nil {
			return xerrors.Errorf("fail to get votes json")
		}
		votes, err := getVotesMap(vj)
		if err != nil {
			return xerrors.Errorf("failed to get voters: %w\n", err)
		}

		type minerBriefInfo struct {
			rawBytePower abi.StoragePower
			dealPower    abi.StoragePower
			balance      abi.TokenAmount
		}

		// power actor
		pa, err := st.GetActor(power.Address)
		if err != nil {
			return xerrors.Errorf("failed to get power actor: %w\n", err)
		}

		powerState, err := power.Load(store, pa)
		if err != nil {
			return xerrors.Errorf("failed to get power state: %w\n", err)
		}

		//market actor
		ma, err := st.GetActor(market.Address)
		if err != nil {
			return xerrors.Errorf("fail to get market actor: %w\n", err)
		}

		marketState, err := market.Load(store, ma)
		if err != nil {
			return xerrors.Errorf("fail to load market state: %w\n", err)
		}

		lookupId := func(addr address.Address) address.Address {
			ret, err := st.LookupID(addr)
			if err != nil {
				panic(err)
			}

			return ret
		}

		// we need to build several pieces of information, as we traverse the state tree:
		// a map of accounts to every msig that they are a signer of
		accountsToMultisigs := make(map[address.Address][]address.Address)
		// a map of multisigs to some info about them for quick lookup
		msigActorsInfo := make(map[address.Address]msigBriefInfo)
		// a map of actors (accounts+multisigs) to every miner that they are an owner of
		ownerMap := make(map[address.Address][]address.Address)
		// a map of accounts to every miner that they are a worker of
		workerMap := make(map[address.Address][]address.Address)
		// a map of miners to some info about them for quick lookup
		minerActorsInfo := make(map[address.Address]minerBriefInfo)
		// a map of client addresses to deal data stored in proposals
		clientToDealStorage := make(map[address.Address]abi.StoragePower)

		fmt.Println("iterating over all actors")
		count := 0
		err = st.ForEach(func(addr address.Address, act *types.Actor) error {
			if count%200000 == 0 {
				fmt.Println("processed ", count, " actors building maps")
			}
			count++
			if builtin.IsMultisigActor(act.Code) {
				ms, err := multisig.Load(store, act)
				if err != nil {
					return fmt.Errorf("load msig failed %v", err)

				}

				// TODO: Confirm that these are always ID addresses
				signers, err := ms.Signers()
				if err != nil {
					return xerrors.Errorf("fail to get msig signers: %w", err)
				}
				for _, s := range signers {
					signerId := lookupId(s)
					accountsToMultisigs[signerId] = append(accountsToMultisigs[signerId], addr)
				}

				locked, err := ms.LockedBalance(abi.ChainEpoch(height))
				if err != nil {
					return xerrors.Errorf("failed to compute locked multisig balance: %w", err)
				}

				threshold, _ := ms.Threshold()
				info := msigBriefInfo{
					ID:        addr,
					Signer:    signers,
					Balance:   big.Max(big.Zero(), types.BigSub(act.Balance, locked)),
					Threshold: threshold,
				}
				msigActorsInfo[addr] = info
			}

			if builtin.IsStorageMinerActor(act.Code) {
				m, err := miner.Load(store, act)
				if err != nil {
					return xerrors.Errorf("fail to load miner actor: %w", err)
				}

				info, err := m.Info()
				if err != nil {
					return xerrors.Errorf("fail to get miner info: %w\n", err)
				}

				ownerId := lookupId(info.Owner)
				ownerMap[ownerId] = append(ownerMap[ownerId], addr)

				workerId := lookupId(info.Worker)
				workerMap[workerId] = append(workerMap[workerId], addr)

				lockedFunds, err := m.LockedFunds()
				if err != nil {
					return err
				}

				bal := big.Sub(act.Balance, lockedFunds.TotalLockedFunds())
				bal = big.Max(big.Zero(), bal)

				pow, ok, err := powerState.MinerPower(addr)
				if err != nil {
					return err
				}

				if !ok {
					pow.RawBytePower = big.Zero()
				}

				minerActorsInfo[addr] = minerBriefInfo{
					rawBytePower: pow.RawBytePower,
					// gets added up outside this loop
					dealPower: big.Zero(),
					balance:   bal,
				}
			}

			return nil
		})

		if err != nil {
			return err
		}

		fmt.Println("iterating over proposals")
		dealProposals, err := marketState.Proposals()
		if err != nil {
			return err
		}

		dealStates, err := marketState.States()
		if err != nil {
			return err
		}

		if err := dealProposals.ForEach(func(dealID abi.DealID, d market.DealProposal) error {

			dealState, ok, err := dealStates.Get(dealID)
			if err != nil {
				return err
			}
			if !ok || dealState.SectorStartEpoch == -1 {
				// effectively a continue
				return nil
			}

			clientId := lookupId(d.Client)
			if cd, found := clientToDealStorage[clientId]; found {
				clientToDealStorage[clientId] = big.Add(cd, big.NewInt(int64(d.PieceSize)))
			} else {
				clientToDealStorage[clientId] = big.NewInt(int64(d.PieceSize))
			}

			providerId := lookupId(d.Provider)
			mai, found := minerActorsInfo[providerId]

			if !found {
				return xerrors.Errorf("didn't find miner %s", providerId)
			}

			mai.dealPower = big.Add(mai.dealPower, big.NewInt(int64(d.PieceSize)))
			minerActorsInfo[providerId] = mai
			return nil
		}); err != nil {
			return xerrors.Errorf("fail to get deals")
		}

		// now tabulate votes

		approveBalance := abi.NewTokenAmount(0)
		rejectionBalance := abi.NewTokenAmount(0)
		clientApproveBytes := big.Zero()
		clientRejectBytes := big.Zero()
		msigPendingVotes := make(map[address.Address]msigVote) //map[msig ID]msigVote
		msigVotes := make(map[address.Address]Option)
		minerVotes := make(map[address.Address]Option)
		fmt.Println("counting account and multisig votes")
		for _, vote := range votes {
			signerId, err := st.LookupID(vote.SignerAddress)
			if err != nil {
				fmt.Println("voter ", vote.SignerAddress, " not found in state tree, skipping")
				continue
			}

			//process votes for regular accounts
			accountActor, err := st.GetActor(signerId)
			if err != nil {
				return xerrors.Errorf("fail to get account account for signer: %w\n", err)
			}

			clientBytes, ok := clientToDealStorage[signerId]
			if !ok {
				clientBytes = big.Zero()
			}

			if vote.OptionID == Approve {
				approveBalance = types.BigAdd(approveBalance, accountActor.Balance)
				clientApproveBytes = big.Add(clientApproveBytes, clientBytes)
			} else {
				rejectionBalance = types.BigAdd(rejectionBalance, accountActor.Balance)
				clientRejectBytes = big.Add(clientRejectBytes, clientBytes)
			}

			if minerInfos, found := ownerMap[signerId]; found {
				for _, minerInfo := range minerInfos {
					minerVotes[minerInfo] = vote.OptionID
				}
			}
			if minerInfos, found := workerMap[signerId]; found {
				for _, minerInfo := range minerInfos {
					if _, ok := minerVotes[minerInfo]; !ok {
						minerVotes[minerInfo] = vote.OptionID
					}
				}
			}

			//process msigs
			// There is a possibility that enough signers have voted for BOTH options in the poll to be above the threshold
			// Because we are iterating over votes in order they arrived, the first option to go over the threshold will win
			// This is in line with onchain behaviour (consider a case where signers are competing to withdraw all the funds
			// in an msig into 2 different accounts)
			if mss, found := accountsToMultisigs[signerId]; found {
				for _, ms := range mss { //get all the msig signer has
					if _, ok := msigVotes[ms]; ok {
						// msig has already voted, skip
						continue
					}
					if mpv, found := msigPendingVotes[ms]; found { //other signers of the multisig have voted, yet the threshold has not met
						if vote.OptionID == Approve {
							if mpv.ApproveCount+1 == mpv.Multisig.Threshold { //met threshold
								approveBalance = types.BigAdd(approveBalance, mpv.Multisig.Balance)
								delete(msigPendingVotes, ms) //threshold, can skip later signer votes
								msigVotes[ms] = vote.OptionID

							} else {
								mpv.ApproveCount++
								msigPendingVotes[ms] = mpv
							}
						} else {
							if mpv.RejectCount+1 == mpv.Multisig.Threshold { //met threshold
								rejectionBalance = types.BigAdd(rejectionBalance, mpv.Multisig.Balance)
								delete(msigPendingVotes, ms) //threshold, can skip later signer votes
								msigVotes[ms] = vote.OptionID

							} else {
								mpv.RejectCount++
								msigPendingVotes[ms] = mpv
							}
						}
					} else { //first vote received from one of the signers of the msig
						msi, ok := msigActorsInfo[ms]
						if !ok {
							return xerrors.Errorf("didn't find msig %s in msig map", ms)
						}

						if msi.Threshold == 1 { //met threshold with this signer's single vote
							if vote.OptionID == Approve {
								approveBalance = types.BigAdd(approveBalance, msi.Balance)
								msigVotes[ms] = Approve

							} else {
								rejectionBalance = types.BigAdd(rejectionBalance, msi.Balance)
								msigVotes[ms] = Reject
							}
						} else { //threshold not met, add to pending vote
							if vote.OptionID == Approve {
								msigPendingVotes[ms] = msigVote{
									Multisig:     msi,
									ApproveCount: 1,
								}
							} else {
								msigPendingVotes[ms] = msigVote{
									Multisig:    msi,
									RejectCount: 1,
								}
							}
						}
					}
				}
			}
		}

		for s, v := range msigVotes {
			if minerInfos, found := ownerMap[s]; found {
				for _, minerInfo := range minerInfos {
					minerVotes[minerInfo] = v
				}
			}
			if minerInfos, found := workerMap[s]; found {
				for _, minerInfo := range minerInfos {
					if _, ok := minerVotes[minerInfo]; !ok {
						minerVotes[minerInfo] = v
					}
				}
			}
		}

		approveRBP := big.Zero()
		approveDealPower := big.Zero()
		rejectionRBP := big.Zero()
		rejectionDealPower := big.Zero()
		fmt.Println("adding up miner votes")
		for minerAddr, vote := range minerVotes {
			mbi, ok := minerActorsInfo[minerAddr]
			if !ok {
				return xerrors.Errorf("failed to find miner info for %s", minerAddr)
			}

			if vote == Approve {
				approveBalance = big.Add(approveBalance, mbi.balance)
				approveRBP = big.Add(approveRBP, mbi.rawBytePower)
				approveDealPower = big.Add(approveDealPower, mbi.dealPower)
			} else {
				rejectionBalance = big.Add(rejectionBalance, mbi.balance)
				rejectionRBP = big.Add(rejectionRBP, mbi.rawBytePower)
				rejectionDealPower = big.Add(rejectionDealPower, mbi.dealPower)
			}
		}

		fmt.Println("Total acceptance token: ", approveBalance)
		fmt.Println("Total rejection token: ", rejectionBalance)

		fmt.Println("Total acceptance SP deal power: ", approveDealPower)
		fmt.Println("Total rejection SP deal power: ", rejectionDealPower)

		fmt.Println("Total acceptance SP rb power: ", approveRBP)
		fmt.Println("Total rejection SP rb power: ", rejectionRBP)

		fmt.Println("Total acceptance Client rb power: ", clientApproveBytes)
		fmt.Println("Total rejection Client rb power: ", clientRejectBytes)

		fmt.Println("\n\nFinal results **drumroll**")
		if rejectionBalance.GreaterThanEqual(big.Mul(approveBalance, big.NewInt(3))) {
			fmt.Println("token holders VETO FIP-0036!")
		} else if approveBalance.LessThanEqual(rejectionBalance) {
			fmt.Println("token holders REJECT FIP-0036")
		} else {
			fmt.Println("token holders ACCEPT FIP-0036")
		}

		if rejectionDealPower.GreaterThanEqual(big.Mul(approveDealPower, big.NewInt(3))) {
			fmt.Println("SPs by deal data stored VETO FIP-0036!")
		} else if approveDealPower.LessThanEqual(rejectionDealPower) {
			fmt.Println("SPs by deal data stored REJECT FIP-0036")
		} else {
			fmt.Println("SPs by deal data stored ACCEPT FIP-0036")
		}

		if rejectionRBP.GreaterThanEqual(big.Mul(approveRBP, big.NewInt(3))) {
			fmt.Println("SPs by total raw byte power VETO FIP-0036!")
		} else if approveRBP.LessThanEqual(rejectionRBP) {
			fmt.Println("SPs by total raw byte power REJECT FIP-0036")
		} else {
			fmt.Println("SPs by total raw byte power ACCEPT FIP-0036")
		}

		if clientRejectBytes.GreaterThanEqual(big.Mul(clientApproveBytes, big.NewInt(3))) {
			fmt.Println("Storage Clients VETO FIP-0036!")
		} else if clientApproveBytes.LessThanEqual(clientRejectBytes) {
			fmt.Println("Storage Clients REJECT FIP-0036")
		} else {
			fmt.Println("Storage Clients ACCEPT FIP-0036")
		}

		return nil
	},
}

// Returns voted sorted by votes from earliest to latest
func getVotesMap(file string) ([]Vote, error) {
	var votes []Vote
	vb, err := os.ReadFile(file)
	if err != nil {
		return nil, xerrors.Errorf("read vote: %w", err)
	}

	if err := json.Unmarshal(vb, &votes); err != nil {
		return nil, xerrors.Errorf("unmarshal vote: %w", err)
	}

	sort.SliceStable(votes, func(i, j int) bool {
		return votes[i].ID < votes[j].ID
	})

	return votes, nil
}
