package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"math/rand"
	"os"
	"strings"
	"time"
)

// Use reservoir sampling to show a handful of example traces from each bucket.
// We can bucket on different dimensions: one measurement would be the number of
// nanoseconds that it took to return the response to the caller. Another (for
// hunting down bugs in the analysis tools) would be the time between the
// inbound call start and the end of the last "related" outbound call, to
// determine if the analysis code is too lenient in what it considers to be
// related.
//
// We can also start the dive by showing a few ribbons of the process's behavior
// over time. How many calls started (or finished?) in this "10ms" window, and
// how long did they take to complete? We can show this for inbound calls, or
// for various flavors of outbound calls. It should be easy to spot the effects
// of GC in this view, and to either examine it or avoid considering it.
//
// More per-process ribbons: How many inbound calls are alive right now? How
// many inbound calls are idle right now? How many calls do we have out to each
// dependency right now?
//
// Other ways of sorting through inbound calls from one process: How much time
// does the call's main goroutine spend idling with particular stacks? What is
// the set of stacks that the call's main goroutine has at times when it blocks?
//
// We also need better debugging tools for when fresh labels fail to apply to a
// goroutine, leaving outbound calls attached to the wrong cause. This might be
// via re-running analysis and generated detailed logs all changes (and rejected
// changes) for a particular goroutine.
//
// Why do some "server/http" regions end up as the child of the parentless
// transaction? RoutesD is like this.

//

// Debug the traces from graphql where child events are attributed to other
// calls

// Debug the traces from routesd where inbound calls aren't seen as roots

// Look through a set of call traces and group them by outbound call type and
// into buckets by duration

// For a call trace of interest, show the full causal path from the inbound call
// to all outbound calls, including details on which goroutines activate which
// others, and at which call stacks.

func main() {
	oneFile := flag.String("print-one", "", "Print a view of the single transaction described in the file")
	listFile := flag.String("summarize", "", "Print a summary of the transactions described in the file")
	flag.Parse()

	if *oneFile != "" {
		f, err := os.Open(*oneFile)
		if err != nil {
			log.Fatalf("err %q", err)
		}
		defer f.Close()

		var tx transactionReport
		err = json.NewDecoder(f).Decode(&tx)
		if err != nil {
			log.Fatalf("err %q", err)
		}

		fmt.Printf("%s", printOne(&tx))

		return
	}

	if *listFile != "" {
		f, err := os.Open(*listFile)
		if err != nil {
			log.Fatalf("err %q", err)
		}
		defer f.Close()

		var txs []*transactionReport
		err = json.NewDecoder(f).Decode(&txs)
		if err != nil {
			log.Fatalf("err %q", err)
		}

		fmt.Printf("%s", printSummary(txs))

		return
	}
}

func filter(txs []*transactionReport, fn func(tx *transactionReport) bool) []*transactionReport {
	var out []*transactionReport
	for _, tx := range txs {
		if fn(tx) {
			out = append(out, tx)
		}
	}
	return out
}

func printSummary(txs []*transactionReport) []byte {
	var buf bytes.Buffer

	var min time.Duration
	var max time.Duration

	for i, tx := range txs {
		start, end := time.Duration(tx.StartNanos), time.Duration(tx.StartNanos+tx.DurationNanos)
		if i == 0 {
			min = start
			max = end
		} else {
			if start < min {
				min = start
			}
			if end > max {
				max = end
			}
		}
	}

	interval := 10 * time.Millisecond
	offset := min.Truncate(interval)

	starts := make([]int, (max-offset).Truncate(interval)/interval+1)
	ends := make([]int, len(starts))

	for _, tx := range txs {
		starts[int((time.Duration(tx.StartNanos).Truncate(interval)-offset)/interval)]++
		ends[int((time.Duration(tx.StartNanos+tx.DurationNanos).Truncate(interval)-offset)/interval)]++
	}

	fmt.Fprintf(&buf, "hi %d %d %d %d %d\n", len(txs), min, max, max-min, len(starts))

	alive := 0
	for i := range starts {
		alive += starts[i]
		fmt.Fprintf(&buf, "%3dms %3d %3d %3d\n",
			(time.Duration(i)*interval + offset).Milliseconds(),
			starts[i], ends[i], alive)
		alive -= ends[i]
	}

	rng := rand.New(rand.NewSource(time.Now().UnixNano()))

	todo := filter(txs, func(tx *transactionReport) bool {
		// for _, r := range tx.CallRegions {
		// 	if r.Kind == "client/postgres" {
		// 		return true
		// 	}
		// }
		return true
	})
	for d := 1 * time.Nanosecond; d < 1*time.Hour; d *= 2 {
		fn := func(tx *transactionReport) bool {
			return time.Duration(tx.DurationNanos) < d
			// return time.Duration(tx.Summary.SelfWallNanos) < d
		}
		fasts := filter(todo, fn)
		todo = filter(todo, func(tx *transactionReport) bool { return !fn(tx) })

		if len(fasts) > 0 {
			tx := fasts[rng.Intn(len(fasts))]
			fmt.Fprintf(&buf, "\n%d faster than %s\ngoroutine %d at %s\n%s",
				len(fasts), d, tx.Goroutine, time.Duration(tx.StartNanos), printOne(tx))
		}
	}

	return buf.Bytes()
}

func printOne(tx *transactionReport) []byte {
	var buf bytes.Buffer

	min := tx.StartNanos
	max := tx.StartNanos + tx.DurationNanos
	for _, region := range tx.CallRegions {
		if start := region.StartNanos; start < min {
			min = start
		}
		if end := region.StartNanos + region.DurationNanos; end > max {
			max = end
		}
	}

	fn := func(name string, start, end int64) {
		const width = 100
		left := (start - min) * width / (max - min)
		right := (end - min) * width / (max - min)
		if right-left < 1 {
			right = left + 1
		}
		fmt.Fprintf(&buf, "%s%s%s % -20s @ %0.3f + %0.3f\n",
			strings.Repeat(" ", int(left)),
			strings.Repeat(".", int(right-left)),
			strings.Repeat(" ", int(width-right)),
			name,
			float64(start-min)/1e6,
			float64(end-start)/1e6,
		)
	}

	fn(tx.Kind, tx.StartNanos, tx.StartNanos+tx.DurationNanos)
	for _, region := range tx.CallRegions {
		fn(region.Kind, region.StartNanos, region.StartNanos+region.DurationNanos)
	}

	return buf.Bytes()
}

type transactionReport struct {
	Kind          string
	Goroutine     uint64
	StartNanos    int64
	DurationNanos int64

	CallRegions      []*transactionRegion
	ExecutionRegions []*transactionRegion

	Summary *transactionSummary
}

type transactionRegion struct {
	Kind          string
	Goroutine     uint64
	StartNanos    int64
	DurationNanos int64
}

type transactionSummary struct {
	TotalProcessingNanos     int64
	SelfWallNanos            int64
	SelfIdleNanos            int64
	WaterfallGenerationCount int
}
