// Copyright (c) 2020-2024 Blockwatch Data Inc.
// Author: alex@blockwatch.cc

package series

import (
	"encoding/json"
	"fmt"
	"strconv"
	"time"

	"blockwatch.cc/packdb/pack"
	"blockwatch.cc/packdb/util"
	"blockwatch.cc/tzindex/etl/model"
	"blockwatch.cc/tzindex/rpc"
	"blockwatch.cc/tzindex/server"
)

var (
	// all series field names as list
	supplySeriesNames util.StringList
)

func init() {
	fields, err := pack.Fields(&model.Supply{})
	if err != nil {
		log.Fatalf("supply field type error: %v\n", err)
	}
	// strip row_id (first field)
	supplySeriesNames = util.StringList(fields.Aliases()[1:])
	supplySeriesNames.AddUnique("count")
}

// configurable marshalling helper
type SupplySeries struct {
	model.Supply
	bucketTime time.Time

	columns util.StringList // cond. cols & order when brief
	params  *rpc.Params     // blockchain amount conversion
	verbose bool            // cond. marshal
	null    bool
}

var _ SeriesBucket = (*SupplySeries)(nil)

func (s *SupplySeries) Init(params *rpc.Params, columns []string, verbose bool) {
	s.params = params
	s.columns = columns
	s.verbose = verbose
}

func (s *SupplySeries) IsEmpty() bool {
	return s.Supply.RowId == 0
}

func (s *SupplySeries) Add(m SeriesModel) {
	o := m.(*model.Supply)
	s.Supply = *o
}

func (s *SupplySeries) Reset() {
	s.bucketTime = time.Time{}
	s.null = false
}

func (s *SupplySeries) Null(ts time.Time) SeriesBucket {
	s.Reset()
	s.bucketTime = ts
	s.null = true
	return s
}

func (s *SupplySeries) Zero(ts time.Time) SeriesBucket {
	s.Reset()
	s.bucketTime = ts
	return s
}

func (s *SupplySeries) SetTime(ts time.Time) SeriesBucket {
	s.bucketTime = ts
	return s
}

func (s *SupplySeries) Time() time.Time {
	return s.bucketTime
}

func (s *SupplySeries) Clone() SeriesBucket {
	c := &SupplySeries{
		Supply:     s.Supply,
		bucketTime: s.bucketTime,
	}
	c.columns = s.columns
	c.params = s.params
	c.verbose = s.verbose
	c.null = s.null
	return c
}

func (s *SupplySeries) Interpolate(m SeriesBucket, ts time.Time) SeriesBucket {
	// unused, sematically there is one supply table entry per block
	return s
}

func (s *SupplySeries) MarshalJSON() ([]byte, error) {
	if s.verbose {
		return s.MarshalJSONVerbose()
	} else {
		return s.MarshalJSONBrief()
	}
}

