package webserver

import (
	"container/heap"
	"errors"
	"math/rand"
	"regexp"
	"sort"
	"sync"
	"unicode"

	data "git.martyn.berlin/martyn/twitchsingstools/internal/data"
	irc "git.martyn.berlin/martyn/twitchsingstools/internal/irc"
	"github.com/dustin/go-humanize"
	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"

	"encoding/json"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

//var store = sessions.NewCookieStore(os.Getenv("SESSION_KEY"))

type twitchauthresponse struct {
	Access_token  string   `json: "access_token"`
	Expires_in    int      `json: "expires_in"`
	Refresh_token string   `json: "refresh_token"`
	Scope         []string `json: "scope"`
	Token_type    string   `json: "token_type"`
}

type twitchUser struct {
	Id                string `json: "id"`
	Login             string `json: "login"`
	Display_name      string `json: "display_name"`
	Type              string `json: "type"`
	Broadcaster_type  string `json: "affiliate"`
	Description       string `json: "description"`
	Profile_image_url string `json: "profile_image_url"`
	Offline_image_url string `json: "offline_image_url"`
	View_count        int    `json: "view_count"`
}

type twitchUsersBigResponse struct {
	Data []twitchUser `json:"data"`
}

type twitchCursor struct {
	Cursor string `json:"cursor"`
}
type videoStruct struct {
	CreatedAt    string `json:"created_at"`    // Date when the video was created.
	Description  string `json:"description"`   // Description of the video.
	Duration     string `json:"duration"`      // Length of the video.
	ID           string `json:"id"`            // ID of the video.
	Language     string `json:"language"`      // Language of the video.
	Pagination   string `json:"pagination"`    // A cursor value, to be used in a subsequent request to specify the starting point of the next set of results.
	PublishedAt  string `json:"published_at"`  // Date when the video was published.
	ThumbnailURL string `json:"thumbnail_url"` // Template URL for the thumbnail of the video.
	Title        string `json:"title"`         // Title of the video.
	Type         string `json:"type"`          // Type of video. Valid values: "upload", "archive", "highlight".
	URL          string `json:"url"`           // URL of the video.
	UserID       string `json:"user_id"`       // ID of the user who owns the video.
	UserName     string `json:"user_name"`     // Display name corresponding to user_id.
	ViewCount    int    `json:"view_count"`    // Number of times the video has been viewed.
	Viewable     string `json:"viewable"`      // Indicates whether the video is publicly viewable. Valid values: "public", "private".
}
type videosResponse struct {
	Data       []videoStruct `json:"data"`
	Pagination twitchCursor  `json:"pagination"`
}

var ircBot *irc.KardBot
var globalData *data.GlobalData

func HealthHandler(response http.ResponseWriter, request *http.Request) {
	response.Header().Add("Content-type", "text/plain")
	fmt.Fprint(response, "I'm okay jack!")
}

func NotFoundHandler(response http.ResponseWriter, request *http.Request) {
	response.Header().Add("X-Template-File", "html"+request.URL.Path)
	response.WriteHeader(404)
	tmpl := template.Must(template.ParseFiles("web/404.html"))
	tmpl.Execute(response, nil)
}

func CSSHandler(response http.ResponseWriter, request *http.Request) {
	response.Header().Add("Content-type", "text/css")
	tmpl := template.Must(template.ParseFiles("web/cover.css"))
	tmpl.Execute(response, nil)
}

func RootHandler(response http.ResponseWriter, request *http.Request) {
	request.URL.Path = "/index.html"
	TemplateHandler(response, request)
}

func TemplateHandler(response http.ResponseWriter, request *http.Request) {
	response.Header().Add("X-Template-File", "web"+request.URL.Path)
	type TemplateData struct {
		Prompt       string
		AvailCount   int
		ChannelCount int
		MessageCount int
		ClientID     string
		BaseURI      string
	}
	_ = strings.ToLower("Hello")
	if strings.Index(request.URL.Path, "/") < 0 {
		http.Error(response, "No slashes wat - "+request.URL.Path, http.StatusInternalServerError)
		return
	}

	basenameSlice := strings.Split(request.URL.Path, "/")
	basename := basenameSlice[len(basenameSlice)-1]
	//fmt.Fprintf(response, "%q", basenameSlice)
	tmpl, err := template.New(basename).Funcs(template.FuncMap{
		"ToUpper": strings.ToUpper,
		"ToLower": strings.ToLower,
	}).ParseFiles("web" + request.URL.Path)
	if err != nil {
		http.Error(response, err.Error(), http.StatusInternalServerError)
		return
		// NotFoundHandler(response, request)
		// return
	}
	var td = TemplateData{ircBot.Prompts[rand.Intn(len(ircBot.Prompts))], len(ircBot.Prompts), ircBot.ActiveChannels(), 0, ircBot.AppCredentials.ClientID, "https://" + globalData.Config.ExternalURL}
	err = tmpl.Execute(response, td)
	if err != nil {
		http.Error(response, err.Error(), http.StatusInternalServerError)
		return
	}
}

func LeaveHandler(response http.ResponseWriter, request *http.Request) {
	request.URL.Path = "/bye.html"
	TemplateHandler(response, request)
}

func humanTimeFromTimeString(s string) string {
	return s
	// format := "2006-01-02 15:04:05 +0000 UTC"
	// d, _ := time.Parse(format, s)
	// return humanize.Time(d)
}

type AugmentedSingsVideoStruct struct {
	Date               time.Time
	NiceDate           string
	ShortDate          string
	FullTitle          string
	Duet               bool
	OtherSinger        string
	SongTitle          string
	LastSungSong       time.Time
	NiceLastSungSong   string
	LastSungSinger     time.Time
	NiceLastSungSinger string
	VideoURL           string
	VideoNumber        string //yes, I don't care any more.
}

func AugmentSingsVideoStructForCSV(input data.SingsVideoStruct) AugmentedSingsVideoStruct {
	var ret AugmentedSingsVideoStruct
	ret.Date = input.Date
	ret.NiceDate = input.Date.Format("2006-01-02 15:04:05")
	ret.ShortDate = input.Date.Format("2006-01-02")
	ret.FullTitle = input.FullTitle
	ret.Duet = input.Duet
	ret.OtherSinger = input.OtherSinger
	ret.SongTitle = input.SongTitle
	ret.LastSungSong = input.LastSungSong
	ret.NiceLastSungSong = input.LastSungSong.Format("2006-01-02 15:04:05")
	ret.LastSungSinger = input.LastSungSinger
	ret.VideoURL = input.VideoURL
	urlParts := strings.Split(input.VideoURL, "/")
	ret.VideoNumber = urlParts[len(urlParts)-1]
	if !ret.Duet {
		ret.NiceLastSungSinger = "Solo performance"
	} else {
		ret.NiceLastSungSinger = input.LastSungSinger.Format("2006-01-02 15:04:05")
	}
	return ret
}

func AugmentSingsVideoStructSliceForCSV(input []data.SingsVideoStruct) []AugmentedSingsVideoStruct {
	ret := make([]AugmentedSingsVideoStruct, 0)
	for _, record := range input {
		ret = append(ret, AugmentSingsVideoStructForCSV(record))
	}
	return ret
}

func AugmentSingsVideoStruct(input data.SingsVideoStruct) AugmentedSingsVideoStruct {
	var ret AugmentedSingsVideoStruct
	ret.Date = input.Date
	ret.NiceDate = humanize.Time(input.Date)
	ret.FullTitle = input.FullTitle
	ret.Duet = input.Duet
	ret.OtherSinger = input.OtherSinger
	ret.SongTitle = input.SongTitle
	ret.LastSungSong = input.LastSungSong
	ret.NiceLastSungSong = humanize.Time(input.LastSungSong)
	ret.LastSungSinger = input.LastSungSinger
	if !ret.Duet {
		ret.NiceLastSungSinger = "Solo performance"
	} else {
		ret.NiceLastSungSinger = humanize.Time(input.LastSungSinger)
	}
	return ret
}

func AugmentSingsVideoStructSlice(input []data.SingsVideoStruct) []AugmentedSingsVideoStruct {
	ret := make([]AugmentedSingsVideoStruct, 0)
	for _, record := range input {
		ret = append(ret, AugmentSingsVideoStruct(record))
	}
	return ret
}

func UnauthorizedHandler(response http.ResponseWriter, request *http.Request) {
	response.Header().Add("X-Template-File", "html"+request.URL.Path)
	response.WriteHeader(401)
	tmpl := template.Must(template.ParseFiles("web/401.html"))
	tmpl.Execute(response, nil)
}

func twitchHTTPClient(call string, bearer string) (string, error) {
	url := "https://api.twitch.tv/helix/" + call
	var bearerHeader = "Bearer " + bearer

	req, err := http.NewRequest("GET", url, nil)
	req.Header.Add("Client-ID", ircBot.AppCredentials.ClientID)
	req.Header.Add("Authorization", bearerHeader)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, _ := ioutil.ReadAll(resp.Body)
	if resp.StatusCode != 200 {
		return string(http.StatusText(resp.StatusCode)), errors.New("HTTP ERROR: " + http.StatusText(resp.StatusCode))
	} else {
		return string([]byte(body)), nil
	}
}

func ValidateTwitchBearerToken(bearer string) (bool, error) {
	url := "https://id.twitch.tv/oauth2/validate"
	var bearerHeader = "Bearer " + bearer

	req, err := http.NewRequest("GET", url, nil)
	req.Header.Add("Client-ID", ircBot.AppCredentials.ClientID)
	req.Header.Add("Authorization", bearerHeader)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return false, err
	}
	defer resp.Body.Close()

	_, _ = ioutil.ReadAll(resp.Body)
	return resp.StatusCode == 200, nil
}

