// Package web stellt das Upload- und Sortier-Interface bereit.
package web

import (
	"crypto/sha1"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"ehc/tvwall/internal/config"
	"ehc/tvwall/internal/player"
	"ehc/tvwall/internal/playlist"
)

// Server buendelt alles, was die Handler brauchen.
type Server struct {
	cfg      *config.Config
	pl       *playlist.Manager
	player   *player.Player
	settings *config.Store

	thumbMu sync.Mutex
	hasFF   bool
}

// New erzeugt den Webserver.
func New(cfg *config.Config, pl *playlist.Manager, pp *player.Player, settings *config.Store) *Server {
	s := &Server{cfg: cfg, pl: pl, player: pp, settings: settings}
	if _, err := lookPath("ffmpeg"); err == nil {
		s.hasFF = true
	} else {
		log.Printf("web: ffmpeg nicht gefunden - Vorschaubilder fuer Videos sind deaktiviert")
	}
	return s
}

// Handler liefert den fertig verdrahteten HTTP-Handler.
func (s *Server) Handler() http.Handler {
	mux := http.NewServeMux()

	mux.HandleFunc("/", s.handleIndex)
	mux.Handle("/static/", http.StripPrefix("/static/", staticHandler()))
	mux.HandleFunc("/api/state", s.handleState)
	mux.HandleFunc("/api/upload", s.handleUpload)
	mux.HandleFunc("/api/order", s.handleOrder)
	mux.HandleFunc("/api/delete", s.handleDelete)
	mux.HandleFunc("/api/enabled", s.handleEnabled)
	mux.HandleFunc("/api/settings", s.handleSettings)
	mux.HandleFunc("/api/control", s.handleControl)
	mux.HandleFunc("/thumb/", s.handleThumb)
	mux.HandleFunc("/media/", s.handleMedia)
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		io.WriteString(w, "ok\n")
	})

	return s.withAuth(mux)
}

// withAuth schuetzt alles per HTTP-Basic-Auth, sofern ein Passwort gesetzt ist.
func (s *Server) withAuth(next http.Handler) http.Handler {
	if s.cfg.WebPassword == "" {
		return next
	}
	want := []byte(s.cfg.WebPassword)
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/healthz" {
			next.ServeHTTP(w, r)
			return
		}
		_, pass, ok := r.BasicAuth()
		if !ok || subtle.ConstantTimeCompare([]byte(pass), want) != 1 {
			w.Header().Set("WWW-Authenticate", `Basic realm="TV-Wall"`)
			http.Error(w, "Anmeldung erforderlich", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// ---------------------------------------------------------------------------
// Handler
// ---------------------------------------------------------------------------

func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	writeAsset(w, "index.html")
}

type stateResponse struct {
	Entries  []playlist.Entry `json:"entries"`
	Version  uint64           `json:"version"`
	Status   player.Status    `json:"status"`
	Settings config.Settings  `json:"settings"`
	Device   deviceInfo       `json:"device"`
	Disk     diskInfo         `json:"disk"`
}

type deviceInfo struct {
	SSID      string `json:"ssid"`
	Hostname  string `json:"hostname"`
	Images    int    `json:"images"`
	Videos    int    `json:"videos"`
	Active    int    `json:"active"`
	Thumbs    bool   `json:"thumbs"`
	MaxUpload int64  `json:"max_upload_mb"`
}

func (s *Server) handleState(w http.ResponseWriter, r *http.Request) {
	entries := s.pl.Entries()
	var images, videos, active int
	for _, e := range entries {
		if e.Kind == "video" {
			videos++
		} else {
			images++
		}
		if e.Enabled {
			active++
		}
	}
	if entries == nil {
		entries = []playlist.Entry{}
	}
	free, total := diskUsage(s.cfg.MediaDir())

	writeJSON(w, http.StatusOK, stateResponse{
		Entries:  entries,
		Version:  s.pl.Version(),
		Status:   s.player.Status(),
		Settings: s.settings.Get(),
		Device: deviceInfo{
			SSID:      s.cfg.APSSID,
			Hostname:  s.cfg.Hostname,
			Images:    images,
			Videos:    videos,
			Active:    active,
			Thumbs:    s.hasFF,
			MaxUpload: s.cfg.MaxUploadMB,
		},
		Disk: diskInfo{Free: free, Total: total},
	})
}

type uploadResult struct {
	Uploaded []string `json:"uploaded"`
	Skipped  []string `json:"skipped"`
}

func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		httpError(w, http.StatusMethodNotAllowed, "nur POST")
		return
	}
	maxBytes := s.cfg.MaxUploadMB << 20
	r.Body = http.MaxBytesReader(w, r.Body, maxBytes)

	mr, err := r.MultipartReader()
	if err != nil {
		httpError(w, http.StatusBadRequest, "kein gueltiger Upload: "+err.Error())
		return
	}

	res := uploadResult{Uploaded: []string{}, Skipped: []string{}}
	for {
		part, err := mr.NextPart()
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			httpError(w, http.StatusBadRequest, "Upload abgebrochen: "+err.Error())
			return
		}
		name := playlist.CleanUploadName(part.FileName())
		if name == "" {
			part.Close()
			continue
		}
		if !playlist.UploadAllowed(name) {
			res.Skipped = append(res.Skipped, name+" (Dateityp wird nicht unterstuetzt)")
			part.Close()
			continue
		}
		if playlist.Convertible(name) && !s.hasFF {
			res.Skipped = append(res.Skipped, name+" (HEIC braucht ffmpeg auf dem Pi)")
			part.Close()
			continue
		}
		saved, err := s.saveUpload(part, name)
		part.Close()
		if err != nil {
			log.Printf("web: upload %q: %v", name, err)
			res.Skipped = append(res.Skipped, name+" ("+err.Error()+")")
			continue
		}
		res.Uploaded = append(res.Uploaded, saved)
	}

	if len(res.Uploaded) > 0 {
		if _, err := s.pl.Scan(); err != nil {
			log.Printf("web: scan nach upload: %v", err)
		}
	}
	writeJSON(w, http.StatusOK, res)
}

