package main

import (
	"encoding/json"
	"sort"
	"sync"
	"time"
)

type SeriesWindow struct {
	Step     time.Duration
	Capacity int
}

type SeriesPoint struct {
	TsUnix int64             `json:"ts_unix"`
	Counts map[string]uint64 `json:"counts"`
}

type Snapshot struct {
	CollectorID   string            `json:"collector_id"`
	Hostname      string            `json:"hostname"`
	Endpoint      string            `json:"endpoint"`
	NowUnix       int64             `json:"now_unix"`
	Counters      map[string]uint64 `json:"counters"`
	Levels        map[string]uint64 `json:"levels"`
	HTTP          map[string]uint64 `json:"http"`
	TopErrors     []KV              `json:"top_errors"`
	Series        []SeriesPoint     `json:"series"`
	UptimeSeconds uint64            `json:"uptime_seconds"`
}

type KV struct {
	Key   string `json:"key"`
	Value uint64 `json:"value"`
}

type Metrics struct {
	mu sync.RWMutex

	start time.Time

	counters map[string]uint64
	levels   map[string]uint64
	http     map[string]uint64

	errKW map[string]uint64

	seriesCfg SeriesWindow
	series    []SeriesPoint
}

func NewMetrics(w SeriesWindow) *Metrics {
	if w.Step <= 0 {
		w.Step = 5 * time.Second
	}
	if w.Capacity <= 0 {
		w.Capacity = 240
	}
	return &Metrics{
		start:     time.Now(),
		counters:  map[string]uint64{},
		levels:    map[string]uint64{},
		http:      map[string]uint64{},
		errKW:     map[string]uint64{},
		seriesCfg: w,
		series:    make([]SeriesPoint, 0, w.Capacity),
	}
}

func (m *Metrics) IncLines(n uint64)         { m.incCounter("lines_total", n) }
func (m *Metrics) IncParseErrors(n uint64)   { m.incCounter("parse_errors_total", n) }
func (m *Metrics) IncDroppedLines(n uint64)  { m.incCounter("dropped_lines_total", n) }
func (m *Metrics) IncRotate(n uint64)        { m.incCounter("rotate_events_total", n) }
func (m *Metrics) IncBatchesSent(n uint64)   { m.incCounter("batches_sent_total", n) }
func (m *Metrics) IncBatchesFailed(n uint64) { m.incCounter("batches_failed_total", n) }
func (m *Metrics) IncBatchDropped(n uint64)  { m.incCounter("batches_dropped_total", n) }

func (m *Metrics) incCounter(key string, n uint64) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.counters[key] += n
}

func (m *Metrics) IncLevel(level string, n uint64) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.levels[level] += n
}

func (m *Metrics) IncHTTP(code int, n uint64) {
	if code <= 0 {
		return
	}
	bucket := httpBucket(code)
	m.mu.Lock()
	defer m.mu.Unlock()
	m.http[bucket] += n
}

func (m *Metrics) IncErrKW(kws []string) {
	if len(kws) == 0 {
		return
	}
	m.mu.Lock()
	defer m.mu.Unlock()
	for _, k := range kws {
		m.errKW[k]++
	}
}

func (m *Metrics) PushSeriesPoint(ts time.Time, byLevel map[string]uint64) {
	m.mu.Lock()
	defer m.mu.Unlock()
	pt := SeriesPoint{
		TsUnix: ts.Unix(),
		Counts: copyMap(byLevel),
	}
	if len(m.series) == m.seriesCfg.Capacity {
		copy(m.series, m.series[1:])
		m.series[len(m.series)-1] = pt
	} else {
		m.series = append(m.series, pt)
	}
}