func twitchVidToSingsVid(twitchFormat videoStruct) (data.SingsVideoStruct, error) {
	var ret data.SingsVideoStruct
	layout := "2006-01-02T15:04:05Z"
	var d time.Time
	d, err := time.Parse(layout, twitchFormat.CreatedAt)
	if err != nil {
		return ret, err
	}
	ret.Date = d
	ret.VideoURL = twitchFormat.URL

	var DuetRegex = regexp.MustCompile(`^Duet with ([^ ]*): (.*)$`)
	matches := DuetRegex.FindAllStringSubmatch(twitchFormat.Title, -1)
	if matches == nil {
		var SoloRegex = regexp.MustCompile(`^Solo performance: (.*)$`)
		matches := SoloRegex.FindAllStringSubmatch(twitchFormat.Title, -1)
		if matches == nil {
			ret.SongTitle = "Does not match either solo or duet performance regex "
			ret.FullTitle = twitchFormat.Title
			return ret, errors.New("Does not match either solo or duet performance regex : " + twitchFormat.Title)
		}
		ret.Duet = false
		ret.SongTitle = matches[0][1]
		ret.FullTitle = twitchFormat.Title
		return ret, nil
	}
	ret.Duet = true
	ret.OtherSinger = matches[0][1]
	ret.SongTitle = matches[0][2]
	ret.FullTitle = twitchFormat.Title
	return ret, nil
}

