package main

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"image/gif"
	"log"
	"mime"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
)

var speedLabelRE = regexp.MustCompile(`^[0-9]+(\.[0-9]+)?x$`)

type app struct {
	root string
	py   string
}

type speedResponse struct {
	Source     string `json:"source"`
	Output     string `json:"output"`
	Label      string `json:"label"`
	DurationMS int    `json:"duration_ms"`
}

func main() {
	addr := flag.String("addr", "127.0.0.1:8080", "server address")
	root := flag.String("root", ".", "playground root")
	python := flag.String("python", ".venv/bin/python", "python executable")
	flag.Parse()

	absRoot, err := filepath.Abs(*root)
	if err != nil {
		log.Fatal(err)
	}

	a := &app{root: absRoot, py: *python}
	mux := http.NewServeMux()
	mux.HandleFunc("/", a.handleReport)
	mux.HandleFunc("/files/", a.handleFile)
	mux.HandleFunc("/api/speed", a.handleSpeed)
	mux.HandleFunc("/api/rebuild", a.handleRebuild)

	log.Printf("serving %s at http://%s", absRoot, *addr)
	log.Fatal(http.ListenAndServe(*addr, mux))
}

func (a *app) handleReport(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" && r.URL.Path != "/report.html" {
		http.NotFound(w, r)
		return
	}
	if err := a.rebuildReport(); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	servePath(w, r, filepath.Join(a.root, "report.html"))
}

func (a *app) handleFile(w http.ResponseWriter, r *http.Request) {
	rel := strings.TrimPrefix(r.URL.Path, "/files/")
	path, err := a.safePath(rel)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	servePath(w, r, path)
}

func (a *app) handleRebuild(w http.ResponseWriter, _ *http.Request) {
	if err := a.rebuildReport(); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	writeJSON(w, map[string]string{"status": "ok"})
}

func (a *app) handleSpeed(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost && r.Method != http.MethodGet {
		http.Error(w, "method must be GET or POST", http.StatusMethodNotAllowed)
		return
	}

	source, err := a.safePath(r.URL.Query().Get("path"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if filepath.Ext(source) != ".gif" {
		http.Error(w, "path must point to a .gif", http.StatusBadRequest)
		return
	}
	if strings.Contains(filepath.Base(source), "_speed_") {
		http.Error(w, "source must be an original gif, not a speed variant", http.StatusBadRequest)
		return
	}

	label := r.URL.Query().Get("label")
	if label == "" {
		label = r.URL.Query().Get("speed")
	}
	durationMS, err := durationFromRequest(label, r.URL.Query().Get("duration_ms"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if label == "" {
		label = fmt.Sprintf("%dms", durationMS)
	}
	if !speedLabelRE.MatchString(label) && !strings.HasSuffix(label, "ms") {
		http.Error(w, "label must look like 1x, 3x, or 50ms", http.StatusBadRequest)
		return
	}

	output := speedVariantPath(source, label)
	if err := rewriteGIFDuration(source, output, durationMS); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if err := a.rebuildReport(); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	relOutput, _ := filepath.Rel(a.root, output)
	writeJSON(w, speedResponse{
		Source:     mustRel(a.root, source),
		Output:     filepath.ToSlash(relOutput),
		Label:      label,
		DurationMS: durationMS,
	})
}

func (a *app) rebuildReport() error {
	cmd := exec.Command(a.py, "scripts/make_report.py")
	cmd.Dir = a.root
	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("rebuild report failed: %w\n%s", err, string(out))
	}
	return nil
}

func (a *app) safePath(rel string) (string, error) {
	if rel == "" {
		return "", errors.New("missing path")
	}
	if strings.HasPrefix(rel, "/") {
		return "", errors.New("path must be relative")
	}
	clean := filepath.Clean(filepath.FromSlash(rel))
	path := filepath.Join(a.root, clean)
	abs, err := filepath.Abs(path)
	if err != nil {
		return "", err
	}
	if abs != a.root && !strings.HasPrefix(abs, a.root+string(os.PathSeparator)) {
		return "", errors.New("path escapes root")
	}
	return abs, nil
}

func servePath(w http.ResponseWriter, r *http.Request, path string) {
	if ctype := mime.TypeByExtension(filepath.Ext(path)); ctype != "" {
		w.Header().Set("Content-Type", ctype)
	}
	http.ServeFile(w, r, path)
}

func writeJSON(w http.ResponseWriter, value any) {
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(value); err != nil {
		log.Printf("write json: %v", err)
	}
}

func durationFromRequest(label string, explicit string) (int, error) {
	if explicit != "" {
		duration, err := strconv.Atoi(explicit)
		if err != nil || duration < 1 {
			return 0, errors.New("duration_ms must be a positive integer")
		}
		return duration, nil
	}
	switch label {
	case "", "1x":
		return 50, nil
	case "3x":
		return 17, nil
	default:
		if strings.HasSuffix(label, "x") {
			speed, err := strconv.ParseFloat(strings.TrimSuffix(label, "x"), 64)
			if err != nil || speed <= 0 {
				return 0, errors.New("speed label must be positive, like 2x")
			}
			duration := int(50.0/speed + 0.5)
			if duration < 1 {
				duration = 1
			}
			return duration, nil
		}
		return 0, errors.New("provide label=1x/3x or duration_ms=N")
	}
}

func speedVariantPath(source string, label string) string {
	ext := filepath.Ext(source)
	stem := strings.TrimSuffix(source, ext)
	safeLabel := strings.ReplaceAll(label, ".", "p")
	return stem + "_speed_" + safeLabel + ext
}

func rewriteGIFDuration(source string, output string, durationMS int) error {
	handle, err := os.Open(source)
	if err != nil {
		return err
	}
	defer handle.Close()

	g, err := gif.DecodeAll(handle)
	if err != nil {
		return err
	}
	delay := durationMS / 10
	if delay < 1 {
		delay = 1
	}
	for i := range g.Delay {
		g.Delay[i] = delay
	}

	out, err := os.Create(output)
	if err != nil {
		return err
	}
	defer out.Close()
	return gif.EncodeAll(out, g)
}

func mustRel(root string, path string) string {
	rel, err := filepath.Rel(root, path)
	if err != nil {
		return path
	}
	return filepath.ToSlash(rel)
}
