package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"html/template"
	"io/ioutil"
	"log"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"code.justin.tv/release/trace/analysis"
	"code.justin.tv/release/trace/analysis/render"
	"code.justin.tv/release/trace/analysis/tx"
	"code.justin.tv/release/trace/persistent"
)

var templates = map[string]string{
	"header":      "html/header.html",
	"base":        "html/base.html",
	"breadcrumbs": "html/components/breadcrumbs.html",
}

// A project is a collection of commands.
type project struct {
	Name     string
	commands map[string]struct{}
}

func newProject(name string) project {
	return project{
		Name:     name,
		commands: make(map[string]struct{}),
	}
}

func (p project) Commands() []string {
	s := make([]string, 0)
	for c := range p.commands {
		s = append(s, c)
	}
	sort.Strings(s)
	return s
}

func (p project) IsACommand() bool {
	_, ok := p.commands[""]
	return ok
}

// An org is a collection of projects.
type org struct {
	Name     string
	projects map[string]project
}

func newOrg(name string) org {
	return org{
		Name:     name,
		projects: make(map[string]project),
	}
}

func (o org) Projects() []project {
	names := make([]string, 0)
	for name := range o.projects {
		names = append(names, name)
	}

	sort.Strings(names)

	projects := make([]project, 0)
	for _, name := range names {
		projects = append(projects, o.projects[name])
	}

	return projects
}

// orgs is a collection of org structs
type orgs map[string]org

// addService adds a service to the orgs tree
func (os orgs) addService(name string) {
	const (
		codePrefix   = "code.justin.tv"
		repoSegments = 3
	)

	if !strings.HasPrefix(name, codePrefix) {
		return
	}

	parts := strings.SplitN(name, "/", repoSegments+1)
	if len(parts) < repoSegments {
		// This is an incomplete service name, like 'code.justin.tv/web'
		return
	}
	// parts[0] is 'code.justin.tv'
	orgName, projName := parts[1], parts[2]
	var cmdName string
	if len(parts) == 4 {
		cmdName = parts[3]
	}

	o, ok := os[orgName]
	if !ok {
		o = newOrg(orgName)
		os[orgName] = o
	}
	p, ok := o.projects[projName]
	if !ok {
		p = newProject(projName)
		o.projects[projName] = p
	}
	p.commands[cmdName] = struct{}{}
}

type Report struct {
	// WritePath is the path to the directory to write report data and
	// assets into.
	WritePath string
	// URLPrefix is put in front of all internal URLs to let this
	// report's assets get served properly. This should be relative to
	// the root path of the server which is serving the HTML and asset
	// files that the report generates.
	URLPrefix string

	Services map[string]tx.TransactionSet
	Orgs     orgs

	// local database for storing report and sampled transactions
	DB *persistent.DB

	// driver for templating - includes defaults and includable
	// subtemplates
	template *template.Template
}

func NewReport(writePath string, urlPrefix string) *Report {
	rep := &Report{
		WritePath: writePath,
		URLPrefix: urlPrefix,
		Services:  make(map[string]tx.TransactionSet),
		Orgs:      make(orgs),
	}
	if err := rep.setTemplateDefaults(); err != nil {
		panic(err)
	}
	return rep
}

func clean(txs tx.TransactionSet) tx.TransactionSet {
	brokenDuration := 0
	out := tx.NewInMemoryTransactionSet()
	for _, tx := range txs.All() {
		if tx.RootDuration() != time.Duration(0) {
			out.Add(tx)
		} else {
			brokenDuration += 1
		}
	}
	log.Printf("brokenduration=%d", brokenDuration)
	return out
}