type SongSings struct {
	SongTitle string
	Sings     int
}
type SingerSings struct {
	SingerName string
	Sings      int
}

func calculateTopNSongs(songCache []data.SingsVideoStruct, howMany int) []SongSings {
	songMap := map[string]int{}
	for _, record := range songCache {
		sings := songMap[record.SongTitle]
		sings += 1
		songMap[record.SongTitle] = sings
	}
	slice := make([]SongSings, 0)
	for key, value := range songMap {
		ss := SongSings{key, value}
		slice = append(slice, ss)
	}
	sort.SliceStable(slice, func(i, j int) bool {
		return slice[i].Sings > slice[j].Sings
	})
	var ret []SongSings
	for i := 1; i <= howMany; i++ {
		ret = append(ret, slice[i])
	}
	return ret
}

func IsLower(s string) bool {
	for _, r := range s {
		if !unicode.IsLower(r) && unicode.IsLetter(r) {
			return false
		}
	}
	return true
}

func unmangleSingerName(MangledCaseName string, songCache []data.SingsVideoStruct) string {
	options := make(map[string]string, 0)
	for _, record := range songCache {
		if strings.ToUpper(MangledCaseName) == strings.ToUpper(record.OtherSinger) {
			options[record.OtherSinger] = "WHATEVER"
		}
	}
	// One answer means we don't care upper, lower, mixed.
	if len(options) == 1 {
		for key := range options {
			return key
		}
	}
	// More than one is probably closed-beta where the name was lowercased.
	for key := range options {
		if !IsLower(key) {
			return key
		}
	}
	// Eep, we shouldn't get here, let's just return something.
	for key := range options {
		return key
	}
	return ""
}

