package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"math"
	"net/http"
	"path/filepath"
	"sort"
	"strings"
	"sync/atomic"
	"time"

	jira "github.com/andygrunwald/go-jira"
)

type Fingerprint struct {
	Key         string `json:"key"`
	Link        string `json:"link"`
	Type        string `json:"type"`
	Summary     string `json:"summary"`
	Description string `json:"description"`
}

type IssueFingerprint struct {
	Key   string `json:"key"`
	Match bool   `json:"match"`
}

type Issue struct {
	Key          string             `json:"key"`
	Link         string             `json:"link"`
	Summary      string             `json:"summary"`
	Date         time.Time          `json:"date"`
	Fingerprints []IssueFingerprint `json:"fingerprints"`
}

func (issue Issue) GetFingerprint(key string) *IssueFingerprint {
	for _, f := range issue.Fingerprints {
		if f.Key == key {
			return &f
		}
	}
	return nil
}

type KnowlegeBase struct {
	Fingerprints []Fingerprint
	Issues       []Issue
}

var globalKnowlegeBase atomic.Value

func reloadKnowlegeBase() error {
	log.Println("reloading knowlegebase")

	tp := jira.BasicAuthTransport{
		Username: *jiraUser,
		Password: *jiraPassword,
	}
	client, err := jira.NewClient(tp.Client(), "https://internal.pingcap.net/jira/")
	if err != nil {
		return err
	}

	var kb KnowlegeBase

	issues, _, err := client.Issue.Search("project=ELF", &jira.SearchOptions{MaxResults: -1, Fields: []string{"summary", "description", "customfield_10956"}})
	if err != nil {
		return err
	}
	for _, issue := range issues {
		typ, ok := issue.Fields.Unknowns["customfield_10956"].(map[string]interface{})
		if !ok {
			log.Printf("Fingerprint type not found for issue %v\n", issue.Key)
			continue
		}

		kb.Fingerprints = append(kb.Fingerprints, Fingerprint{
			Key:         issue.Key,
			Link:        fmt.Sprintf("https://internal.pingcap.net/jira/browse/%v", issue.Key),
			Type:        typ["value"].(string),
			Summary:     issue.Fields.Summary,
			Description: issue.Fields.Description,
		})
	}

	issues, _, err = client.Issue.Search("project in (TIDB, TOOL, ONCALL) AND issueLinkType in (matches, mismatches)", &jira.SearchOptions{MaxResults: -1, Fields: []string{"summary", "description", "created", "issuelinks"}})
	if err != nil {
		return err
	}
	for _, issue := range issues {
		var fingerprints []IssueFingerprint
		for _, link := range issue.Fields.IssueLinks {
			if strings.HasPrefix(link.Type.Name, "Fingerprint") && link.OutwardIssue != nil && strings.HasPrefix(link.OutwardIssue.Key, "ELF") {
				fingerprints = append(fingerprints, IssueFingerprint{
					Key:   link.OutwardIssue.Key,
					Match: link.Type.Name == "FingerprintMatch",
				})
			}
		}
		if len(fingerprints) == 0 {
			continue
		}

		kb.Issues = append(kb.Issues, Issue{
			Key:          issue.Key,
			Link:         fmt.Sprintf("https://internal.pingcap.net/jira/browse/%v", issue.Key),
			Summary:      issue.Fields.Summary,
			Date:         time.Time(issue.Fields.Created),
			Fingerprints: fingerprints,
		})
	}

	globalKnowlegeBase.Store(kb)

	return nil
}

var fingerPrintScoreWeights = map[string][]int{
	// Order: True+True, True+N/A, True+False, False+True, False+N/A, False+False
	"":          []int{100, 0, -200, -200, 0, 50},
	"customer":  []int{80, 0, 0, 0, 0, 0},
	"problem":   []int{400, 0, -600, -600, 0, 150},
	"errorcode": []int{400, 0, -500, -500, 0, 150},
	"metrics":   []int{300, 0, -500, -500, 0, 150},
	"operation": []int{200, 0, -400, -400, 0, 100},
}

type bySimilarity struct {
	index  Issue
	issues []Issue
	types  map[string]string
}

