// Logic related to expvar handling: reporting live stats such as
// session and topic counts, memory usage etc.
// The stats updates happen in a separate go routine to avoid
// locking on main logic routines.

package main

import (
	"encoding/json"
	"expvar"
	"net/http"
	"runtime"
	"sort"
	"time"

	"github.com/tinode/chat/server/logs"
	"github.com/tinode/chat/server/store"
)

// A simple implementation of histogram expvar.Var.
// `Bounds` specifies the histogram buckets as follows (length = len(bounds)):
//
//	(-inf, Bounds[i]) for i = 0
//	[Bounds[i-1], Bounds[i]) for 0 < i < length
//	[Bounds[i-1], +inf) for i = length
type histogram struct {
	Count          int64     `json:"count"`
	Sum            float64   `json:"sum"`
	CountPerBucket []int64   `json:"count_per_bucket"`
	Bounds         []float64 `json:"bounds"`
}

func (h *histogram) addSample(v float64) {
	h.Count++
	h.Sum += v
	idx := sort.SearchFloat64s(h.Bounds, v)
	h.CountPerBucket[idx]++
}

func (h *histogram) String() string {
	if r, err := json.Marshal(h); err == nil {
		return string(r)
	}
	return ""
}

type varUpdate struct {
	// Name of the variable to update
	varname string
	// Value to publish (int, float, etc.)
	value interface{}
	// Treat the count as an increment as opposite to the final value.
	inc bool
}

// Initialize stats reporting through expvar.
func statsInit(mux *http.ServeMux, path string) {
	if path == "" || path == "-" {
		return
	}

	mux.Handle(path, expvar.Handler())
	globals.statsUpdate = make(chan *varUpdate, 1024)

	start := time.Now()
	expvar.Publish("Uptime", expvar.Func(func() interface{} {
		return time.Since(start).Seconds()
	}))
	expvar.Publish("NumGoroutines", expvar.Func(func() interface{} {
		return runtime.NumGoroutine()
	}))

	go statsUpdater()

	logs.Info.Printf("stats: variables exposed at '%s'", path)
}

func statsRegisterDbStats() {
	if f := store.Store.DbStats(); f != nil {
		expvar.Publish("DbStats", expvar.Func(f))
	}
}

// Register integer variable. Don't check for initialization.
func statsRegisterInt(name string) {
	expvar.Publish(name, new(expvar.Int))
}

// Register histogram variable. `bounds` specifies histogram buckets/bins
// (see comment next to the `histogram` struct definition).
func statsRegisterHistogram(name string, bounds []float64) {
	numBuckets := len(bounds) + 1
	expvar.Publish(name, &histogram{
		CountPerBucket: make([]int64, numBuckets),
		Bounds:         bounds,
	})
}

// Async publish int variable.
func statsSet(name string, val int64) {
	if globals.statsUpdate != nil {
		select {
		case globals.statsUpdate <- &varUpdate{name, val, false}:
		default:
		}
	}
}

// Async publish an increment (decrement) to int variable.
func statsInc(name string, val int) {
	if globals.statsUpdate != nil {
		select {
		case globals.statsUpdate <- &varUpdate{name, int64(val), true}:
		default:
		}
	}
}

// Async publish a value (add a sample) to a histogram variable.
func statsAddHistSample(name string, val float64) {
	if globals.statsUpdate != nil {
		select {
		case globals.statsUpdate <- &varUpdate{varname: name, value: val}:
		default:
		}
	}
}

// Stop publishing stats.
func statsShutdown() {
	if globals.statsUpdate != nil {
		globals.statsUpdate <- nil
	}
}

// The go routine which actually publishes stats updates.
func statsUpdater() {
	for upd := range globals.statsUpdate {
		if upd == nil {
			globals.statsUpdate = nil
			// Dont' care to close the channel.
			break
		}

		// Handle var update
		if ev := expvar.Get(upd.varname); ev != nil {
			switch v := ev.(type) {
			case *expvar.Int:
				count := upd.value.(int64)
				if upd.inc {
					v.Add(count)
				} else {
					v.Set(count)
				}
			case *histogram:
				val := upd.value.(float64)
				v.addSample(val)
			default:
				logs.Err.Panicf("stats: unsupported expvar type %T", ev)
			}
		} else {
			panic("stats: update to unknown variable " + upd.varname)
		}
	}

	logs.Info.Println("stats: shutdown")
}