// Populate fills a report with data
func (r *Report) Populate(txs tx.TransactionSet) {
	txs = clean(txs)
	r.Services = analysis.SplitByServer(txs)

	svcNames := make([]string, 0, len(r.Services))
	for name, svc := range r.Services {
		if svc.Len() == 0 {
			delete(r.Services, name)
			continue
		}
		svcNames = append(svcNames, name)
	}

	sort.Strings(svcNames)
	for name, svc := range r.Services {
		log.Printf("service=%q requests=%d", name, svc.Len())
		r.Orgs.addService(name)
	}
}

// Template processing helper functions
// URLFor provides an absolute URL to the given path in the Report.
func (r *Report) URLFor(subpath string) string {
	return path.Join(r.URLPrefix, subpath)
}

// Returns the URL for a transaction
func (r *Report) URLForTransaction(t *tx.Transaction) string {
	if t == nil {
		return ""
	}
	queryParams := url.Values{}
	queryParams.Add("id", t.StringID())
	queryString := queryParams.Encode()
	return r.URLFor(fmt.Sprintf("tx.html?%s", queryString))
}

func (r *Report) URLForService(org, proj, cmd string) string {
	const repo = "code.justin.tv"
	parts := []string{
		"bin",
		repo,
		org,
		proj,
	}
	if cmd != "" {
		parts = append(parts, cmd)
	}
	return r.URLFor(path.Join(parts...) + ".html")
}

func seq(start, end, step float64) []float64 {
	reverse := false
	if start > end {
		reverse = true
		start, end = end, start
	}
	out := []float64{}
	for val := start; val <= end; val += step {
		out = append(out, val)
	}
	if reverse {
		reversed := make([]float64, len(out))
		for i, val := range out {
			reversed[len(out)-i-1] = val
		}
		out = reversed
	}
	return out
}

func (r *Report) setTemplateDefaults() error {
	t := template.New("")
	// set default functions
	defaults := template.FuncMap{
		"URLFor":        r.URLFor,
		"URLForTx":      r.URLForTransaction,
		"URLForService": r.URLForService,
		"add":           func(a, b int) int { return a + 1 },
		"scale":         func(x, y float64) float64 { return x * y },
		"seq":           seq,
		"join":          strings.Join,
	}
	t = t.Funcs(defaults)
	// set base templates
	var err error
	for name, path := range templates {
		t, err = t.New(name).Parse(string(render.MustAsset(path)))
		if err != nil {
			return err
		}
	}
	r.template = t
	return nil
}

func (r *Report) writeFile(path string, src []byte) error {
	dest := filepath.Join(r.WritePath, path)
	err := os.MkdirAll(filepath.Dir(dest), 0755)
	if err != nil {
		return err
	}
	err = ioutil.WriteFile(dest, src, 0644)
	if err != nil {
		return err
	}
	return nil
}

func (r *Report) writeAsset(path string) error {
	return r.writeFile(path, render.MustAsset(path))
}

func (r *Report) WriteTemplate(writeTo string, assetPath string, data interface{}) error {
	tmplSrc := string(render.MustAsset(assetPath))
	base, err := r.template.Clone()
	if err != nil {
		return err
	}
	tmpl, err := base.New(writeTo).Parse(tmplSrc)
	if err != nil {
		return err
	}

	var buf bytes.Buffer
	err = tmpl.Execute(&buf, data)
	if err != nil {
		return err
	}

	return r.writeFile(writeTo, buf.Bytes())
}

func (r *Report) DumpHTML() error {
	var err error
	if err = r.writeStylesheet(); err != nil {
		return err
	}
	if err = r.writeTransactionScript(); err != nil {
		return err
	}
	if err = r.writeIndex(); err != nil {
		return err
	}
	if err = r.writeTransactionBasePage(); err != nil {
		return err
	}
	if err = r.writeServiceList(); err != nil {
		return err
	}
	for name, svc := range r.Services {
		if err = r.writeTransansactionsForService(name, svc); err != nil {
			return err
		}
	}
	return nil
}

func (r *Report) writeStylesheet() error {
	log.Println("writing stylesheet")
	return r.writeAsset("css/style.css")
}

