package storagesc

import (
	"fmt"
	"math"
	"math/rand"
	"strconv"
	"sync"

	"0chain.net/chaincore/transaction"
	"github.com/0chain/common/core/currency"

	cstate "0chain.net/chaincore/chain/state"
	"0chain.net/core/common"
	"0chain.net/core/encryption"
	"0chain.net/core/maths"
	"0chain.net/smartcontract/dbs/event"
	"0chain.net/smartcontract/stakepool/spenum"
	"github.com/0chain/common/core/logging"
	"github.com/0chain/common/core/util"
	"go.uber.org/zap"
)

type BlobberBlockRewardsInput struct {
	Round int64 `json:"round,omitempty"`
}

func (ssc *StorageSmartContract) blobberBlockRewards(t *transaction.Transaction, input []byte, balances cstate.StateContextI) (ferr error) {
	logging.Logger.Info("blobberBlockRewards started",
		zap.Int64("round", balances.GetBlock().Round),
		zap.String("block_hash", balances.GetBlock().Hash))

	var (
		totalQStake float64
		weight      []float64
		totalWeight float64
	)

	conf, err := ssc.getConfig(balances, true)
	if err != nil {
		return common.NewError("blobber_block_rewards_failed",
			"cannot get smart contract configurations: "+err.Error())
	}

	if balances.GetBlock().Round%conf.BlockReward.TriggerPeriod != 0 {
		return common.NewError("blobber_block_rewards_failed",
			"block reward trigger period not reached")
	}

	if conf.BlockReward.BlockReward == 0 || conf.BlockReward.TriggerPeriod == 0 {
		return nil
	}

	bbr, err := getBlockReward(conf.BlockReward.BlockReward, balances.GetBlock().Round,
		conf.BlockReward.BlockRewardChangePeriod, conf.BlockReward.BlockRewardChangeRatio)
	if err != nil {
		return common.NewError("blobber_block_rewards_failed",
			"cannot get block rewards: "+err.Error())
	}

	activePassedBlobberRewardPart, err := getActivePassedBlobberRewardsPartitions(balances, conf.BlockReward.TriggerPeriod)
	if err != nil {
		return common.NewError("blobber_block_rewards_failed",
			"cannot get all blobbers list: "+err.Error())
	}

	defer func() {
		//we are returning error here and can't shadow it with return of next calls
		if ferr != nil {
			return
		}
		logging.Logger.Info("blobber_block_rewards : cleaning older partition",
			zap.Any("round", BlobberRewardKey(GetPreviousRewardRound(balances.GetBlock().Round, conf.BlockReward.TriggerPeriod))))

		_, ferr = balances.DeleteTrieNode(BlobberRewardKey(GetPreviousRewardRound(balances.GetBlock().Round, conf.BlockReward.TriggerPeriod)))
		if ferr != nil {
			logging.Logger.Error("blobber_block_rewards_failed",
				zap.String("deleting blobber reward node", ferr.Error()))
		}
	}()

	hashString := encryption.Hash(balances.GetTransaction().Hash + balances.GetBlock().PrevHash)
	var randomSeed int64
	randomSeed, err = strconv.ParseInt(hashString[0:15], 16, 64)
	if err != nil {
		return common.NewError("blobber_block_rewards_failed",
			"error in creating seed"+err.Error())
	}
	r := rand.New(rand.NewSource(randomSeed))

	var blobberRewards []BlobberRewardNode
	if err := activePassedBlobberRewardPart.GetRandomItems(balances, r, &blobberRewards); err != nil {
		logging.Logger.Info("blobber_block_rewards_failed",
			zap.String("getting random partition", err.Error()))
		if err != util.ErrValueNotPresent {
			return err
		}
		return nil
	}

	type spResp struct {
		index int
		sp    *stakePool
	}

	var wg sync.WaitGroup
	errC := make(chan error, len(blobberRewards))
	spC := make(chan spResp, len(blobberRewards))
	for i, br := range blobberRewards {
		wg.Add(1)
		go func(b BlobberRewardNode, i int) {
			defer wg.Done()
			if sp, err := ssc.getStakePool(spenum.Blobber, b.ID, balances); err != nil {
				errC <- err
			} else {
				spC <- spResp{
					index: i,
					sp:    sp,
				}
			}
		}(br, i)
	}
	wg.Wait()
	close(spC)

	select {
	case err := <-errC:
		return err
	default:
	}

	stakePools := make([]*stakePool, len(blobberRewards))
	before := make([]currency.Coin, len(blobberRewards))
	for resp := range spC {
		stakePools[resp.index] = resp.sp
		stake, err := resp.sp.stake()
		if err != nil {
			return err
		}
		before[resp.index] = stake
	}

	qualifyingBlobberIds := make([]string, len(blobberRewards))

	for i, br := range blobberRewards {
		sp := stakePools[i]

		staked, err := sp.stake()
		if err != nil {
			return err
		}

		stake := float64(staked)

		gamma := maths.GetGamma(
			conf.BlockReward.Gamma.A,
			conf.BlockReward.Gamma.B,
			conf.BlockReward.Gamma.Alpha,
			br.TotalData,
			br.DataRead,
		)

		zeta := maths.GetZeta(
			conf.BlockReward.Zeta.I,
			conf.BlockReward.Zeta.K,
			conf.BlockReward.Zeta.Mu,
			float64(br.WritePrice),
			float64(br.ReadPrice),
		)

		qualifyingBlobberIds[i] = br.ID
		totalQStake += stake
		blobberWeight := ((gamma * zeta) + 1) * stake * float64(br.SuccessChallenges)
		weight = append(weight, blobberWeight)
		totalWeight += blobberWeight
	}

	if totalWeight == 0 {
		totalWeight = 1
	}

	rewardBal := bbr
	for i, qsp := range stakePools {
		if rewardBal == 0 {
			break
		}
		weightRatio := weight[i] / totalWeight
		if weightRatio > 0 && weightRatio <= 1 {
			fBBR, err := bbr.Float64()
			if err != nil {
				return err
			}
			reward, err := currency.Float64ToCoin(fBBR * weightRatio)
			if err != nil {
				return err
			}
			if reward > rewardBal {
				reward = rewardBal
				rewardBal = 0
			} else {
				rewardBal -= reward
			}
			logging.Logger.Info("blobber_block_rewards_pass",
				zap.Uint64("reward", uint64(reward)),
				zap.String("blobber id", qualifyingBlobberIds[i]),
				zap.Int64("round", balances.GetBlock().Round),
				zap.String("block_hash", balances.GetBlock().Hash))

			if err := qsp.DistributeRewards(
				reward, qualifyingBlobberIds[i], spenum.Blobber, spenum.BlockRewardBlobber, balances); err != nil {
				return common.NewError("blobber_block_rewards_failed", "minting capacity reward"+err.Error())
			}

		} else {
			logging.Logger.Error("blobber_bloc_rewards - error in weight ratio",
				zap.Any("stake pool", qsp))
			return common.NewError("blobber_block_rewards_failed", "weight ratio out of bound")
		}
	}

	if rewardBal > 0 {
		rShare, rl, err := currency.DistributeCoin(rewardBal, int64(len(stakePools)))
		if err != nil {
			return err
		}

		if rShare > 0 {
			for i := range stakePools {
				if err := stakePools[i].DistributeRewards(rShare, qualifyingBlobberIds[i], spenum.Blobber, spenum.BlockRewardBlobber, balances); err != nil {
					return common.NewError("blobber_block_rewards_failed", "minting capacity reward"+err.Error())
				}
			}
		}

		if rl > 0 {
			for i := 0; i < int(rl); i++ {
				if err := stakePools[i].DistributeRewards(1, qualifyingBlobberIds[i], spenum.Blobber, spenum.BlockRewardBlobber, balances); err != nil {
					return common.NewError("blobber_block_rewards_failed", "minting capacity reward"+err.Error())
				}
			}
		}

	}

	for i, qsp := range stakePools {
		if err = qsp.Save(spenum.Blobber, qualifyingBlobberIds[i], balances); err != nil {
			return common.NewError("blobber_block_rewards_failed",
				"saving stake pool: "+err.Error())
		}
		staked, err := qsp.stake()
		if err != nil {
			return common.NewError("blobber_block_rewards_failed",
				"getting stake pool stake: "+err.Error())
		}

		bid := qualifyingBlobberIds[i]
		tag, data := event.NewUpdateBlobberTotalStakeEvent(bid, staked)
		balances.EmitEvent(event.TypeStats, tag, bid, data)
	}

	return nil
}

func getBlockReward(
	br currency.Coin,
	currentRound,
	brChangePeriod int64,
	brChangeRatio float64) (currency.Coin, error) {
	if brChangeRatio <= 0 || brChangeRatio >= 1 {
		return 0, fmt.Errorf("unexpected block reward change ratio: %f", brChangeRatio)
	}
	changeBalance := 1 - brChangeRatio
	changePeriods := currentRound / brChangePeriod

	factor := math.Pow(changeBalance, float64(changePeriods))
	return currency.MultFloat64(br, factor)
}

func GetCurrentRewardRound(currentRound, period int64) int64 {
	if period > 0 {
		extra := currentRound % period
		return currentRound - extra
	}
	return 0
}

func GetPreviousRewardRound(currentRound, period int64) int64 {
	crr := GetCurrentRewardRound(currentRound, period)
	if crr >= period {
		return crr - period
	} else {
		return 0
	}
}