func (s *SupplySeries) MarshalJSONVerbose() ([]byte, error) {
	supply := struct {
		Height              int64     `json:"height"`
		Cycle               int64     `json:"cycle"`
		Timestamp           time.Time `json:"time"`
		Count               int       `json:"count"`
		Total               float64   `json:"total"`
		Activated           float64   `json:"activated"`
		Unclaimed           float64   `json:"unclaimed"`
		Circulating         float64   `json:"circulating"`
		Liquid              float64   `json:"liquid"`
		Delegated           float64   `json:"delegated"`
		Staking             float64   `json:"staking"`
		Unstaking           float64   `json:"unstaking"`
		Shielded            float64   `json:"shielded"`
		ActiveStake         float64   `json:"active_stake"`
		ActiveDelegated     float64   `json:"active_delegated"`
		ActiveStaking       float64   `json:"active_staking"`
		InactiveDelegated   float64   `json:"inactive_delegated"`
		InactiveStaking     float64   `json:"inactive_staking"`
		Minted              float64   `json:"minted"`
		MintedBaking        float64   `json:"minted_baking"`
		MintedEndorsing     float64   `json:"minted_endorsing"`
		MintedSeeding       float64   `json:"minted_seeding"`
		MintedAirdrop       float64   `json:"minted_airdrop"`
		MintedSubsidy       float64   `json:"minted_subsidy"`
		Burned              float64   `json:"burned"`
		BurnedDoubleBaking  float64   `json:"burned_double_baking"`
		BurnedDoubleEndorse float64   `json:"burned_double_endorse"`
		BurnedOrigination   float64   `json:"burned_origination"`
		BurnedAllocation    float64   `json:"burned_allocation"`
		BurnedStorage       float64   `json:"burned_storage"`
		BurnedExplicit      float64   `json:"burned_explicit"`
		BurnedSeedMiss      float64   `json:"burned_seed_miss"`
		BurnedOffline       float64   `json:"burned_offline"`
		BurnedRollup        float64   `json:"burned_rollup"`
		Frozen              float64   `json:"frozen"`
		FrozenDeposits      float64   `json:"frozen_deposits"`
		FrozenRewards       float64   `json:"frozen_rewards"`
		FrozenFees          float64   `json:"frozen_fees"`
		FrozenBonds         float64   `json:"frozen_bonds"`
		FrozenStake         float64   `json:"frozen_stake"`
		FrozenBakerStake    float64   `json:"frozen_baker_stake"`
		FrozenStakerStake   float64   `json:"frozen_staker_stake"`
	}{
		Height:              s.Height,
		Cycle:               s.Cycle,
		Timestamp:           s.bucketTime,
		Count:               1,
		Total:               s.params.ConvertValue(s.Total),
		Activated:           s.params.ConvertValue(s.Activated),
		Unclaimed:           s.params.ConvertValue(s.Unclaimed),
		Circulating:         s.params.ConvertValue(s.Circulating),
		Liquid:              s.params.ConvertValue(s.Liquid),
		Delegated:           s.params.ConvertValue(s.Delegated),
		Staking:             s.params.ConvertValue(s.Staking),
		Unstaking:           s.params.ConvertValue(s.Unstaking),
		Shielded:            s.params.ConvertValue(s.Shielded),
		ActiveStake:         s.params.ConvertValue(s.ActiveStake),
		ActiveDelegated:     s.params.ConvertValue(s.ActiveDelegated),
		ActiveStaking:       s.params.ConvertValue(s.ActiveStaking),
		InactiveDelegated:   s.params.ConvertValue(s.InactiveDelegated),
		InactiveStaking:     s.params.ConvertValue(s.InactiveStaking),
		Minted:              s.params.ConvertValue(s.Minted),
		MintedBaking:        s.params.ConvertValue(s.MintedBaking),
		MintedEndorsing:     s.params.ConvertValue(s.MintedEndorsing),
		MintedSeeding:       s.params.ConvertValue(s.MintedSeeding),
		MintedAirdrop:       s.params.ConvertValue(s.MintedAirdrop),
		MintedSubsidy:       s.params.ConvertValue(s.MintedSubsidy),
		Burned:              s.params.ConvertValue(s.Burned),
		BurnedDoubleBaking:  s.params.ConvertValue(s.BurnedDoubleBaking),
		BurnedDoubleEndorse: s.params.ConvertValue(s.BurnedDoubleEndorse),
		BurnedOrigination:   s.params.ConvertValue(s.BurnedOrigination),
		BurnedAllocation:    s.params.ConvertValue(s.BurnedAllocation),
		BurnedStorage:       s.params.ConvertValue(s.BurnedStorage),
		BurnedExplicit:      s.params.ConvertValue(s.BurnedExplicit),
		BurnedSeedMiss:      s.params.ConvertValue(s.BurnedSeedMiss),
		BurnedOffline:       s.params.ConvertValue(s.BurnedOffline),
		BurnedRollup:        s.params.ConvertValue(s.BurnedRollup),
		Frozen:              s.params.ConvertValue(s.Frozen),
		FrozenDeposits:      s.params.ConvertValue(s.FrozenDeposits),
		FrozenRewards:       s.params.ConvertValue(s.FrozenRewards),
		FrozenFees:          s.params.ConvertValue(s.FrozenFees),
		FrozenBonds:         s.params.ConvertValue(s.FrozenBonds),
		FrozenStake:         s.params.ConvertValue(s.FrozenStake),
		FrozenBakerStake:    s.params.ConvertValue(s.FrozenBakerStake),
		FrozenStakerStake:   s.params.ConvertValue(s.FrozenStakerStake),
	}
	return json.Marshal(supply)
}

