// Copyright (C) 2025 Marius Schellenberger

package main

import (
	"cmp"
	"errors"
	"flag"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"net/http"
	"os"
	"strconv"
	"sync"
	"time"

	"git.giftfish.de/ston1th/godrop/v2"
)

var (
	version string
	listen  string
)

const bfsURL = "https://uvi.bfs.de/Tagesgrafiken"

var allowedFiles = []string{
	"/etc/resolv.conf",
	"/etc/localtime",
	"/etc/hosts",
	"/etc/ssl/cert.pem",
}

func main() {
	flag.StringVar(&listen, "l", ":7878", "listen addr")
	flag.Parse()

	if len(cityList) != len(fileList) {
		fatal(errors.New("city and file list length differ"))
	}
	if err := godrop.PledgePromises("stdio inet rpath unveil"); err != nil {
		fatal(err)
	}
	for _, file := range allowedFiles {
		if err := godrop.Unveil(file, "r"); err != nil {
			fatal(err)
		}
	}
	if err := godrop.UnveilBlock(); err != nil {
		fatal(err)
	}

	ic := &ImageCrawler{
		url: bfsURL,
		c:   &http.Client{},
		m:   make(map[int]*Image),
	}
	http.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "POST" {
			r.ParseForm()
			loc := r.PostForm.Get("loc")
			_, ok := locFromString(loc)
			if !ok {
				http.Redirect(w, r, "/", http.StatusMovedPermanently)
				return
			}
			setCookie(w, loc)
			http.Redirect(w, r, "/", http.StatusMovedPermanently)
			return
		}
		w.Write([]byte(locPage))
	})
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		cc := r.CookiesNamed("loc")
		var c *http.Cookie
		if len(cc) > 0 {
			c = cc[0]
		} else {
			c = setCookie(w, DefaultCity)
		}
		num, ok := locFromString(c.Value)
		if !ok {
			writeError(w, "Error", "Error")
			return
		}
		img, mod, err := ic.getImage(num)
		mod = mod.Local()
		location := cityList[num]
		if err != nil {
			writeError(w, mod.Format(time.RFC1123), location)
			return
		}
		vals := getUV(img)
		if len(vals) == 0 {
			writeError(w, mod.Format(time.RFC1123), location)
			return
		}
		last := vals[len(vals)-1]
		imgURL := bfsURL + "/" + fileList[num]
		fmt.Fprintf(w, indexPage,
			getColor(last),
			fmt.Sprintf("%.2f", last),
			mod.Format(time.RFC1123),
			time.Now().Format(time.RFC1123),
			location,
			imgURL,
			version,
		)
	})
	err := http.ListenAndServe(listen, nil)
	if err != nil {
		fatal(err)
	}
}

func setCookie(w http.ResponseWriter, loc string) *http.Cookie {
	c := &http.Cookie{
		Name:     "loc",
		Path:     "/",
		Value:    loc,
		Secure:   true,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
	}
	http.SetCookie(w, c)
	return c
}

func writeError(w http.ResponseWriter, mod, location string) {
	fmt.Fprintf(w, indexPage,
		"000000",
		"Error",
		mod,
		time.Now().Format(time.RFC1123),
		location,
		version,
	)
}

