package main

import (
	"bufio"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"text/template"
	"time"
)

const (
	statsURL     = "https://covid19stats.alberta.ca"
	cacheTimeout = time.Hour * 8
)

var (
	cache *Cache
	t     *template.Template // HTML Templates used for web mode.  Initialized as a null pointer has near zero overhead when cli mode is run
)

// ABGovCovidData stores the Javascript object which is ripped from the statsURL.  It's parsed as JSON encoded data.
type ABGovCovidData [][]string

// They screwed this up so much, the first index in the column, not the row.  WTF alberta
// [0] = case number alberta
// [1] = date
// [2] = geo zone
// [3] = gender
// [4] = age range
// [5] = case status
// [6] = confirmed/suspected

// Cache keeps a local copy of the data in memory.  The cache will only update if a web request comes in, not on a fixed timer.
// This is to ensure reduced load on Alberta's servers and to ensure a quick response when possible, and err on the side of caution when not possible.
type Cache struct {
	Data struct {
		ActiveCasesEdmonton int
		TotalCasesEdmonton  int
		ActiveCasesAlberta  int
		TotalCasesAlberta   int
	}
	UpdatedDate time.Time
}

func main() {
	webMode := flag.Bool("web", false, "listen on port 8080 as web server")
	flag.Parse()
	if *webMode {
		// Setup a web server
		stop := make(chan os.Signal, 1)
		signal.Notify(stop, os.Interrupt)
		// Initial load of data
		go func() {
			for {
				var err error
				cache, err = getUpdatedData()
				if err != nil {
					log.Printf("cache is empty and failed to download data from ab website: %v", err)
					fmt.Println("sleeping for 2 hours")
					time.Sleep(time.Hour * 2)
					continue
				}
				break
			}
		}()
		// Web server routing
		go func() {
			http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("public"))))
			http.HandleFunc("/", homePageHandler)
			log.Fatal(http.ListenAndServe(":8080", nil))
		}()
		log.Println("started covid19-edmonton server on 8080")

		// Parse all template files at startup
		var err error
		t, err = template.ParseGlob("./templates/*")
		if err != nil {
			log.Fatalf("couldn't parse HTML templates in templates directory: %v", err)
		}

		<-stop
		log.Println("shutting covid19-edmonton server down")
	} else {
		var err error
		cache, err = getUpdatedData()
		if err != nil {
			log.Fatalf("failed to get data from alberta government website: %v", err)
		}
		fmt.Printf("As of %s Mountain Time (the real time)\n\n", cache.UpdatedDate.Format("2006 Jan 02 15:04:05"))
		// CLI mode, print output to stdout
		fmt.Println("Edmonton Active: ", cache.Data.ActiveCasesEdmonton)
		fmt.Println("Edmonton Total: ", cache.Data.TotalCasesEdmonton)
		fmt.Println("Alberta Active: ", cache.Data.ActiveCasesAlberta)
		fmt.Println("Alberta Total: ", cache.Data.TotalCasesAlberta)

		log.Print("\n\nPress 'Enter' to continue...")
		bufio.NewReader(os.Stdin).ReadBytes('\n')
	}
}

// getUpdatedData reaches out to Alberta's website to get the most recent data and returns a pointer to a cache and an error
func getUpdatedData() (*Cache, error) {
	log.Printf("getting up to date data from AB website")
	// Download the latest stats
	resp, err := http.Get(statsURL)
	if err != nil {
		return nil, fmt.Errorf("failed to download stats page '%s': %v", statsURL, err)
	}
	defer resp.Body.Close()

	bodyB, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read stats page response body: %v", err)
	}

	// Parse the HTML for the data

	// Find the beginning, trim off everything before
	split := strings.Split(string(bodyB), "\"data\":[[\"1\",")
	body := fmt.Sprintf("%s%s", "[[\"1\",", split[1])

	// Find the end, trim off everything after
	split = strings.Split(body, "]]")
	body = fmt.Sprintf("%s%s", split[0], "]]")

	// Parse the json string into a struct
	data := ABGovCovidData{}
	err = json.Unmarshal([]byte(body), &data)
	if err != nil {
		return nil, fmt.Errorf("failed to parse data as json: %v", err)
	}

	cache := &Cache{UpdatedDate: time.Now()}

	// count the cases
	for i := range data[2] {
		if data[2][i] == "Edmonton Zone" {
			cache.Data.TotalCasesEdmonton++
			if data[5][i] == "Active" {
				cache.Data.ActiveCasesEdmonton++
			}
		}
		if data[5][i] == "Active" {
			cache.Data.ActiveCasesAlberta++
		}
		cache.Data.TotalCasesAlberta++
	}

	return cache, nil
}

// Web Mode only: HTTP GET /
func homePageHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		if cache == nil {
			w.WriteHeader(http.StatusFailedDependency)
			log.Printf("unable to serve page due to no cached data, possibly due to application still starting up, or AB government site down, or changed formatting - may need to review how page is parsed")
			return
		}
		if time.Now().After(cache.UpdatedDate.Add(cacheTimeout)) {
			w.WriteHeader(http.StatusOK)
			tempCache, err := getUpdatedData() // Hold tempCache in case there's an error, we don't want to nullify our pointer to a working cache that has aged.  We will proceed with aged data.
			if err != nil {
				log.Printf("failed to update cache: %v", err)
			} else {
				cache = tempCache // we retrieved it successfully, so overwrite our old data
			}
		}

		err := t.ExecuteTemplate(w, "index.html", cache)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			log.Fatalf("failed to execute index.html template: %v", err)
		}

	} else {
		w.WriteHeader(http.StatusMethodNotAllowed)
	}
}
