package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"math"
	"math/rand"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"sync"
	"sync/atomic"
	"time"

	"github.com/VictoriaMetrics/VictoriaMetrics/lib/buildinfo"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/envflag"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)

var (
	addr    = flag.String("addr", "stdout", "HTTP address to push the generated logs to; if it is set to stdout, then logs are generated to stdout")
	workers = flag.Int("workers", 1, "The number of workers to use to push logs to -addr")

	start         = newTimeFlag("start", "-1d", "Generated logs start from this time; see https://docs.victoriametrics.com/#timestamp-formats")
	end           = newTimeFlag("end", "0s", "Generated logs end at this time; see https://docs.victoriametrics.com/#timestamp-formats")
	activeStreams = flag.Int("activeStreams", 100, "The number of active log streams to generate; see https://docs.victoriametrics.com/victorialogs/keyconcepts/#stream-fields")
	totalStreams  = flag.Int("totalStreams", 0, "The number of total log streams; if -totalStreams > -activeStreams, then some active streams are substituted with new streams "+
		"during data generation")
	logsPerStream     = flag.Int64("logsPerStream", 1_000, "The number of log entries to generate per each log stream. Log entries are evenly distributed between -start and -end")
	constFieldsPerLog = flag.Int("constFieldsPerLog", 3, "The number of fields with constaint values to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	varFieldsPerLog = flag.Int("varFieldsPerLog", 1, "The number of fields with variable values to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	dictFieldsPerLog = flag.Int("dictFieldsPerLog", 2, "The number of fields with up to 8 different values to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	u8FieldsPerLog = flag.Int("u8FieldsPerLog", 1, "The number of fields with uint8 values to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	u16FieldsPerLog = flag.Int("u16FieldsPerLog", 1, "The number of fields with uint16 values to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	u32FieldsPerLog = flag.Int("u32FieldsPerLog", 1, "The number of fields with uint32 values to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	u64FieldsPerLog = flag.Int("u64FieldsPerLog", 1, "The number of fields with uint64 values to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	i64FieldsPerLog = flag.Int("i64FieldsPerLog", 1, "The number of fields with int64 values to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	floatFieldsPerLog = flag.Int("floatFieldsPerLog", 1, "The number of fields with float64 values to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	ipFieldsPerLog = flag.Int("ipFieldsPerLog", 1, "The number of fields with IPv4 values to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	timestampFieldsPerLog = flag.Int("timestampFieldsPerLog", 1, "The number of fields with ISO8601 timestamps per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")
	jsonFieldsPerLog = flag.Int("jsonFieldsPerLog", 1, "The number of JSON fields to generate per each log entry; "+
		"see https://docs.victoriametrics.com/victorialogs/keyconcepts/#data-model")

	statInterval = flag.Duration("statInterval", 10*time.Second, "The interval between publishing the stats")
)

func main() {
	// Write flags and help message to stdout, since it is easier to grep or pipe.
	flag.CommandLine.SetOutput(os.Stdout)
	envflag.Parse()
	buildinfo.Init()
	logger.Init()

	var remoteWriteURL *url.URL
	if *addr != "stdout" {
		urlParsed, err := url.Parse(*addr)
		if err != nil {
			logger.Fatalf("cannot parse -addr=%q: %s", *addr, err)
		}
		qs, err := url.ParseQuery(urlParsed.RawQuery)
		if err != nil {
			logger.Fatalf("cannot parse query string in -addr=%q: %w", *addr, err)
		}
		qs.Set("_stream_fields", "host,worker_id")
		urlParsed.RawQuery = qs.Encode()
		remoteWriteURL = urlParsed
	}

	if start.nsec >= end.nsec {
		logger.Fatalf("-start=%s must be smaller than -end=%s", start, end)
	}
	if *activeStreams <= 0 {
		logger.Fatalf("-activeStreams must be bigger than 0; got %d", *activeStreams)
	}
	if *logsPerStream <= 0 {
		logger.Fatalf("-logsPerStream must be bigger than 0; got %d", *logsPerStream)
	}
	if *totalStreams < *activeStreams {
		*totalStreams = *activeStreams
	}

	cfg := &workerConfig{
		url:           remoteWriteURL,
		activeStreams: *activeStreams,
		totalStreams:  *totalStreams,
	}

	// divide total and active streams among workers
	if *workers <= 0 {
		logger.Fatalf("-workers must be bigger than 0; got %d", *workers)
	}
	if *workers > *activeStreams {
		logger.Fatalf("-workers=%d cannot exceed -activeStreams=%d", *workers, *activeStreams)
	}
	cfg.activeStreams /= *workers
	cfg.totalStreams /= *workers

	logger.Infof("start -workers=%d workers for ingesting -logsPerStream=%d log entries per each -totalStreams=%d (-activeStreams=%d) on a time range -start=%s, -end=%s to -addr=%s",
		*workers, *logsPerStream, *totalStreams, *activeStreams, toRFC3339(start.nsec), toRFC3339(end.nsec), *addr)

	startTime := time.Now()
	var wg sync.WaitGroup
	for i := 0; i < *workers; i++ {
		wg.Add(1)
		go func(workerID int) {
			defer wg.Done()
			generateAndPushLogs(cfg, workerID)
		}(i)
	}

	go func() {
		prevEntries := uint64(0)
		prevBytes := uint64(0)
		ticker := time.NewTicker(*statInterval)
		for range ticker.C {
			currEntries := logEntriesCount.Load()
			deltaEntries := currEntries - prevEntries
			rateEntries := float64(deltaEntries) / statInterval.Seconds()

			currBytes := bytesGenerated.Load()
			deltaBytes := currBytes - prevBytes
			rateBytes := float64(deltaBytes) / statInterval.Seconds()
			logger.Infof("generated %dK log entries (%dK total) at %.0fK entries/sec, %dMB (%dMB total) at %.0fMB/sec",
				deltaEntries/1e3, currEntries/1e3, rateEntries/1e3, deltaBytes/1e6, currBytes/1e6, rateBytes/1e6)

			prevEntries = currEntries
			prevBytes = currBytes
		}
	}()

	wg.Wait()

	dSecs := time.Since(startTime).Seconds()
	currEntries := logEntriesCount.Load()
	currBytes := bytesGenerated.Load()
	rateEntries := float64(currEntries) / dSecs
	rateBytes := float64(currBytes) / dSecs
	logger.Infof("ingested %dK log entries (%dMB) in %.3f seconds; avg ingestion rate: %.0fK entries/sec, %.0fMB/sec", currEntries/1e3, currBytes/1e6, dSecs, rateEntries/1e3, rateBytes/1e6)
}

var logEntriesCount atomic.Uint64

var bytesGenerated atomic.Uint64

type workerConfig struct {
	url           *url.URL
	activeStreams int
	totalStreams  int
}

type statWriter struct {
	w io.Writer
}

func (sw *statWriter) Write(p []byte) (int, error) {
	bytesGenerated.Add(uint64(len(p)))
	return sw.w.Write(p)
}

func generateAndPushLogs(cfg *workerConfig, workerID int) {
	pr, pw := io.Pipe()
	sw := &statWriter{
		w: pw,
	}
	bw := bufio.NewWriter(sw)
	doneCh := make(chan struct{})
	go func() {
		generateLogs(bw, workerID, cfg.activeStreams, cfg.totalStreams)
		_ = bw.Flush()
		_ = pw.Close()
		close(doneCh)
	}()

	if cfg.url == nil {
		_, err := io.Copy(os.Stdout, pr)
		if err != nil {
			logger.Fatalf("unexpected error when writing logs to stdout: %s", err)
		}
		return
	}

	req, err := http.NewRequest("POST", cfg.url.String(), pr)
	if err != nil {
		logger.Fatalf("cannot create request to %q: %s", cfg.url, err)
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		logger.Fatalf("cannot perform request to %q: %s", cfg.url, err)
	}
	if resp.StatusCode/100 != 2 {
		logger.Fatalf("unexpected status code got from %q: %d; want 2xx", cfg.url, err)
	}

	// Wait until all the generateLogs goroutine is finished.
	<-doneCh
}

func generateLogs(bw *bufio.Writer, workerID, activeStreams, totalStreams int) {
	streamLifetime := int64(float64(end.nsec-start.nsec) * (float64(activeStreams) / float64(totalStreams)))
	streamStep := int64(float64(end.nsec-start.nsec) / float64(totalStreams-activeStreams+1))
	step := streamLifetime / (*logsPerStream - 1)

	currNsec := start.nsec
	for currNsec < end.nsec {
		firstStreamID := int((currNsec - start.nsec) / streamStep)
		generateLogsAtTimestamp(bw, workerID, currNsec, firstStreamID, activeStreams)
		currNsec += step
	}
}

var runID = toUUID(rand.Uint64(), rand.Uint64())

func generateLogsAtTimestamp(bw *bufio.Writer, workerID int, ts int64, firstStreamID, activeStreams int) {
	streamID := firstStreamID
	timeStr := toRFC3339(ts)
	for i := 0; i < activeStreams; i++ {
		ip := toIPv4(rand.Uint32())
		uuid := toUUID(rand.Uint64(), rand.Uint64())
		fmt.Fprintf(bw, `{"_time":"%s","_msg":"message for the stream %d and worker %d; ip=%s; uuid=%s; u64=%d","host":"host_%d","worker_id":"%d"`,
			timeStr, streamID, workerID, ip, uuid, rand.Uint64(), streamID, workerID)
		fmt.Fprintf(bw, `,"run_id":"%s"`, runID)
		for j := 0; j < *constFieldsPerLog; j++ {
			fmt.Fprintf(bw, `,"const_%d":"some value %d %d"`, j, j, streamID)
		}
		for j := 0; j < *varFieldsPerLog; j++ {
			fmt.Fprintf(bw, `,"var_%d":"some value %d %d"`, j, j, rand.Uint64())
		}
		for j := 0; j < *dictFieldsPerLog; j++ {
			fmt.Fprintf(bw, `,"dict_%d":"%s"`, j, dictValues[rand.Intn(len(dictValues))])
		}
		for j := 0; j < *u8FieldsPerLog; j++ {
			fmt.Fprintf(bw, `,"u8_%d":"%d"`, j, uint8(rand.Uint32()))
		}
		for j := 0; j < *u16FieldsPerLog; j++ {
			fmt.Fprintf(bw, `,"u16_%d":"%d"`, j, uint16(rand.Uint32()))
		}
		for j := 0; j < *u32FieldsPerLog; j++ {
			fmt.Fprintf(bw, `,"u32_%d":"%d"`, j, rand.Uint32())
		}
		for j := 0; j < *u64FieldsPerLog; j++ {
			fmt.Fprintf(bw, `,"u64_%d":"%d"`, j, rand.Uint64())
		}
		for j := 0; j < *i64FieldsPerLog; j++ {
			fmt.Fprintf(bw, `,"i64_%d":"%d"`, j, int64(rand.Uint64()))
		}
		for j := 0; j < *floatFieldsPerLog; j++ {
			fmt.Fprintf(bw, `,"float_%d":"%v"`, j, math.Round(10_000*rand.Float64())/1000)
		}
		for j := 0; j < *ipFieldsPerLog; j++ {
			ip := toIPv4(rand.Uint32())
			fmt.Fprintf(bw, `,"ip_%d":"%s"`, j, ip)
		}
		for j := 0; j < *timestampFieldsPerLog; j++ {
			timestamp := toISO8601(int64(rand.Uint64()))
			fmt.Fprintf(bw, `,"timestamp_%d":"%s"`, j, timestamp)
		}
		for j := 0; j < *jsonFieldsPerLog; j++ {
			fmt.Fprintf(bw, `,"json_%d":"{\"foo\":\"bar_%d\",\"baz\":{\"a\":[\"x\",\"y\"]},\"f3\":NaN,\"f4\":%d}"`, j, rand.Intn(10), rand.Intn(100))
		}
		fmt.Fprintf(bw, "}\n")

		logEntriesCount.Add(1)
		streamID++
	}
}

var dictValues = []string{
	"debug",
	"info",
	"warn",
	"error",
	"fatal",
	"ERROR",
	"FATAL",
	"INFO",
}

func newTimeFlag(name, defaultValue, description string) *timeFlag {
	var tf timeFlag
	if err := tf.Set(defaultValue); err != nil {
		logger.Panicf("invalid defaultValue=%q for flag %q: %w", defaultValue, name, err)
	}
	flag.Var(&tf, name, description)
	return &tf
}

type timeFlag struct {
	s    string
	nsec int64
}

func (tf *timeFlag) Set(s string) error {
	msec, err := promutils.ParseTimeMsec(s)
	if err != nil {
		return fmt.Errorf("cannot parse time from %q: %w", s, err)
	}
	tf.s = s
	tf.nsec = msec * 1e6
	return nil
}

func (tf *timeFlag) String() string {
	return tf.s
}

func toRFC3339(nsec int64) string {
	return time.Unix(0, nsec).UTC().Format(time.RFC3339Nano)
}

func toISO8601(nsec int64) string {
	return time.Unix(0, nsec).UTC().Format("2006-01-02T15:04:05.000Z")
}

func toIPv4(n uint32) string {
	dst := make([]byte, 0, len("255.255.255.255"))
	dst = marshalUint64(dst, uint64(n>>24))
	dst = append(dst, '.')
	dst = marshalUint64(dst, uint64((n>>16)&0xff))
	dst = append(dst, '.')
	dst = marshalUint64(dst, uint64((n>>8)&0xff))
	dst = append(dst, '.')
	dst = marshalUint64(dst, uint64(n&0xff))
	return string(dst)
}

func toUUID(a, b uint64) string {
	return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", a&(1<<32-1), (a>>32)&(1<<16-1), (a >> 48), b&(1<<16-1), b>>16)
}

// marshalUint64 appends string representation of n to dst and returns the result.
func marshalUint64(dst []byte, n uint64) []byte {
	return strconv.AppendUint(dst, n, 10)
}