func fatal(err error) {
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

type ImageCrawler struct {
	mu  sync.RWMutex
	url string
	c   *http.Client
	m   map[int]*Image
}

type Image struct {
	mu       sync.RWMutex
	ic       *ImageCrawler
	file     string
	img      image.Image
	etag     string
	modified time.Time
	lastGet  time.Time
}

func (ic *ImageCrawler) getImage(loc int) (img image.Image, mod time.Time, err error) {
	file := fileList[loc]
	ic.mu.RLock()
	if i, ok := ic.m[loc]; ok {
		ic.mu.RUnlock()
		return i.getImage()
	}
	ic.mu.RUnlock()
	ic.mu.Lock()
	i := &Image{
		ic:   ic,
		file: file,
	}
	ic.m[loc] = i
	ic.mu.Unlock()
	return i.getImage()
}

func (i *Image) getImage() (img image.Image, mod time.Time, err error) {
	i.mu.RLock()
	if !i.lastGet.IsZero() &&
		time.Now().Sub(i.lastGet) < time.Minute*3 {
		defer i.mu.RUnlock()
		return i.img, i.modified, nil
	}
	i.mu.RUnlock()
	req, err := http.NewRequest("GET", i.ic.url+"/"+i.file, nil)
	if err != nil {
		return
	}
	req.Header.Set("user-agent", "uvgo/"+version)
	i.mu.RLock()
	if !i.modified.IsZero() {
		req.Header.Set("if-modified-since", i.modified.Format(time.RFC1123))
	}
	if i.etag != "" {
		req.Header.Set("if-none-match", i.etag)
	}
	i.mu.RUnlock()
	resp, err := i.ic.c.Do(req)
	if err != nil {
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNotModified {
		i.mu.RLock()
		defer i.mu.RUnlock()
		return i.img, i.modified, nil
	}
	if resp.StatusCode == http.StatusOK {
		img, err = png.Decode(resp.Body)
		if err != nil {
			return
		}
		i.mu.Lock()
		i.img = img
		modTime, err := time.Parse(time.RFC1123, resp.Header.Get("last-modified"))
		if err == nil {
			i.modified = modTime
		}
		i.etag = resp.Header.Get("etag")
		i.lastGet = time.Now()
		defer i.mu.Unlock()
		return i.img, i.modified, nil
	}
	i.mu.RLock()
	defer i.mu.RUnlock()
	if i.img != nil {
		return i.img, i.modified, nil
	}
	return nil, time.Time{}, errors.New("error getting image")
}

const (
	X        = 82
	maxX     = 574
	maxStart = 150
	Y        = 431
)

func scale(img image.Image) int {
	startY := Y
	for y := Y; y > 0; y-- {
		col := img.At(X, y)
		if !isBlack(col) {
			startY = y
			break
		}
	}
	c := 0
	for y := startY; y > 0; y-- {
		col := img.At(X, y)
		if isWhite(col) {
			c = startY - y
			startY = y
			break
		}
	}
	for y := startY; y > 0; y-- {
		col := img.At(X, y)
		if !isWhite(col) {
			return (startY + c) - y
		}
	}
	return -1
}

func start(img image.Image) int {
	for x := X; x < maxStart; x++ {
		col := img.At(x, Y)
		if !isStart(col) {
			return x
		}
	}
	return -1
}

func uv(img image.Image, x, s int) float64 {
	start := Y
	end := Y
	for y := start; y > 0; y-- {
		col := img.At(x, y)
		if isBlack(col) || isWhite(col) {
			end = y - 1
			break
		}
	}
	v := start - end
	if v <= s {
		return float64(v) / float64(s)
	}
	return float64(v+s) / 2 / float64(s)
}

func getUV(img image.Image) (vals []float64) {
	s := scale(img)
	if s == -1 {
		return
	}
	x := start(img)
	if x == -1 {
		return
	}
	for ; x < maxX; x++ {
		col := img.At(x, Y)
		if isStop(col) {
			break
		}
		vals = append(vals, uv(img, x, s))
	}
	return
}

func between[T cmp.Ordered](v, min, max T) bool {
	return v >= min && v <= max
}

func getColor(v float64) string {
	switch {
	case between(v, 0, 1):
		return "006300"
	case between(v, 1, 2):
		return "00a014"
	case between(v, 2, 3):
		return "81c600"
	case between(v, 3, 4):
		return "fff800"
	case between(v, 4, 5):
		return "ffd100"
	case between(v, 5, 6):
		return "ffa600"
	case between(v, 6, 7):
		return "ff7200"
	case between(v, 7, 8):
		return "ff4a00"
	case between(v, 8, 9):
		return "ff0000"
	case between(v, 9, 10):
		return "ff0078"
	case v >= 10:
		return "a80080"
	}
	return "000000"
}

func colorIs(c color.RGBA, v uint8) bool {
	return c.R == v && c.G == v && c.B == v
}

func isWhite(col color.Color) bool {
	col = color.RGBAModel.Convert(col)
	c, ok := col.(color.RGBA)
	if !ok {
		return false
	}
	return colorIs(c, 255)
}

func isBlack(col color.Color) bool {
	col = color.RGBAModel.Convert(col)
	c, ok := col.(color.RGBA)
	if !ok {
		return false
	}
	return colorIs(c, 0)
}

func isStop(col color.Color) bool {
	col = color.RGBAModel.Convert(col)
	c, ok := col.(color.RGBA)
	if !ok {
		return false
	}
	if colorIs(c, 241) || colorIs(c, 88) {
		return true
	}
	return false
}

func isStart(col color.Color) bool {
	col = color.RGBAModel.Convert(col)
	c, ok := col.(color.RGBA)
	if !ok {
		return false
	}
	if colorIs(c, 241) || colorIs(c, 214) || colorIs(c, 0) {
		return true
	}
	return false
}

// DefaultCity is Muenchen
// Array index 24
const DefaultCity = "24"

var fileList = []string{
	"EEr_Andernach_today.png",
	"EEr_Berlin_Potsdamer_Platz_today.png",
	"EEr_Bonn_today.png",
	"EEr_Boesel_B_today.png",
	"EEr_Chieming_today.png",
	"EEr_Cuxhaven_B_today.png",
	"EEr_Dortmund_today.png",
	"EEr_Duderstadt_B_today.png",
	"EEr_Eckernfoerde_today.png",
	"EEr_Fichtelberg_today.png",
	"EEr_Friedrichshafen_today.png",
	"EEr_Genthin_today.png",
	"EEr_Giessen-Wettenberg_today.png",
	"EEr_Goerlitz_today.png",
	"EEr_Groemitz_today.png",
	"EEr_Hamburg_today.png",
	"EEr_Hohenpeissenberg_today.png",
	"EEr_Kassel_today.png",
	"EEr_Klippeneck_today.png",
	"EEr_Kulmbach_today.png",
	"EEr_Langen_today.png",
	"EEr_Lindenberg_today.png",
	"EEr_Lueneburg_today.png",
	"EEr_Melpitz_today.png",
	"EEr_Muenchen_today.png",
	"EEr_Norderney_today.png",
	"EEr_Belm-Osnabrueck_today.png",
	"EEr_Salzgitter_today.png",
	"EEr_Sankt Augustin_today.png",
	"EEr_Schauinsland_today.png",
	"EEr_Schweinfurt_Wasserlosen_today.png",
	"EEr_Stuttgart_today.png",
	"EEr_Sylt_Tinnum_today.png",
	"EEr_Tholey_today.png",
	"EEr_Todendorf_today.png",
	"EEr_Waldhof_Falkenstein_today.png",
	"EEr_Waldmuenchen_today.png",
	"EEr_Wasserkuppe bei Fulda_today.png",
	"EEr_Weissenburg_today.png",
	"EEr_Wurmberg_B_today.png",
	"EEr_Zingst_today.png",
	"EEr_Zirchow_today.png",
	"EEr_Schneefernerhaus_today.png",
}

func locFromString(s string) (i int, ok bool) {
	i, err := strconv.Atoi(s)
	if err != nil {
		return
	}
	return i, validLoc(i)
}

func validLoc(loc int) bool {
	return loc >= 0 && loc < len(cityList)
}

var cityList = []string{
	"Andernach",
	"Berlin",
	"Bonn",
	"Bösel",
	"Chieming",
	"Cuxhaven",
	"Dortmund",
	"Duderstadt",
	"Eckernförde",
	"Fichtelberg",
	"Friedrichshafen",
	"Genthin",
	"Gießen",
	"Görlitz",
	"Grömitz",
	"Hamburg",
	"Hohenpeißenberg",
	"Kassel",
	"Klippeneck",
	"Kulmbach",
	"Langen",
	"Lindenberg",
	"Lüneburg",
	"Melpitz",
	"München",
	"Norderney",
	"Osnabrück",
	"Salzgitter",
	"Sankt Augustin",
	"Schauinsland",
	"Schweinfurt",
	"Stuttgart",
	"Sylt (Westerland)",
	"Tholey",
	"Todendorf",
	"Waldhof",
	"Waldmünchen",
	"Wasserkuppe",
	"Weißenburg",
	"Wurmberg",
	"Zingst",
	"Zirchow",
	"Zugspitze (Schneefernerhaus)",
}

func generateLocPage() string {
	loc := `<!doctype html><html>
<head>
<title>UV Index Change Location</title>
<meta name="theme-color" content="#d6d6d6">
</head>
<body style="background-color:#d6d6d6;">
<center>
<h5 style="font-size:4em;">Change Location</h5>
<form action="/set" method="post">
<select name="loc" style="font-size:5em;margin-bottom:50px;">`
	for i, v := range cityList {
		loc += `<option value="` + strconv.Itoa(i) + `">` + v + `</option>`
	}
	loc += `</select><br><input type="submit" style="font-size:5em;" value="Change"></input></form></center></body></html>`
	return loc
}

var locPage = generateLocPage()
var indexPage = `<!doctype html>
<html>
<head>
<title>UV Index</title>
<style>
a, a:link, a:visited {
  color: #007d9c;
}
</style>
<meta name="theme-color" content="#d6d6d6">
</head>
<body style="background-color:#d6d6d6;">
<center>
<h5 style="font-size:3.5em;">UV Index</h5>
<h1 style="font-size:15em;color:#%s;">%s</h1>
<h5 style="font-size:2em;">Updated: %s</h5>
<h5 style="font-size:2em;">Server: %s</h5>
<h5 style="font-size:2em;"><a href="/set">Change Location</a>: %s</h5>
<h5 style="font-size:2em;"><a href="%s" rel="nofollow noreferrer noopener" target="_blank">View Image</a></h5>
<footer>UV data copyright belongs to <a href="https://www.bfs.de/DE/themen/opt/uv/uv-index/aktuelle-tagesverlaeufe/aktuell_node.html" rel="nofollow noreferrer noopener" target="_blank">Bundesamt für Strahlenschutz</a><br>
Use this service at your own risk.<br>
I take no responsibility for missing or incorrect data.<br>
© <a href="https://git.giftfish.de/ston1th/uvgo" rel="nofollow noreferrer noopener" target="_blank">uvgo</a> v%s<br><br>
</footer>
</center>
</body>
</html>`