func (s *SupplySeries) MarshalJSONBrief() ([]byte, error) {
	dec := s.params.Decimals
	buf := make([]byte, 0, 2048)
	buf = append(buf, '[')
	for i, v := range s.columns {
		if s.null {
			switch v {
			case "time":
				buf = strconv.AppendInt(buf, util.UnixMilliNonZero(s.bucketTime), 10)
			default:
				buf = append(buf, null...)
			}
		} else {
			switch v {
			case "height":
				buf = strconv.AppendInt(buf, s.Height, 10)
			case "cycle":
				buf = strconv.AppendInt(buf, s.Cycle, 10)
			case "time":
				buf = strconv.AppendInt(buf, util.UnixMilliNonZero(s.bucketTime), 10)
			case "count":
				buf = strconv.AppendInt(buf, 1, 10)
			case "total":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Total), 'f', dec, 64)
			case "activated":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Activated), 'f', dec, 64)
			case "unclaimed":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Unclaimed), 'f', dec, 64)
			case "circulating":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Circulating), 'f', dec, 64)
			case "liquid":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Liquid), 'f', dec, 64)
			case "delegated":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Delegated), 'f', dec, 64)
			case "staking":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Staking), 'f', dec, 64)
			case "unstaking":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Unstaking), 'f', dec, 64)
			case "shielded":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Shielded), 'f', dec, 64)
			case "active_stake":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.ActiveStake), 'f', dec, 64)
			case "active_delegated":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.ActiveDelegated), 'f', dec, 64)
			case "active_staking":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.ActiveStaking), 'f', dec, 64)
			case "inactive_delegated":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.InactiveDelegated), 'f', dec, 64)
			case "inactive_staking":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.InactiveStaking), 'f', dec, 64)
			case "minted":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Minted), 'f', dec, 64)
			case "minted_baking":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.MintedBaking), 'f', dec, 64)
			case "minted_endorsing":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.MintedEndorsing), 'f', dec, 64)
			case "minted_seeding":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.MintedSeeding), 'f', dec, 64)
			case "minted_airdrop":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.MintedAirdrop), 'f', dec, 64)
			case "minted_subsidy":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.MintedSubsidy), 'f', dec, 64)
			case "burned":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Burned), 'f', dec, 64)
			case "burned_double_baking":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.BurnedDoubleBaking), 'f', dec, 64)
			case "burned_double_endorse":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.BurnedDoubleEndorse), 'f', dec, 64)
			case "burned_origination":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.BurnedOrigination), 'f', dec, 64)
			case "burned_allocation":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.BurnedAllocation), 'f', dec, 64)
			case "burned_storage":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.BurnedStorage), 'f', dec, 64)
			case "burned_explict":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.BurnedExplicit), 'f', dec, 64)
			case "burned_seed_miss":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.BurnedSeedMiss), 'f', dec, 64)
			case "burned_offline":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.BurnedOffline), 'f', dec, 64)
			case "burned_rollup":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.BurnedRollup), 'f', dec, 64)
			case "frozen":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.Frozen), 'f', dec, 64)
			case "frozen_deposits":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.FrozenDeposits), 'f', dec, 64)
			case "frozen_rewards":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.FrozenRewards), 'f', dec, 64)
			case "frozen_fees":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.FrozenFees), 'f', dec, 64)
			case "frozen_bonds":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.FrozenBonds), 'f', dec, 64)
			case "frozen_stake":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.FrozenStake), 'f', dec, 64)
			case "frozen_baker_stake":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.FrozenBakerStake), 'f', dec, 64)
			case "frozen_staker_stake":
				buf = strconv.AppendFloat(buf, s.params.ConvertValue(s.FrozenStakerStake), 'f', dec, 64)
			default:
				continue
			}
		}
		if i < len(s.columns)-1 {
			buf = append(buf, ',')
		}
	}
	buf = append(buf, ']')
	return buf, nil
}