func prependSong(x []SingerSings, y SingerSings) []SingerSings {
	x = append(x, SingerSings{})
	copy(x[1:], x)
	x[0] = y
	return x
}

type kv struct {
	Key   string
	Value int
}

func getHeap(m map[string]int) *KVHeap {
	h := &KVHeap{}
	heap.Init(h)
	for k, v := range m {
		heap.Push(h, kv{k, v})
	}
	return h
}

type KVHeap []kv

func (h KVHeap) Len() int           { return len(h) }
func (h KVHeap) Less(i, j int) bool { return h[i].Value > h[j].Value }
func (h KVHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *KVHeap) Push(x interface{}) {
	*h = append(*h, x.(kv))
}

func (h *KVHeap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}

var cacheLock sync.Mutex

type CacheDetails struct {
	Age       time.Duration `json: "cache_age"`
	AgeStr    string        `json: "cache_age_nice"`
	SongCount int           `json: "expires_in"`
}

func getCacheDetails(channel string) CacheDetails {
	var ret CacheDetails
	channelData := globalData.ChannelData[channel]
	ret.Age = time.Now().Sub(channelData.VideoCacheUpdated)
	ret.AgeStr = humanize.Time(channelData.VideoCacheUpdated)
	ret.SongCount = len(channelData.VideoCache)
	return ret
}

func forceUpdateCache(channel string) {
	fmt.Printf("Forcing cache update!")
	channelData := globalData.ChannelData[channel]
	tenHours := time.Hour * -10
	videoCacheUpdated := time.Now().Add(tenHours) // Subtract 10 hours from now, cache is 10 hours old.
	channelData.VideoCacheUpdated = videoCacheUpdated
	globalData.ChannelData[channel] = channelData
	updateCacheIfNecessary(channel)
}

func updateCacheIfNecessary(channel string) {
	cacheLock.Lock()
	channelData := globalData.ChannelData[channel]
	if time.Now().Sub(channelData.VideoCacheUpdated).Hours() > 1 {
		fmt.Printf("Cache of %d performances is older than an hour - %.1f hours old to be precise... fetching.\n", len(channelData.VideoCache), time.Now().Sub(globalData.ChannelData[channel].VideoCacheUpdated).Hours())
		vids, err := fetchAllVoDs(channelData.TwitchUserID, channelData.Bearer)
		if err != nil {
			errCache := make([]data.SingsVideoStruct, 0)
			var ret data.SingsVideoStruct
			ret.FullTitle = "Error fetching videos: " + err.Error()
			errCache = append(errCache, ret)
			vids = errCache
		}
		updateCalculatedFields(vids)
		globalData.UpdateVideoCache(channel, vids)
	} else {
		fmt.Printf("Cache of %d performances is younger than an hour - %.1f hours old to be precise... not fetching.\n", len(channelData.VideoCache), time.Now().Sub(globalData.ChannelData[channel].VideoCacheUpdated).Hours())
	}
	cacheLock.Unlock()
}

func calculateTopNSingers(songCache []data.SingsVideoStruct, howMany int) []SingerSings {
	songMap := map[string]int{}
	songCount := 0
	for _, record := range songCache {
		if record.Duet {
			sings := songMap[strings.ToUpper(record.OtherSinger)]
			sings++
			songCount++
			songMap[strings.ToUpper(record.OtherSinger)] = sings
		}
	}
	slice := make([]SingerSings, 0)
	h := getHeap(songMap)
	for i := 0; i < howMany; i++ {
		deets := heap.Pop(h)
		position := i + 1
		fmt.Printf("%d) %#v\n", position, deets)
		ss := SingerSings{unmangleSingerName(deets.(kv).Key, songCache), deets.(kv).Value}
		slice = append(slice, ss)
	}
	fmt.Printf("Considered %d songs, Shan has %d\n", songCount, songMap["SHANXOX_"])
	return slice
}

func calculateLastSungSongDate(songCache []data.SingsVideoStruct, SongTitle string) time.Time {
	var t time.Time
	for _, record := range songCache {
		if record.SongTitle == SongTitle {
			if record.Date.After(t) {
				t = record.Date
			}
		}
	}
	return t
}

