package main

import (
	"bytes"
	"flag"
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"code.justin.tv/common/chitin"
	_ "code.justin.tv/common/golibs/bininfo"
)

func main() {
	var period time.Duration
	flag.DurationVar(&period, "period", 10*time.Second, "Delay between reporting cycles")
	var dataDir string
	flag.StringVar(&dataDir, "data", "", "Path to data directory")
	var reportDir string
	flag.StringVar(&reportDir, "reports", "", "Path to reports directory")
	var cmd string
	flag.StringVar(&cmd, "cmd", "/bin/true", "Path to report command")
	flag.Parse()

	err := chitin.ExperimentalTraceProcessOptIn()
	if err != nil {
		log.Fatalf("trace enable err=%q", err)
	}

	// we'll run reports periodically
	tick := time.NewTimer(period)
	defer tick.Stop()
	// the first run should happen immediately
	tick.Reset(1 * time.Nanosecond)

	for {
		// time to go!
		<-tick.C
		// TODO: jitter
		tick.Reset(period)

		task := reportTask{
			dataDir:   dataDir,
			reportDir: reportDir,

			reportCmd: cmd,
		}

		err := task.run()
		if err != nil {
			log.Printf("report err=%q", err)
		}
	}
}

type reportTask struct {
	dataDir   string
	reportDir string

	reportCmd string
}

func (task *reportTask) run() error {
	log.Printf("report-state=%q", "begin")
	defer log.Printf("report-state=%q", "end")

	fullname, err := findData(task.dataDir)
	if err != nil {
		return err
	}

	destination := filepath.Join(task.reportDir, filepath.Base(fullname))
	log.Printf("destination=%q", destination)

	if _, err := os.Stat(destination); err != nil {
		if !os.IsNotExist(err) {
			return err
		}
	} else {
		return fmt.Errorf("report destination already exists: %v", destination)
	}

	err = os.MkdirAll(destination, 0755)
	if err != nil {
		return err
	}

	outdir := filepath.Join(destination, "report")

	// nginx is serving from task.reportDir, so all URLs should be
	// relative to that path
	urlPrefix, err := filepath.Rel(task.reportDir, outdir)
	if err != nil {
		return err
	}
	urlPrefix = "/" + urlPrefix

	cmd := exec.Command(task.reportCmd,
		"-cpuprofile", filepath.Join(destination, "cpu.out"),
		"-count=-1",
		"-input", fullname,
		"-outdir", outdir,
		"-urlprefix", urlPrefix,
	)
	setOptions(cmd)

	stdout, err := os.Create(filepath.Join(destination, "stdout.txt"))
	if err != nil {
		return err
	}
	defer func(c io.Closer) {
		err := c.Close()
		if err != nil {
			log.Printf("close err=%q", err)
		}
	}(stdout)
	stderr, err := os.Create(filepath.Join(destination, "stderr.txt"))
	if err != nil {
		return err
	}
	defer func(c io.Closer) {
		err := c.Close()
		if err != nil {
			log.Printf("close err=%q", err)
		}
	}(stderr)
	cmd.Stdout, cmd.Stderr = stdout, stderr

	err = cmd.Run()
	if err != nil {
		return err
	}

	// TODO: we get the location of the pprof executable as relative to this
	// runreport executable .. is this too dirty?

	// TODO: dereference report binary once at the start of this function so
	// we'll run pprof against the binary that we ran, even if there's a
	// concurrent deploy.

	ourBin, err := filepath.Abs(os.Args[0])
	if err != nil {
		return err
	}
	log.Printf("ourbin=%q", ourBin)
	pprofBin := filepath.Join(filepath.Dir(ourBin), "../tool/pprof")
	log.Printf("pprofbin=%q", pprofBin)

	pprof := exec.Command(pprofBin,
		"-dot", "-output", filepath.Join(destination, "cpu.dot"),
		cmd.Path, filepath.Join(destination, "cpu.out"))
	setOptions(pprof)
	log.Printf("tmpdir=%q", os.TempDir())
	pprof.Env = []string{"PPROF_TMPDIR=" + os.TempDir()}
	buf, err := pprof.CombinedOutput()
	log.Printf("pprof output=%q", buf)
	if err != nil {
		return err
	}

	err = task.writeIndex(filepath.ToSlash(filepath.Join(filepath.Base(fullname), "report")))
	if err != nil {
		return err
	}

	return nil
}

// writeIndex updates the "index.html" redirect page
func (task *reportTask) writeIndex(uri string) error {
	var buf bytes.Buffer

	template.Must(template.New("").Parse(`
<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta http-equiv="refresh" content="0; url={{.}}" />
  <title>Current Trace Report</title>
</head>

<body>
  <p>The current Trace report is available at <a href="{{.}}">{{.}}</a>.</p>
</body>
</html>
`[1:])).Execute(&buf, uri+"/")

	finalName := filepath.Join(task.reportDir, "index.html")

	tmpFile, err := ioutil.TempFile(task.reportDir, "index")
	if err != nil {
		return err
	}

	defer func() {
		err := os.Remove(tmpFile.Name())
		if err != nil {
			// best-effort cleanup, ignore any error
		}
	}()

	_, err = io.Copy(tmpFile, &buf)
	cerr := tmpFile.Close()
	if err != nil {
		return err
	}
	if cerr != nil {
		return cerr
	}

	err = os.Rename(tmpFile.Name(), finalName)
	if err != nil {
		return err
	}

	return nil
}

func findData(dataDir string) (string, error) {
	infos, err := ioutil.ReadDir(dataDir)
	if err != nil {
		return "", err
	}

	timely := make([]os.FileInfo, 0, len(infos))
	for _, info := range infos {
		if strings.HasSuffix(info.Name(), ".timely.pb") {
			timely = append(timely, info)
		}
	}

	sort.Sort(sort.Reverse(fileInfoByTime(timely)))

	for i, info := range timely {
		fullname := filepath.Join(dataDir, info.Name())
		if i == 1 {
			// the second newest file is the newest known-complete file
			return fullname, nil
		}
	}

	return "", os.ErrNotExist
}

type fileInfoByTime []os.FileInfo

func (l fileInfoByTime) Len() int           { return len(l) }
func (l fileInfoByTime) Less(i, j int) bool { return l[i].ModTime().Before(l[j].ModTime()) }
func (l fileInfoByTime) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }

var osSetOptions func(cmd *exec.Cmd)

func setOptions(cmd *exec.Cmd) {
	if osSetOptions != nil {
		osSetOptions(cmd)
	}
}