func (a bySimilarity) Len() int      { return len(a.issues) }
func (a bySimilarity) Swap(i, j int) { a.issues[i], a.issues[j] = a.issues[j], a.issues[i] }
func (a bySimilarity) Less(i, j int) bool {
	getScore := func(x, y Issue) (score float64) {
		for _, fpx := range x.Fingerprints {
			fpy := y.GetFingerprint(fpx.Key)
			scoreWeight, ok := fingerPrintScoreWeights[a.types[fpx.Key]]
			if !ok {
				scoreWeight = fingerPrintScoreWeights[""]
			}
			var index int
			if !fpx.Match {
				index += 3
			}
			if fpy == nil {
				index++
			} else if !fpy.Match {
				index += 2
			}
			score += float64(scoreWeight[index])
		}

		if timeScore := 365 - math.Abs(x.Date.Sub(y.Date).Hours())/24; timeScore > 0 {
			score += timeScore
		}
		return
	}
	return getScore(a.index, a.issues[i]) < getScore(a.index, a.issues[j])
}

type byDistinction struct {
	fingerprints []Fingerprint
	issues       []Issue
}

func (a byDistinction) Len() int { return len(a.fingerprints) }
func (a byDistinction) Swap(i, j int) {
	a.fingerprints[i], a.fingerprints[j] = a.fingerprints[j], a.fingerprints[i]
}
func (a byDistinction) Less(i, j int) bool {
	score := func(issues []Issue, fp Fingerprint) int {
		m := make(map[bool]struct{})
		for i, issue := range issues {
			m[issue.GetFingerprint(fp.Key) != nil] = struct{}{}
			if len(m) == 2 {
				return i
			}
		}
		return len(issues)
	}
	return score(a.issues, a.fingerprints[i]) < score(a.issues, a.fingerprints[j])
}

var (
	jiraUser     = flag.String("u", "", "jira user name")
	jiraPassword = flag.String("p", "", "jira password")
	addr         = flag.String("addr", ":8080", "HTTP addr")
)

func main() {
	flag.Parse()

	if err := reloadKnowlegeBase(); err != nil {
		panic(err)
	}

	go func() {
		for {
			now := time.Now()
			time.Sleep(now.Truncate(10 * time.Minute).Add(10 * time.Minute).Sub(now))
			if err := reloadKnowlegeBase(); err != nil {
				log.Println("reload err:", err)
			}
		}
	}()

	serveFile := func(path, file string) {
		var builtinTypesLower = map[string]string{
			".css":  "text/css; charset=utf-8",
			".gif":  "image/gif",
			".htm":  "text/html; charset=utf-8",
			".html": "text/html; charset=utf-8",
			".jpg":  "image/jpeg",
			".js":   "application/javascript",
			".wasm": "application/wasm",
			".pdf":  "application/pdf",
			".png":  "image/png",
			".svg":  "image/svg+xml",
			".xml":  "text/xml; charset=utf-8",
		}

		http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
			ext := strings.ToLower(filepath.Ext(path))
			if t, ok := builtinTypesLower[ext]; ok {
				w.Header().Set("Content-Type", t)
			}
			data, _ := Asset(file)
			w.Write(data)
		})
	}

	for _, fileName := range AssetNames() {
		serveFile("/"+fileName, fileName)
	}
	serveFile("/", "index.html")

	http.HandleFunc("/list", func(w http.ResponseWriter, r *http.Request) {
		data, err := ioutil.ReadAll(r.Body)
		r.Body.Close()
		if err != nil {
			log.Println(err)
			return
		}
		var fingerprints []IssueFingerprint
		err = json.Unmarshal(data, &fingerprints)
		if err != nil {
			log.Println(err)
			return
		}

		index := Issue{Date: time.Now(), Fingerprints: fingerprints}

		kb := globalKnowlegeBase.Load().(KnowlegeBase)
		fingerprintTypes := make(map[string]string)
		for _, fp := range kb.Fingerprints {
			fingerprintTypes[fp.Key] = fp.Type
		}

		type Res struct {
			Fingerprints []Fingerprint `json:"fingerprints"`
			Issues       []Issue       `json:"issues"`
		}
		var res Res
		res.Fingerprints = append(res.Fingerprints, kb.Fingerprints...)
		res.Issues = append(res.Issues, kb.Issues...)
		sort.Sort(sort.Reverse(bySimilarity{
			index:  index,
			issues: res.Issues,
			types:  fingerprintTypes,
		}))
		sort.Sort(byDistinction{
			fingerprints: res.Fingerprints,
			issues:       res.Issues,
		})

		b, _ := json.Marshal(res)
		w.Write(b)
	})

	log.Println("ready to serve on", *addr)
	http.ListenAndServe(*addr, nil)
}