func (s *SupplySeries) MarshalCSV() ([]string, error) {
	dec := s.params.Decimals
	res := make([]string, len(s.columns))
	for i, v := range s.columns {
		if s.null {
			switch v {
			case "time":
				res[i] = strconv.Quote(s.bucketTime.Format(time.RFC3339))
			default:
				continue
			}
		}
		switch v {
		case "height":
			res[i] = strconv.FormatInt(s.Height, 10)
		case "cycle":
			res[i] = strconv.FormatInt(s.Cycle, 10)
		case "time":
			res[i] = strconv.Quote(s.bucketTime.Format(time.RFC3339))
		case "count":
			res[i] = strconv.FormatInt(1, 10)
		case "total":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Total), 'f', dec, 64)
		case "activated":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Activated), 'f', dec, 64)
		case "unclaimed":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Unclaimed), 'f', dec, 64)
		case "circulating":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Circulating), 'f', dec, 64)
		case "liquid":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Liquid), 'f', dec, 64)
		case "delegated":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Delegated), 'f', dec, 64)
		case "staking":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Staking), 'f', dec, 64)
		case "unstaking":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Unstaking), 'f', dec, 64)
		case "shielded":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Shielded), 'f', dec, 64)
		case "active_stake":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.ActiveStake), 'f', dec, 64)
		case "active_delegated":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.ActiveDelegated), 'f', dec, 64)
		case "active_staking":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.ActiveStaking), 'f', dec, 64)
		case "inactive_delegated":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.InactiveDelegated), 'f', dec, 64)
		case "inactive_staking":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.InactiveStaking), 'f', dec, 64)
		case "minted":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Minted), 'f', dec, 64)
		case "minted_baking":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.MintedBaking), 'f', dec, 64)
		case "minted_endorsing":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.MintedEndorsing), 'f', dec, 64)
		case "minted_seeding":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.MintedSeeding), 'f', dec, 64)
		case "minted_airdrop":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.MintedAirdrop), 'f', dec, 64)
		case "minted_subsidy":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.MintedSubsidy), 'f', dec, 64)
		case "burned":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Burned), 'f', dec, 64)
		case "burned_double_baking":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.BurnedDoubleBaking), 'f', dec, 64)
		case "burned_double_endorse":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.BurnedDoubleEndorse), 'f', dec, 64)
		case "burned_origination":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.BurnedOrigination), 'f', dec, 64)
		case "burned_allocation":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.BurnedAllocation), 'f', dec, 64)
		case "burned_storage":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.BurnedStorage), 'f', dec, 64)
		case "burned_explict":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.BurnedExplicit), 'f', dec, 64)
		case "burned_seed_miss":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.BurnedSeedMiss), 'f', dec, 64)
		case "burned_offline":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.BurnedOffline), 'f', dec, 64)
		case "burned_rollup":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.BurnedRollup), 'f', dec, 64)
		case "frozen":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.Frozen), 'f', dec, 64)
		case "frozen_deposits":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.FrozenDeposits), 'f', dec, 64)
		case "frozen_rewards":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.FrozenRewards), 'f', dec, 64)
		case "frozen_fees":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.FrozenFees), 'f', dec, 64)
		case "frozen_bonds":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.FrozenBonds), 'f', dec, 64)
		case "frozen_stake":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.FrozenStake), 'f', dec, 64)
		case "frozen_baker_stake":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.FrozenBakerStake), 'f', dec, 64)
		case "frozen_staker_stake":
			res[i] = strconv.FormatFloat(s.params.ConvertValue(s.FrozenStakerStake), 'f', dec, 64)
		default:
			continue
		}
	}
	return res, nil
}

func (s *SupplySeries) BuildQuery(ctx *server.Context, args *SeriesRequest) pack.Query {
	// access table
	table, err := ctx.Indexer.Table(args.Series)
	if err != nil {
		panic(server.ENotFound(server.EC_RESOURCE_NOTFOUND, fmt.Sprintf("cannot access table '%s'", args.Series), err))
	}

	// time is auto-added from parser
	if len(args.Columns) == 1 {
		// use all series columns
		args.Columns = supplySeriesNames
	}
	// resolve short column names
	srcNames := make([]string, 0, len(args.Columns))
	for _, v := range args.Columns {
		// ignore count column
		if v == "count" {
			continue
		}
		// ignore non-series columns
		if !supplySeriesNames.Contains(v) {
			panic(server.EBadRequest(server.EC_PARAM_INVALID, fmt.Sprintf("invalid time-series column '%s'", v), nil))
		}
		srcNames = append(srcNames, v)
	}

	// build table query, no dynamic filter conditions
	return pack.NewQuery(ctx.RequestID).
		WithTable(table).
		WithFields(srcNames...).
		WithOrder(args.Order).
		AndRange("time", args.From.Time(), args.To.Time())
}