func (r *Report) writeTransactionScript() error {
	log.Println("writing tx.js")
	return r.writeAsset("js/tx.js")
}

func (r *Report) writeIndex() error {
	log.Println("writing index")
	data := map[string]interface{}{
		"Orgs":     r.Orgs,
		"PathJoin": path.Join,
	}
	return r.WriteTemplate("index.html", "html/index.html", data)
}

func (r *Report) writeServiceList() error {
	log.Println("writing service list")
	var err error
	for name, data := range r.Services {
		if err = r.writeServiceReport(name, data); err != nil {
			return err
		}
	}
	return nil
}

func (r *Report) writeServiceReport(name string, svcData tx.TransactionSet) error {
	log.Printf("writing service report for %s\n", name)
	dest := filepath.Join("bin", name+".html")

	signatures := analysis.SplitBySignature(svcData)

	log.Printf("writing signature transaction reports for %s\n", name)
	for _, sigData := range signatures {
		for _, t := range sigData.Examples {
			if err := r.writeTransactionData(t); err != nil {
				return err
			}
		}
	}

	breadcrumbs := [][2]string{
		[2]string{r.URLFor("/"), "home"},
		[2]string{r.URLFor(dest), name},
	}

	data := map[string]interface{}{
		"SvcName":        name,
		"SvcData":        svcData,
		"Signatures":     signatures,
		"TimePercentile": timePercentile,
		"SampleTxs":      analysis.Sample,
		"Slowest":        analysis.Percentile(svcData, 1.0, tx.ByTime),
		"Median":         analysis.Percentile(svcData, 0.5, tx.ByTime),
		"Fastest":        analysis.Percentile(svcData, 0.0, tx.ByTime),
		"Deepest":        analysis.Percentile(svcData, 1.0, tx.ByDepth),
		"Largest":        analysis.Percentile(svcData, 1.0, tx.BySize),
		"VegaSpec":       vega(svcData),
		"ReversePath":    reversePath,
		"Breadcrumbs":    breadcrumbs,
	}
	err := r.WriteTemplate(dest, "html/binary.html", data)
	if err != nil {
		return err
	}
	return nil
}

func (r *Report) writeTransansactionsForService(name string, data tx.TransactionSet) error {
	if err := r.writeTransactionData(analysis.Percentile(data, 1.0, tx.ByTime)); err != nil {
		return err
	}
	if err := r.writeTransactionData(analysis.Percentile(data, 0.5, tx.ByTime)); err != nil {
		return err
	}
	if err := r.writeTransactionData(analysis.Percentile(data, 0.0, tx.ByTime)); err != nil {
		return err
	}
	if err := r.writeTransactionData(analysis.Percentile(data, 1.0, tx.ByDepth)); err != nil {
		return err
	}
	if err := r.writeTransactionData(analysis.Percentile(data, 1.0, tx.BySize)); err != nil {
		return err
	}
	return nil
}

func (r *Report) writeTransactionData(t *tx.Transaction) error {
	log.Printf("writing data for transaction %s\n", t.StringID())

	dest := filepath.Join("tx", t.StringID()+".json")
	bytes, err := json.Marshal(t.Root)
	if err != nil {
		return err
	}

	if r.DB != nil && t.Tx != nil {
		err := r.DB.StoreTransaction(t.Tx)
		if err != nil {
			return err
		}
	}

	return r.writeFile(dest, bytes)
}

func (r *Report) writeTransactionBasePage() error {
	log.Println("writing base page for transaction reports")
	return r.WriteTemplate("tx.html", "html/tx.html", "")
}

func reversePath(in string) string {
	parts := strings.Split(in, "/")
	for i := range parts {
		parts[i] = ".."
	}
	return strings.Join(parts, "/")
}

func timePercentile(s tx.TransactionSet, p float64) float64 {
	return float64(analysis.TimingPercentile(s, p)) / float64(time.Millisecond)
}