func calculateLastSungSingerDate(songCache []data.SingsVideoStruct, Singer string) time.Time {
	var t time.Time
	for _, record := range songCache {
		if strings.ToUpper(record.OtherSinger) == strings.ToUpper(Singer) {
			if record.Date.After(t) {
				t = record.Date
				if strings.ToUpper(Singer) == "SHANXOX_" {
					fmt.Printf("Last sang with %s (%s) %s", Singer, record.OtherSinger, t)
				}
			}
		}
	}
	return t
}

func updateCalculatedFields(songCache []data.SingsVideoStruct) {
	for i, record := range songCache {
		if record.Duet {
			songCache[i].LastSungSinger = calculateLastSungSingerDate(songCache, record.OtherSinger)
		}
		songCache[i].LastSungSong = calculateLastSungSongDate(songCache, record.SongTitle)
	}
}

func fetchVoDsPagesRecursive(userID string, bearer string, from string) ([]data.SingsVideoStruct, error) {
	url := ""
	if from == "" {
		url = "videos?user_id=" + userID + "&first=100&type=upload"
	} else {
		url = "videos?user_id=" + userID + "&first=100&type=upload&after=" + from
	}

	vidResponse, err := twitchHTTPClient(url, bearer)
	if err != nil {
		return nil, err
	}

	var fullResponse videosResponse
	err = json.Unmarshal([]byte(vidResponse), &fullResponse)
	if err != nil {
		return nil, err
	}
	titles := make([]data.SingsVideoStruct, 0)
	for _, videoData := range fullResponse.Data {
		ret, err := twitchVidToSingsVid(videoData)
		if err != nil {
			titles = append(titles, ret)
		} else {
			titles = append(titles, ret)
		}
	}
	if fullResponse.Pagination.Cursor != "" {
		fmt.Println("NOTICE: Recursion needed, cursor is " + fullResponse.Pagination.Cursor)
		recurse, err := fetchVoDsPagesRecursive(userID, bearer, fullResponse.Pagination.Cursor)
		if err != nil {
			fmt.Println("ERROR: Bailing out during recursion because of error : " + err.Error())
		}
		titles = append(titles, recurse...)
	}
	return titles, nil
}

func fetchAllVoDs(userID string, bearer string) ([]data.SingsVideoStruct, error) {
	tokenValid, err := ValidateTwitchBearerToken(bearer)
	if err != nil {
		fmt.Println("Error validating token : " + err.Error())
		return nil, err
	}
	if !tokenValid {
		fmt.Println("Error validating token (revoked?)")
		return nil, errors.New("Failed to validate token with twitch (authorization revoked?!)")
	}
	titles, err := fetchVoDsPagesRecursive(userID, bearer, "")
	if err != nil {
		return make([]data.SingsVideoStruct, 0), err
	}
	return titles, nil
}