// saveUpload schreibt den Datenstrom in eine temporaere Datei und benennt sie
// erst nach vollstaendigem Empfang um - so landen keine halben Dateien in der
// Playliste.
func (s *Server) saveUpload(part *multipart.Part, name string) (string, error) {
	dir := s.pl.MediaDir()
	if err := os.MkdirAll(dir, 0o775); err != nil {
		return "", err
	}
	tmp, err := os.CreateTemp(dir, ".upload-*")
	if err != nil {
		return "", err
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)

	if _, err := io.Copy(tmp, part); err != nil {
		tmp.Close()
		return "", err
	}
	if err := tmp.Close(); err != nil {
		return "", err
	}
	// iPhone-Fotos kommen als HEIC - daraus wird ein JPEG, das mpv anzeigen kann.
	if playlist.Convertible(name) {
		converted := tmpName + ".jpg"
		if err := convertToJPEG(tmpName, converted); err != nil {
			return "", fmt.Errorf("HEIC umwandeln: %w", err)
		}
		defer os.Remove(converted)
		tmpName = converted
		name = strings.TrimSuffix(name, filepath.Ext(name)) + ".jpg"
	}

	if err := os.Chmod(tmpName, 0o664); err != nil {
		return "", err
	}

	final := playlist.UniqueName(dir, name)
	if err := os.Rename(tmpName, filepath.Join(dir, final)); err != nil {
		return "", err
	}
	return final, nil
}

func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		httpError(w, http.StatusMethodNotAllowed, "nur POST")
		return
	}
	var req struct {
		Order []string `json:"order"`
	}
	if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil {
		httpError(w, http.StatusBadRequest, "ungueltige Anfrage")
		return
	}
	if err := s.pl.SetOrder(req.Order); err != nil {
		httpError(w, http.StatusInternalServerError, err.Error())
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}

func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		httpError(w, http.StatusMethodNotAllowed, "nur POST")
		return
	}
	var req struct {
		Name string `json:"name"`
	}
	if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil {
		httpError(w, http.StatusBadRequest, "ungueltige Anfrage")
		return
	}
	if err := s.pl.Delete(req.Name); err != nil {
		httpError(w, http.StatusBadRequest, err.Error())
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}

func (s *Server) handleEnabled(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		httpError(w, http.StatusMethodNotAllowed, "nur POST")
		return
	}
	var req struct {
		Name    string `json:"name"`
		Enabled bool   `json:"enabled"`
	}
	if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil {
		httpError(w, http.StatusBadRequest, "ungueltige Anfrage")
		return
	}
	if err := s.pl.SetEnabled(req.Name, req.Enabled); err != nil {
		httpError(w, http.StatusBadRequest, err.Error())
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}

