package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"flag"
	"fmt"
	"io/fs"
	"log"
	"net"
	"net/http"
	"os"
	"regexp"
	"sort"
	"strings"
	"sync"
)

var (
	baseDir        = flag.String("base_dir", "/store/emf/2024/video/output/", "base directory for output")
	httpListen     = flag.String("http_listen", "", "TCP address to listen&serve")
	httpListenUNIX = flag.String("http_listen_unix", "", "UNIX socket path to listen&serve")

	computeForBase = flag.String("compute_base", "https://prerelease.voc.emf.camp/", "base URL to prepend when computing a secret")
	computeFor     = flag.String("compute", "", "something to compute the secret for")

	list = flag.Bool("list", false, "list the available content")

	devMode = flag.Bool("dev_mode", false, "enable insecure dev mode")
)

func computeSignature(content, secret string) string {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(content))
	return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(mac.Sum(nil))
}

func validateSignature(content, secret, signature string) bool {
	computed := computeSignature(content, secret)
	if *devMode {
		log.Printf("got signature %q; computed signature %q", signature, computed)
	}
	return subtle.ConstantTimeCompare([]byte(computed), []byte(signature)) == 1
}

const (
	talkRegexFragment    = `[0-9]+(_[\p{L}\p{N}\-]+)?`
	fileEndRegexFragment = `[0-9]{8}-[0-9]{6}[.]mp4`
)

var (
	validContentRegex = regexp.MustCompile("^" + talkRegexFragment + "$")
	validFileRegex    = regexp.MustCompile("^" + fileEndRegexFragment + "$")
	filenameRegex     = regexp.MustCompile(`^(` + talkRegexFragment + ")-(" + fileEndRegexFragment + ")$")
)

func logResultFactory(r *http.Request, what string) func(string, ...any) {
	return func(f string, bits ...any) {
		log.Printf("%s{%s}: %s %s - %s", what, r.RemoteAddr, r.Method, r.RequestURI, fmt.Sprintf(f, bits...))
	}
}

func main() {
	flag.Parse()

	contentFS := os.DirFS(*baseDir)

	secret := os.Getenv("EMFMINISERV_SECRET")
	if secret == "" {
		if !*devMode {
			log.Fatal("set EMFMINISERV_SECRET or -dev_mode")
		}
		secret = "testing-do-not-use-in-production"
		log.Printf("secret is set to %q! development mode only.", secret)
	}

	if *httpListen == "" && *httpListenUNIX == "" && *computeFor == "" && !*list {
		log.Printf("need -http_listen, -http_listen_unix, -compute or -list")
		os.Exit(1)
	}

	if *list {
		matches, err := fs.Glob(contentFS, "*.mp4")
		if err != nil {
			log.Fatalf("globbing for *.mp4: %v", err)
		}

		available := map[string]bool{}

		for _, m := range matches {
			bits := filenameRegex.FindStringSubmatch(m)
			if bits == nil {
				continue
			}
			available[bits[1]] = true
		}

		availableKeys := make([]string, 0, len(available))
		for k := range available {
			availableKeys = append(availableKeys, k)
		}
		sort.Strings(availableKeys)

		for _, k := range availableKeys {
			fmt.Printf("%s\n", k)
		}

		os.Exit(0)
	}

	if *computeFor != "" {
		sig := computeSignature(*computeFor, secret)
		fmt.Printf("%s%s/%s/\n", *computeForBase, *computeFor, sig)
		os.Exit(0)
	}

	http.HandleFunc("/{content}/{signature}/{file}", func(rw http.ResponseWriter, r *http.Request) {
		logResult := logResultFactory(r, "contentget")

		if r.Method != "GET" {
			logResult("method not allowed")
			http.Error(rw, "GET only", http.StatusMethodNotAllowed)
			return
		}

		log.Printf("contentget{%s}: %s %s", r.RemoteAddr, r.Method, r.RequestURI)

		content := r.PathValue("content")
		file := r.PathValue("file")
		if !validContentRegex.MatchString(content) || !validFileRegex.MatchString(file) {
			logResult("404 invalid content or file segment")
			http.NotFound(rw, r)
			return
		}
		if !validateSignature(content, secret, r.PathValue("signature")) {
			logResult("404 invalid signature")
			http.NotFound(rw, r)
			return
		}

		rw.Header().Set("x-accel-redir", fmt.Sprintf("/%s-%s", content, file))
		logResult("200 OK")
	})
	http.HandleFunc("/{content}/{signature}/{$}", func(rw http.ResponseWriter, r *http.Request) {
		logResult := logResultFactory(r, "contentdir")

		if r.Method != "GET" {
			logResult("method not allowed")
			http.Error(rw, "GET only", http.StatusMethodNotAllowed)
			return
		}

		content := r.PathValue("content")
		if !validContentRegex.MatchString(content) {
			logResult("404 invalid content segment")
			http.NotFound(rw, r)
			return
		}
		if !validateSignature(content, secret, r.PathValue("signature")) {
			logResult("404 invalid signature")
			http.NotFound(rw, r)
			return
		}

		matches, err := fs.Glob(contentFS, fmt.Sprintf("%s-*", content))
		if err != nil {
			logResult("500 globbing with %s-*: %v", content, err)
			http.Error(rw, "internal server error finding content", http.StatusInternalServerError)
			return
		}
		sort.Strings(matches)

		unprefixed := make([]string, len(matches))
		for n, m := range matches {
			unprefixed[n] = strings.TrimPrefix(m, fmt.Sprintf("%s-", content))
		}

		rw.Header().Set("Content-type", "text/html; encoding=utf-8")

		fmt.Fprintf(rw, "<ul>")
		for _, m := range unprefixed {
			fmt.Fprintf(rw, `<li><a href="%s">%s</a></li>`, m, m)
		}
		fmt.Fprintf(rw, "</ul>")
		logResult("200 OK")
	})

	var wg sync.WaitGroup
	if *httpListen != "" {
		wg.Add(1)
		go func() {
			defer wg.Done()
			log.Fatal(http.ListenAndServe(*httpListen, nil))
		}()
	}
	if *httpListenUNIX != "" {
		wg.Add(1)
		go func() {
			defer wg.Done()
			l, err := net.Listen("unix", *httpListenUNIX)
			if err != nil {
				log.Fatalf("listening on unix:%v: %v", *httpListenUNIX, err)
			}
			defer l.Close()

			if err := os.Chmod(*httpListenUNIX, 0770); err != nil {
				log.Fatalf("chmodding unix:%v: %v", *httpListenUNIX, err)
			}

			log.Fatal(http.Serve(l, nil))
		}()
	}
	wg.Wait()
}