func (m *Metrics) Snapshot(collectorID, hostname, endpoint string, topN int) Snapshot {
	m.mu.RLock()
	defer m.mu.RUnlock()

	levels := copyMap(m.levels)
	httpm := copyMap(m.http)
	counters := copyMap(m.counters)

	top := topK(m.errKW, topN)

	seriesCopy := make([]SeriesPoint, len(m.series))
	copy(seriesCopy, m.series)

	return Snapshot{
		CollectorID:   collectorID,
		Hostname:      hostname,
		Endpoint:      endpoint,
		NowUnix:       time.Now().Unix(),
		Counters:      counters,
		Levels:        levels,
		HTTP:          httpm,
		TopErrors:     top,
		Series:        seriesCopy,
		UptimeSeconds: uint64(time.Since(m.start).Seconds()),
	}
}

func (m *Metrics) PromText(collectorID, hostname, endpoint string) string {
	snap := m.Snapshot(collectorID, hostname, endpoint, 8)
	type line struct {
		k string
		v uint64
	}
	var lines []line
	for k, v := range snap.Counters {
		lines = append(lines, line{k: k, v: v})
	}
	for k, v := range snap.Levels {
		lines = append(lines, line{k: "level_" + sanitizeKey(k) + "_total", v: v})
	}
	for k, v := range snap.HTTP {
		lines = append(lines, line{k: "http_" + sanitizeKey(k) + "_total", v: v})
	}
	sort.Slice(lines, func(i, j int) bool { return lines[i].k < lines[j].k })

	var b []byte
	add := func(s string) { b = append(b, []byte(s)...) }
	add("# concurrent-log-collector metrics\n")
	add("collector_uptime_seconds ")
	add(u64(snap.UptimeSeconds))
	add("\n")
	for _, ln := range lines {
		add(ln.k)
		add(" ")
		add(u64(ln.v))
		add("\n")
	}
	return string(b)
}

func (m *Metrics) JSONPreviewBatch(collectorID, hostname, endpoint string) []byte {
	snap := m.Snapshot(collectorID, hostname, endpoint, 8)
	// smaller "preview payload" (like what is POSTed)
	p := BatchPayload{
		CollectorID: collectorID,
		Hostname:    hostname,
		Endpoint:    endpoint,
		Timestamp:   time.Now().UTC().Format(time.RFC3339Nano),
		Levels:      snap.Levels,
		Counters:    snap.Counters,
		HTTP:        snap.HTTP,
		TopErrors:   snap.TopErrors,
	}
	j, _ := json.MarshalIndent(p, "", "  ")
	return j
}

func copyMap(in map[string]uint64) map[string]uint64 {
	out := make(map[string]uint64, len(in))
	for k, v := range in {
		out[k] = v
	}
	return out
}

func topK(m map[string]uint64, k int) []KV {
	if k <= 0 {
		return nil
	}
	type kv struct {
		k string
		v uint64
	}
	arr := make([]kv, 0, len(m))
	for kk, vv := range m {
		arr = append(arr, kv{k: kk, v: vv})
	}
	sort.Slice(arr, func(i, j int) bool {
		if arr[i].v == arr[j].v {
			return arr[i].k < arr[j].k
		}
		return arr[i].v > arr[j].v
	})
	if len(arr) > k {
		arr = arr[:k]
	}
	out := make([]KV, 0, len(arr))
	for _, it := range arr {
		out = append(out, KV{Key: it.k, Value: it.v})
	}
	return out
}

func httpBucket(code int) string {
	switch {
	case code >= 100 && code < 200:
		return "1xx"
	case code >= 200 && code < 300:
		return "2xx"
	case code >= 300 && code < 400:
		return "3xx"
	case code >= 400 && code < 500:
		return "4xx"
	case code >= 500 && code < 600:
		return "5xx"
	default:
		return "other"
	}
}

func sanitizeKey(s string) string {
	// allow a-zA-Z0-9_
	out := make([]byte, 0, len(s))
	for i := 0; i < len(s); i++ {
		c := s[i]
		if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') {
			out = append(out, c)
		} else {
			out = append(out, '_')
		}
	}
	return string(out)
}

func u64(n uint64) string {
	if n == 0 {
		return "0"
	}
	// manual conversion without fmt for speed
	var buf [32]byte
	i := len(buf)
	for n > 0 {
		i--
		buf[i] = byte('0' + (n % 10))
		n /= 10
	}
	return string(buf[i:])
}