func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		httpError(w, http.StatusMethodNotAllowed, "nur POST")
		return
	}
	cur := s.settings.Get()
	var req struct {
		ImageDuration *float64 `json:"image_duration"`
		Mute          *bool    `json:"mute"`
	}
	if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil {
		httpError(w, http.StatusBadRequest, "ungueltige Anfrage")
		return
	}
	if req.ImageDuration != nil {
		cur.ImageDuration = *req.ImageDuration
	}
	if req.Mute != nil {
		cur.Mute = *req.Mute
	}
	saved, err := s.settings.Set(cur)
	if err != nil {
		httpError(w, http.StatusInternalServerError, err.Error())
		return
	}
	writeJSON(w, http.StatusOK, saved)
}

func (s *Server) handleControl(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		httpError(w, http.StatusMethodNotAllowed, "nur POST")
		return
	}
	var req struct {
		Action string `json:"action"`
	}
	if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil {
		httpError(w, http.StatusBadRequest, "ungueltige Anfrage")
		return
	}

	var err error
	switch req.Action {
	case "next":
		err = s.player.Next()
	case "prev":
		err = s.player.Prev()
	case "pause":
		err = s.player.TogglePause()
	case "reload":
		err = s.player.ReloadNow()
	case "restart":
		err = s.player.Restart()
	case "rescan":
		_, err = s.pl.Scan()
	default:
		httpError(w, http.StatusBadRequest, "unbekannte Aktion")
		return
	}
	if err != nil {
		httpError(w, http.StatusConflict, err.Error())
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}

func (s *Server) handleMedia(w http.ResponseWriter, r *http.Request) {
	name, err := playlist.SafeName(strings.TrimPrefix(r.URL.Path, "/media/"))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	path := filepath.Join(s.pl.MediaDir(), name)
	if !playlist.Supported(name) {
		http.NotFound(w, r)
		return
	}
	http.ServeFile(w, r, path)
}

// handleThumb liefert ein Vorschaubild und legt es dabei im Cache ab.
func (s *Server) handleThumb(w http.ResponseWriter, r *http.Request) {
	name, err := playlist.SafeName(strings.TrimPrefix(r.URL.Path, "/thumb/"))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	kind := playlist.Kind(name)
	if kind == "" {
		http.NotFound(w, r)
		return
	}
	src := filepath.Join(s.pl.MediaDir(), name)
	info, err := os.Stat(src)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	if !s.hasFF {
		if kind == "image" {
			http.ServeFile(w, r, src)
			return
		}
		s.servePlaceholder(w)
		return
	}

	sum := sha1.Sum([]byte(fmt.Sprintf("%s|%d|%d", name, info.ModTime().UnixNano(), info.Size())))
	thumb := filepath.Join(s.cfg.ThumbDir(), hex.EncodeToString(sum[:])+".jpg")

	if _, err := os.Stat(thumb); err != nil {
		s.thumbMu.Lock()
		_, statErr := os.Stat(thumb)
		if statErr != nil {
			err = makeThumb(src, thumb, kind)
		} else {
			err = nil
		}
		s.thumbMu.Unlock()
		if err != nil {
			log.Printf("web: vorschaubild %q: %v", name, err)
			if kind == "image" {
				http.ServeFile(w, r, src)
			} else {
				s.servePlaceholder(w)
			}
			return
		}
	}

	w.Header().Set("Cache-Control", "public, max-age=86400")
	http.ServeFile(w, r, thumb)
}

func (s *Server) servePlaceholder(w http.ResponseWriter) {
	w.Header().Set("Content-Type", "image/svg+xml")
	w.Header().Set("Cache-Control", "public, max-age=86400")
	io.WriteString(w, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 3">`+
		`<rect width="4" height="3" fill="#243040"/>`+
		`<path d="M1.6 1 2.8 1.5 1.6 2z" fill="#6f8cad"/></svg>`)
}

// ---------------------------------------------------------------------------
// Server starten
// ---------------------------------------------------------------------------

// ListenAndServe startet den HTTP-Server auf dem konfigurierten Port.
func (s *Server) ListenAndServe(addr string) error {
	srv := &http.Server{
		Addr:    addr,
		Handler: s.Handler(),
		// Kein ReadTimeout: grosse Videouploads duerfen lange dauern.
		ReadHeaderTimeout: 30 * time.Second,
		IdleTimeout:       120 * time.Second,
	}
	log.Printf("web: hoere auf %s", addr)
	return srv.ListenAndServe()
}

// ---------------------------------------------------------------------------
// Hilfsfunktionen
// ---------------------------------------------------------------------------

func writeJSON(w http.ResponseWriter, code int, v any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(code)
	if err := json.NewEncoder(w).Encode(v); err != nil {
		log.Printf("web: antwort schreiben: %v", err)
	}
}

func httpError(w http.ResponseWriter, code int, msg string) {
	writeJSON(w, code, map[string]string{"error": msg})
}