func TwitchAdminHandler(response http.ResponseWriter, request *http.Request) {

	vars := mux.Vars(request)
	if vars["code"] != "" {
		response.Header().Add("Content-type", "text/plain")
		resp, err := http.PostForm(
			"https://id.twitch.tv/oauth2/token",
			url.Values{
				"client_id":     {ircBot.AppCredentials.ClientID},
				"client_secret": {ircBot.AppCredentials.ClientSecret},
				"code":          {vars["code"]},
				"grant_type":    {"authorization_code"},
				"redirect_uri":  {"https://" + globalData.Config.ExternalURL + "/twitchadmin"}})
		if err != nil {
			response.WriteHeader(500)
			response.Header().Add("Content-type", "text/plain")
			fmt.Fprint(response, "ERROR: "+err.Error())
			return
		}

		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			response.WriteHeader(500)
			response.Header().Add("Content-type", "text/plain")
			fmt.Fprint(response, "ERROR: "+err.Error())
			return
		}

		var oauthResponse twitchauthresponse
		err = json.Unmarshal(body, &oauthResponse)
		if err != nil {
			response.WriteHeader(500)
			response.Header().Add("Content-type", "text/plain")
			fmt.Fprint(response, "ERROR: "+err.Error())
			return
		}

		usersResponse, err := twitchHTTPClient("users", oauthResponse.Access_token)
		if err != nil {
			response.WriteHeader(500)
			response.Header().Add("Content-type", "text/plain")
			fmt.Fprint(response, "ERROR: "+err.Error())
			return
		}

		var usersObject twitchUsersBigResponse
		err = json.Unmarshal([]byte(usersResponse), &usersObject)
		if err != nil {
			response.WriteHeader(500)
			response.Header().Add("Content-type", "text/plain")
			fmt.Fprint(response, "ERROR: "+err.Error())
			return
		}

		if len(usersObject.Data) != 1 {
			response.WriteHeader(500)
			response.Header().Add("Content-type", "text/plain")
			fmt.Fprint(response, "ERROR: Twitch returned not 1 user for the request!\n---\n")
			return
		}

		user := usersObject.Data[0]
		magicCode := globalData.ReadOrCreateChannelKey(user.Login)
		globalData.UpdateBearerToken(user.Login, oauthResponse.Access_token)
		globalData.UpdateTwitchUserID(user.Login, user.Id)
		url := "https://" + globalData.Config.ExternalURL + "/admin/" + user.Login + "/" + magicCode
		http.Redirect(response, request, url, http.StatusFound)

	} else {

		fmt.Fprintf(response, "I'm not okay jack! %v \n", vars)
		for key, val := range vars {
			fmt.Fprintf(response, "%s = %s\n", key, val)
		}
	}
}

func TwitchBackendHandler(response http.ResponseWriter, request *http.Request) {
	response.Header().Add("Content-type", "text/plain")
	vars := mux.Vars(request)
	// fmt.Fprintf(response, "I'm okay jack! %v \n", vars)
	// for key, val := range(vars) {
	// 	fmt.Fprint(response, "%s = %s\n", key, val)
	// }

	if vars["code"] != "" {
		// https://id.twitch.tv/oauth2/token
		// ?client_id=<your client ID>
		// &client_secret=<your client secret>
		// &code=<authorization code received above>
		// &grant_type=authorization_code
		// &redirect_uri=<your registered redirect URI>

		// ircBot.AppCredentials.ClientID
		// ircBot.AppCredentials.Password
		// vars["oauthtoken"]
		// authorization_code
		// "https://"+globalData.Config.ExternalURL+/twitchadmin
		fmt.Println("Asking twitch for more...")
		resp, err := http.PostForm(
			"https://id.twitch.tv/oauth2/token",
			url.Values{
				"client_id":     {ircBot.AppCredentials.ClientID},
				"client_secret": {ircBot.AppCredentials.ClientSecret},
				"code":          {vars["code"]},
				"grant_type":    {"authorization_code"},
				"redirect_uri":  {"https://" + globalData.Config.ExternalURL + "/twitchadmin"}})
		if err != nil {
			response.Header().Add("Content-type", "text/plain")
			fmt.Fprint(response, "ERROR: "+err.Error())
		}
		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			response.Header().Add("Content-type", "text/plain")
			fmt.Fprint(response, "ERROR: "+err.Error())
		}
		response.Header().Add("Content-type", "text/plain")
		fmt.Fprint(response, string(body))
	} else {
		UnauthorizedHandler(response, request)
	}
}

func CSVHandler(response http.ResponseWriter, request *http.Request) {
	vars := mux.Vars(request)
	if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey {
		UnauthorizedHandler(response, request)
		return
	}
	type TemplateData struct {
		Channel      string
		Commands     []data.CommandStruct
		ExtraStrings string
		SinceTime    time.Time
		SinceTimeUTC string
		Leaving      bool
		HasLeft      bool
		SongData     []AugmentedSingsVideoStruct
		TopNSongs    []SongSings
		TopNSingers  []SingerSings
	}
	updateCacheIfNecessary(vars["channel"])
	channelData := globalData.ChannelData[vars["channel"]]
	topNSongs := calculateTopNSongs(channelData.VideoCache, 10)
	topNSingers := calculateTopNSingers(channelData.VideoCache, 10)
	var td = TemplateData{channelData.Name, channelData.Commands, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSliceForCSV(channelData.VideoCache), topNSongs, topNSingers}
	if request.URL.Path[0:4] == "/csv" {
		response.Header().Add("Content-Disposition", "attachment; filename=\"duets.csv\"")
		response.Header().Add("Content-type", "text/csv")
		tmpl := template.Must(template.ParseFiles("web/data.csv"))
		tmpl.Execute(response, td)
	} else {
		response.Header().Add("Content-Disposition", "attachment; filename=\"duets.tsv\"")
		response.Header().Add("Content-type", "text/tab-separated-values")
		tmpl := template.Must(template.ParseFiles("web/data.tsv"))
		tmpl.Execute(response, td)
	}
}

func JSONHandler(response http.ResponseWriter, request *http.Request) {
	vars := mux.Vars(request)
	if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey {
		fmt.Printf("%s != %s\n", vars["key"], globalData.ChannelData[vars["channel"]].AdminKey)
		UnauthorizedHandler(response, request)
		return
	}
	type TemplateData struct {
		Channel      string
		Commands     []data.CommandStruct
		ExtraStrings string
		SinceTime    time.Time
		SinceTimeUTC string
		Leaving      bool
		HasLeft      bool
		SongData     []AugmentedSingsVideoStruct
		TopNSongs    []SongSings
		TopNSingers  []SingerSings
	}
	updateCacheIfNecessary(vars["channel"])
	channelData := globalData.ChannelData[vars["channel"]]

	var topNSongs []SongSings
	var topNSingers []SingerSings

	if request.URL.Path[0:4] != "/deb" {
		topNSongs = calculateTopNSongs(channelData.VideoCache, 10)
		topNSingers = calculateTopNSingers(channelData.VideoCache, 10)
	}
	var td = TemplateData{channelData.Name, channelData.Commands, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSlice(channelData.VideoCache), topNSongs, topNSingers}
	response.Header().Add("Content-type", "application/json")
	if request.URL.Path[0:5] == "/json" {
		tmpl := template.Must(template.ParseFiles("web/data.json"))
		tmpl.Execute(response, td)
	} else if request.URL.Path[0:9] == "/topsongs" {
		tmpl := template.Must(template.ParseFiles("web/topsongs.json"))
		tmpl.Execute(response, td)
	} else { // top 10 singers!
		tmpl := template.Must(template.ParseFiles("web/topsingers.json"))
		tmpl.Execute(response, td)
	}
}

func ScriptHandler(response http.ResponseWriter, request *http.Request) {
	vars := mux.Vars(request)
	if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey {
		UnauthorizedHandler(response, request)
		return
	}
	type TemplateData struct {
		Channel      string
		Commands     []data.CommandStruct
		ExtraStrings string
		SinceTime    time.Time
		SinceTimeUTC string
		Leaving      bool
		HasLeft      bool
		SongData     []AugmentedSingsVideoStruct
		TopNSongs    []SongSings
		TopNSingers  []SingerSings
	}
	updateCacheIfNecessary(vars["channel"])
	channelData := globalData.ChannelData[vars["channel"]]
	topNSongs := calculateTopNSongs(channelData.VideoCache, 10)
	topNSingers := calculateTopNSingers(channelData.VideoCache, 10)
	var td = TemplateData{channelData.Name, channelData.Commands, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSliceForCSV(channelData.VideoCache), topNSongs, topNSingers}
	if request.URL.Path[0:11] == "/script.bat" {
		response.Header().Add("Content-Disposition", "attachment; filename=\"script.bat\"")
		response.Header().Add("Content-type", "application/x-bat")
		tmpl := template.Must(template.ParseFiles("web/script.bat"))
		tmpl.Execute(response, td)
	} else {
		response.Header().Add("Content-Disposition", "attachment; filename=\"script.sh\"")
		response.Header().Add("Content-type", "text/x-shellscript")
		tmpl := template.Must(template.ParseFiles("web/script.sh"))
		tmpl.Execute(response, td)
	}
}

func CacheDetailsHandler(response http.ResponseWriter, request *http.Request) {
	vars := mux.Vars(request)
	if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey {
		UnauthorizedHandler(response, request)
		return
	}
	deets := getCacheDetails(vars["channel"])
	response.Header().Add("Content-type", "application/json")
	enc := json.NewEncoder(response)
	enc.Encode(deets)
}

func BotDetailsHandler(response http.ResponseWriter, request *http.Request) {
	vars := mux.Vars(request)
	if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey {
		UnauthorizedHandler(response, request)
		return
	}
	type ChannelDataSmaller struct {
		Commands          []data.CommandStruct `json:"commands,omitempty"`
		ExtraStrings      string               `json:"extrastrings,omitempty"`
		JoinTime          time.Time            `json:"jointime"`
		HasLeft           bool                 `json:"hasleft"`
		VideoCacheUpdated time.Time            `json:"videoCacheUpdated"`
	}
	var deets ChannelDataSmaller
	deets.Commands = globalData.ChannelData[vars["channel"]].Commands
	deets.ExtraStrings = globalData.ChannelData[vars["channel"]].ExtraStrings
	deets.JoinTime = globalData.ChannelData[vars["channel"]].JoinTime
	deets.HasLeft = globalData.ChannelData[vars["channel"]].HasLeft
	deets.VideoCacheUpdated = globalData.ChannelData[vars["channel"]].VideoCacheUpdated
	response.Header().Add("Content-type", "application/json")
	enc := json.NewEncoder(response)
	enc.Encode(deets)
}

func ReactIndexHandler(entrypoint string) func(w http.ResponseWriter, r *http.Request) {
	fn := func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, entrypoint)
	}

	return http.HandlerFunc(fn)
}

func ForceRefreshHandler(response http.ResponseWriter, request *http.Request) {
	vars := mux.Vars(request)
	if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey {
		UnauthorizedHandler(response, request)
		return
	}
	forceUpdateCache(vars["channel"])
	response.Header().Add("Content-type", "application/json")
	enc := json.NewEncoder(response)
	enc.Encode(true)
}

func JoinHandler(response http.ResponseWriter, request *http.Request) {
	vars := mux.Vars(request)
	if vars["key"] != globalData.ChannelData[vars["channel"]].AdminKey {
		UnauthorizedHandler(response, request)
		return
	}
	globalData.UpdateJoined(vars["channel"], false)
	ircBot.JoinChannel(vars["channel"])
}

func HandleHTTP(passedIrcBot *irc.KardBot, passedGlobalData *data.GlobalData) {
	ircBot = passedIrcBot
	globalData = passedGlobalData
	r := mux.NewRouter()
	loggedRouter := handlers.LoggingHandler(os.Stdout, r)
	r.NotFoundHandler = http.HandlerFunc(NotFoundHandler)
	r.HandleFunc("/healthz", HealthHandler)
	r.HandleFunc("/web/{.*}", TemplateHandler)
	r.HandleFunc("/cachedeets/{channel}/{key}", CacheDetailsHandler)
	r.HandleFunc("/botdeets/{channel}/{key}", BotDetailsHandler)
	r.HandleFunc("/join/{channel}/{key}", JoinHandler)
	r.HandleFunc("/force/{channel}/{key}", ForceRefreshHandler)
	r.HandleFunc("/csv/{channel}/{key}", CSVHandler)
	r.HandleFunc("/tsv/{channel}/{key}", CSVHandler)
	r.HandleFunc("/json/{channel}/{key}", JSONHandler)
	r.HandleFunc("/debug/{channel}/{key}", JSONHandler)
	r.HandleFunc("/topsongs/{channel}/{key}", JSONHandler)
	r.HandleFunc("/topsingers/{channel}/{key}", JSONHandler)
	r.HandleFunc("/script.bat/{channel}/{key}", ScriptHandler)
	r.Path("/twitchtobackend").Queries("access_token", "{access_token}", "scope", "{scope}", "token_type", "{token_type}").HandlerFunc(TwitchBackendHandler)
	r.Path("/twitchadmin").Queries("code", "{code}", "scope", "{scope}").HandlerFunc(TwitchAdminHandler)
	r.PathPrefix("/static").Handler(http.StripPrefix("/static", http.FileServer(http.Dir("./web/react-frontend/static"))))
	r.PathPrefix("/").HandlerFunc(ReactIndexHandler("./web/react-frontend/index.html"))
	http.Handle("/", r)
	srv := &http.Server{
		Handler:      loggedRouter,
		Addr:         "0.0.0.0:5353",
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	fmt.Println("Listening on 0.0.0.0:5353")
	srv.ListenAndServe()
}