package playerserver

import (
	"context"
	"embed"
	"encoding/json"
	"fmt"
	"io/fs"
	"math/rand"
	"net"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

//go:embed assets
var assetsFS embed.FS

// PlaylistItem is a single displayable content item fetched from the backend.
type PlaylistItem struct {
	Src             string `json:"src"`
	Type            string `json:"type"` // web | image | video | pdf
	Title           string `json:"title,omitempty"`
	DurationSeconds int    `json:"duration_seconds"`
}

// NowPlaying describes the current playback state returned to the browser.
type NowPlaying struct {
	// Playlist holds the ordered items to display (primary).
	Playlist []PlaylistItem `json:"playlist,omitempty"`
	// URL is a legacy single-URL fallback (kept for backwards compatibility).
	URL          string `json:"url,omitempty"`
	Status       string `json:"status"`
	Connectivity string `json:"connectivity"`
}

// InfoItem is a single entry shown in the lower-third sysinfo overlay.
type InfoItem struct {
	Label string `json:"label"`
	Value string `json:"value"`
}

// SysInfo holds the items shown in the lower-third overlay.
type SysInfo struct {
	Items []InfoItem `json:"items"`
}

// Server serves the local player UI to Chromium.
type Server struct {
	listenAddr  string
	nowFn       func() NowPlaying
	startupToken string // zufälliger Token der sich bei jedem Start ändert
}

// New creates a Server. listenAddr is e.g. "127.0.0.1:8090".
// nowFn is called on each request and returns the current playback state.
func New(listenAddr string, nowFn func() NowPlaying) *Server {
	token := fmt.Sprintf("%016x", rand.Uint64())
	return &Server{listenAddr: listenAddr, nowFn: nowFn, startupToken: token}
}

// Run starts the HTTP server and blocks until ctx is cancelled.
func (s *Server) Run(ctx context.Context) error {
	sub, err := fs.Sub(assetsFS, "assets")
	if err != nil {
		return err
	}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /player", s.handlePlayer)
	mux.HandleFunc("GET /api/now-playing", s.handleNowPlaying)
	mux.HandleFunc("GET /api/sysinfo", handleSysInfo)
	mux.HandleFunc("GET /api/startup-token", s.handleStartupToken)
	mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub))))

	srv := &http.Server{Handler: mux}

	ln, err := net.Listen("tcp", s.listenAddr)
	if err != nil {
		return err
	}

	go func() {
		<-ctx.Done()
		srv.Close()
	}()

	if err := srv.Serve(ln); err != http.ErrServerClosed {
		return err
	}
	return nil
}

func (s *Server) handlePlayer(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Write([]byte(playerHTML)) //nolint:errcheck
}

func (s *Server) handleNowPlaying(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(s.nowFn()) //nolint:errcheck
}

// handleStartupToken gibt einen zufälligen Token zurück der sich bei jedem
// Agent-Start ändert. Der Browser erkennt daran, dass der Agent neu gestartet
// wurde und lädt die Seite automatisch neu.
func (s *Server) handleStartupToken(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Cache-Control", "no-store")
	json.NewEncoder(w).Encode(map[string]string{"token": s.startupToken}) //nolint:errcheck
}

func handleSysInfo(w http.ResponseWriter, _ *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(collectSysInfo()) //nolint:errcheck
}

// collectSysInfo builds the list of info items shown in the overlay.
// To add more items, append to the slice here.
func collectSysInfo() SysInfo {
	var items []InfoItem
	if h, err := os.Hostname(); err == nil {
		items = append(items, InfoItem{Label: "Hostname", Value: h})
	}
	if up := readUptime(); up != "" {
		items = append(items, InfoItem{Label: "Uptime", Value: up})
	}
	return SysInfo{Items: items}
}

func readUptime() string {
	data, err := os.ReadFile("/proc/uptime")
	if err != nil {
		return ""
	}
	parts := strings.Fields(string(data))
	if len(parts) == 0 {
		return ""
	}
	secs, err := strconv.ParseFloat(parts[0], 64)
	if err != nil {
		return ""
	}
	d := time.Duration(secs) * time.Second
	days := int(d.Hours()) / 24
	hours := int(d.Hours()) % 24
	mins := int(d.Minutes()) % 60
	if days > 0 {
		return fmt.Sprintf("%dd %dh %dm", days, hours, mins)
	}
	if hours > 0 {
		return fmt.Sprintf("%dh %dm", hours, mins)
	}
	return fmt.Sprintf("%dm", mins)
}

const playerHTML = `<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8">
  <title>Morz Infoboard</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; background: #000; overflow: hidden; }

    /* Splash-Hintergrund */
    #splash {
      position: fixed; inset: 0;
      background-size: cover;
      background-position: center;
      background-repeat: no-repeat;
    }

    /* Unteres-Drittel-Overlay mit Systeminformationen */
    #info-overlay {
      position: fixed; bottom: 0; left: 0; right: 0;
      height: 33.3%;
      background: linear-gradient(
        to bottom,
        rgba(0,0,0,0)    0%,
        rgba(0,0,0,0.7)  40%,
        rgba(0,0,0,0.85) 100%
      );
      display: flex;
      align-items: flex-end;
      padding: 0 3% 2.5%;
      gap: 3rem;
      z-index: 1;
    }
    .info-item { display: flex; flex-direction: column; }
    .info-item .label {
      font-family: sans-serif; font-size: 0.7rem;
      letter-spacing: 0.12em; text-transform: uppercase;
      color: rgba(255,255,255,0.5); margin-bottom: 0.25em;
    }
    .info-item .value {
      font-family: monospace; font-size: 1.1rem;
      font-weight: 500; letter-spacing: 0.03em; color: #fff;
    }

    /* Inhalts-Elemente: iframe, img, video */
    #frame, #img-view, #video-view {
      position: fixed; inset: 0;
      width: 100%; height: 100%;
      border: none; display: none; z-index: 10;
      opacity: 0;
      transition: opacity 0.5s ease;
    }
    #img-view {
      object-fit: contain;
      background: #000;
    }
    #video-view {
      object-fit: contain;
      background: #000;
    }

    /* Fehler-Fallback für blockierte iframes */
    #frame-error {
      position: fixed; inset: 0;
      width: 100%; height: 100%;
      display: none; z-index: 10;
      background: #000;
      align-items: center; justify-content: center; flex-direction: column;
      gap: 1rem;
    }
    #frame-error .error-title {
      font-family: sans-serif; font-size: 2rem; color: rgba(255,255,255,0.7);
      text-align: center; padding: 0 10%;
    }
    #frame-error .error-hint {
      font-family: sans-serif; font-size: 1rem; color: rgba(255,255,255,0.35);
    }

    /* Verbindungsstatus-Punkt */
    #dot {
      position: fixed; bottom: 10px; right: 10px;
      width: 10px; height: 10px; border-radius: 50%;
      background: #444; opacity: 0.6; z-index: 9999;
      transition: background 0.5s;
    }
    #dot.online   { background: #4caf50; }
    #dot.degraded { background: #ff9800; }
    #dot.offline  { background: #f44336; }
  </style>
</head>
<body>
  <div id="splash"></div>
  <div id="info-overlay"></div>
  <iframe id="frame" allow="autoplay; fullscreen" allowfullscreen></iframe>
  <img id="img-view" alt="">
  <video id="video-view" autoplay muted playsinline></video>
  <div id="frame-error">
    <span class="error-title" id="frame-error-title"></span>
    <span class="error-hint">Seite kann nicht eingebettet werden</span>
  </div>
  <div id="dot"></div>

  <script>
    var splash      = document.getElementById('splash');
    var overlay     = document.getElementById('info-overlay');
    var frame       = document.getElementById('frame');
    var imgView     = document.getElementById('img-view');
    var videoView   = document.getElementById('video-view');
    var frameError  = document.getElementById('frame-error');
    var frameErrorTitle = document.getElementById('frame-error-title');
    var dot         = document.getElementById('dot');

    // ── Splash-Orientierung ───────────────────────────────────────────
    function updateSplash() {
      var portrait = window.innerHeight > window.innerWidth;
      splash.style.backgroundImage = portrait
        ? 'url(/assets/splash-portrait.png)'
        : 'url(/assets/splash-landscape.png)';
    }
    updateSplash();
    window.addEventListener('resize', updateSplash);

    // ── Sysinfo-Overlay ───────────────────────────────────────────────
    // staticSysItems wird beim pollSysInfo-Callback gesetzt und enthält
    // Hostname und Uptime vom Server.
    var staticSysItems = [];

    function renderSysInfo(staticItems) {
      if (staticItems) { staticSysItems = staticItems; }
      var all = staticSysItems.slice();

      // Dynamische Einträge aus Playlist-Daten anhängen.
      if (dynCurrentTitle) {
        all.push({ label: 'Jetzt', value: dynCurrentTitle });
      }
      if (dynPlaylistLength > 0) {
        all.push({ label: 'Playlist', value: dynPlaylistLength + ' Eintr\u00e4ge' });
      }
      if (dynConnectivity) {
        var connLabel = dynConnectivity === 'online'   ? 'Erreichbar'
                      : dynConnectivity === 'degraded' ? 'Eingeschränkt'
                      : 'Nicht erreichbar';
        all.push({ label: 'Server', value: connLabel });
      }

      overlay.innerHTML = '';
      all.forEach(function(item) {
        var el = document.createElement('div');
        el.className = 'info-item';
        el.innerHTML =
          '<span class="label">' + esc(item.label) + '</span>' +
          '<span class="value">' + esc(item.value) + '</span>';
        overlay.appendChild(el);
      });
    }

    function esc(s) {
      return String(s)
        .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
    }

    // ── Playlist-Rotation ─────────────────────────────────────────────
    var items       = [];   // current playlist
    var currentIdx  = 0;
    var rotateTimer = null;

    // ── Sysinfo-Erweiterung: dynamische Overlay-Daten ─────────────────
    var dynCurrentTitle    = '';   // Titel des aktuell spielenden Items
    var dynPlaylistLength  = 0;    // Anzahl Einträge in der Playlist
    var dynConnectivity    = '';   // online / degraded / offline

    // Returns a fingerprint string for change detection.
    function playlistKey(pl) {
      return (pl || []).map(function(i) { return i.src + ':' + i.duration_seconds; }).join('|');
    }

    function clearRotation() {
      if (rotateTimer) { clearTimeout(rotateTimer); rotateTimer = null; }
    }

    // Versteckt alle Content-Elemente vor dem Anzeigen des richtigen Typs.
    // Blendet zunächst auf opacity:0 aus und entfernt display erst nach der
    // Transition (500ms), damit der Fade-Out sichtbar ist.
    //
    // Race-Condition-Fix: Das setTimeout-Callback prüft vor dem display=none,
    // ob das Element noch opacity=0 hat. Falls displayItem() das Element
    // inzwischen wieder auf display=block+opacity=1 gesetzt hat, wird es
    // nicht fälschlicherweise versteckt.
    function hideAllContent() {
      // Laufendes Video sofort stoppen damit kein Audio weiterläuft.
      videoView.pause();
      videoView.src = '';

      [frame, imgView, videoView].forEach(function(el) {
        if (el.style.display !== 'none') {
          el.style.opacity = '0';
          (function(e) {
            setTimeout(function() {
              // Nur verstecken wenn das Element noch ausgeblendet ist
              // (opacity=0 oder leer). Falls displayItem() es inzwischen
              // wieder sichtbar gemacht hat, nicht anfassen.
              if (e.style.opacity === '0' || e.style.opacity === '') {
                e.style.display = 'none';
              }
            }, 500);
          })(el);
        }
      });
      frameError.style.display = 'none';
    }

    // Blendet den Splash-Screen aus (wird aufgerufen wenn echter Content angezeigt wird).
    function hideSplash() {
      splash.style.display = 'none';
    }

    // Blendet den Splash-Screen wieder ein.
    function showSplashDiv() {
      splash.style.display = '';
    }

    function scheduleNext(durationSeconds) {
      clearRotation();
      var ms = Math.max((durationSeconds || 20), 1) * 1000;
      rotateTimer = setTimeout(function() {
        currentIdx = (currentIdx + 1) % items.length;
        showItem(items[currentIdx]);
      }, ms);
    }

    // TRANSITION_MS muss mit der CSS-Transition-Dauer übereinstimmen.
    var TRANSITION_MS = 500;

    function showItem(item) {
      if (!item) { showSplash(); return; }

      // Erst Fade-Out des aktuellen Inhalts abwarten, dann neuen anzeigen.
      hideAllContent();
      hideSplash();
      overlay.style.display = 'none';

      setTimeout(function() { displayItem(item); }, TRANSITION_MS);
    }

    function displayItem(item) {
      var type = item.type || 'web';

      if (type === 'image') {
        // display setzen, dann per doppeltem rAF opacity auf 1 für Fade-In.
        imgView.src = item.src;
        imgView.style.display = 'block';
        requestAnimationFrame(function() {
          requestAnimationFrame(function() { imgView.style.opacity = '1'; });
        });
        scheduleNext(item.duration_seconds);

      } else if (type === 'video') {
        videoView.src = item.src;
        videoView.style.display = 'block';
        requestAnimationFrame(function() {
          requestAnimationFrame(function() { videoView.style.opacity = '1'; });
        });
        videoView.load();
        videoView.play().catch(function() {});
        // Nach Ablauf der konfigurierten Dauer oder am Ende des Videos rotieren.
        var advanced = false;
        function advanceOnce() {
          if (advanced) return;
          advanced = true;
          currentIdx = (currentIdx + 1) % items.length;
          showItem(items[currentIdx]);
        }
        clearRotation();
        var ms = Math.max((item.duration_seconds || 20), 1) * 1000;
        rotateTimer = setTimeout(advanceOnce, ms);
        videoView.onended = advanceOnce;

      } else {
        // type === 'web', 'pdf' oder unbekannt → iframe
        if (type === 'pdf') {
          frame.src = (function pdfUrl(src) {
            var defaults = {toolbar: '0', navpanes: '0', scrollbar: '0', view: 'Fit', page: '1'};
            var hashIdx = src.indexOf('#');
            var base = hashIdx >= 0 ? src.substring(0, hashIdx) : src;
            var existing = hashIdx >= 0 ? src.substring(hashIdx + 1) : '';
            var params = {};
            existing.split('&').forEach(function(p) {
              var kv = p.split('=');
              if (kv[0]) params[kv[0]] = kv[1] || '';
            });
            for (var k in defaults) {
              if (!(k in params)) params[k] = defaults[k];
            }
            var parts = [];
            for (var k in params) parts.push(k + '=' + params[k]);
            return base + '#' + parts.join('&');
          })(item.src);
        } else {
          if (frame.src !== item.src) { frame.src = item.src; }
        }
        frame.style.display = 'block';
        requestAnimationFrame(function() {
          requestAnimationFrame(function() { frame.style.opacity = '1'; });
        });

        // Fehler-Fallback wenn iframe-Laden fehlschlägt (z.B. X-Frame-Options).
        frame.onerror = null;
        frame.onload = function() {
          // Prüfen ob der iframe-Inhalt zugänglich ist. Bei cross-origin-Blockierung
          // wirft der Zugriff auf contentDocument einen SecurityError – das bedeutet
          // aber, dass die Seite geladen wurde. Wir können X-Frame-Options-Fehler im
          // Browser leider nicht direkt erkennen; stattdessen setzen wir einen
          // kurzen Timeout: Wenn der iframe leer bleibt (about:blank nach dem Load-
          // Event wegen Blockierung), zeigen wir den Fallback.
          try {
            var doc = frame.contentDocument || frame.contentWindow.document;
            // Wenn der Body keine Kinder hat und URL nicht die gewünschte ist →
            // Seite wurde blockiert und durch about:blank ersetzt.
            if (doc && doc.body && doc.body.children.length === 0 &&
                doc.location && doc.location.href === 'about:blank') {
              showFrameError(item);
            }
          } catch (e) {
            // Cross-origin SecurityError → Seite wurde tatsächlich geladen, kein Fehler.
          }
        };
        scheduleNext(item.duration_seconds);
      }
    }

    function showFrameError(item) {
      hideAllContent();
      overlay.style.display = 'none';
      frameErrorTitle.textContent = item.title || item.src;
      frameError.style.display = 'flex';
      // Nach kurzer Wartezeit (3s) zum nächsten Item rotieren.
      clearRotation();
      rotateTimer = setTimeout(function() {
        currentIdx = (currentIdx + 1) % items.length;
        showItem(items[currentIdx]);
      }, 3000);
    }

    function showSplash() {
      clearRotation();
      hideAllContent();
      showSplashDiv();
      overlay.style.display = '';
    }

    var lastPlaylistKey = '';

    function applyNowPlaying(data) {
      // Connectivity-Punkt und dynamische Overlay-Variable aktualisieren.
      dot.className = data.connectivity || '';
      dynConnectivity = data.connectivity || '';

      // Legacy single-URL fallback.
      if (data.url && (!data.playlist || data.playlist.length === 0)) {
        var key = data.url + ':legacy';
        dynPlaylistLength = 1;
        dynCurrentTitle   = data.url;
        renderSysInfo();
        if (lastPlaylistKey !== key) {
          lastPlaylistKey = key;
          items = [{ src: data.url, type: 'web', duration_seconds: 30 }];
          currentIdx = 0;
          showItem(items[0]);
        }
        return;
      }

      var playlist = data.playlist || [];
      dynPlaylistLength = playlist.length;
      if (playlist.length === 0) {
        dynCurrentTitle = '';
        renderSysInfo();
        showSplash();
        lastPlaylistKey = '';
        items = [];
        return;
      }

      // Titel des aktuell laufenden Items ermitteln (currentIdx kann nach
      // einem Playlist-Wechsel ggf. noch auf dem alten Index stehen – wir
      // nehmen das Item des zuletzt gesetzten currentIdx, falls vorhanden).
      var cur = playlist[currentIdx] || playlist[0];
      dynCurrentTitle = cur.title || cur.src || '';
      renderSysInfo();

      var key = playlistKey(playlist);
      if (key === lastPlaylistKey) {
        return; // unchanged — let current rotation continue
      }

      // Playlist changed: start from beginning.
      lastPlaylistKey = key;
      items = playlist;
      currentIdx = 0;
      dynCurrentTitle = items[0].title || items[0].src || '';
      renderSysInfo();
      showItem(items[0]);
    }

    // ── Polling ───────────────────────────────────────────────────────
    function pollSysInfo() {
      fetch('/api/sysinfo')
        .then(function(r) { return r.json(); })
        // Statische Items (Hostname, Uptime) übergeben; renderSysInfo hängt
        // die dynamischen Daten (Titel, Playlist-Länge, Konnektivität) selbst an.
        .then(function(d) { renderSysInfo(d.items); })
        .catch(function() {});
    }

    // Bug 1: Fast-Retry beim Start – alle 2s pollen bis erste Playlist vorliegt,
    // dann auf 30s-Intervall wechseln.
    var fastRetryTimer   = null;
    var slowPollInterval = null;
    var playlistReady    = false;

    function startSlowPoll() {
      if (slowPollInterval) return;
      // Playlist alle 5s prüfen (fängt MQTT-getriggerte Backend-Änderungen schnell ab).
      // Sysinfo läuft separat alle 30s (weiter unten).
      slowPollInterval = setInterval(function() {
        pollNowPlaying();
      }, 5000);
    }

    function pollNowPlaying() {
      fetch('/api/now-playing')
        .then(function(r) { return r.json(); })
        .then(function(data) {
          applyNowPlaying(data);
          // Sobald eine echte Playlist vorhanden ist, Fast-Retry beenden.
          if (!playlistReady && items.length > 0) {
            playlistReady = true;
            if (fastRetryTimer) { clearInterval(fastRetryTimer); fastRetryTimer = null; }
            startSlowPoll();
          }
        })
        .catch(function() { dot.className = 'offline'; });
    }

    pollSysInfo();
    pollNowPlaying();
    setInterval(pollSysInfo, 30000);  // sysinfo weiterhin alle 30s

    // Fast-Retry: alle 2s bis Playlist bereit.
    fastRetryTimer = setInterval(pollNowPlaying, 2000);
    // Spätestens nach 60s auf langsames Polling wechseln (Fallback).
    setTimeout(function() {
      if (!playlistReady) {
        if (fastRetryTimer) { clearInterval(fastRetryTimer); fastRetryTimer = null; }
        startSlowPoll();
      }
    }, 60000);

    // ── Auto-Reload bei Agent-Neustart ───────────────────────────────
    // Der Agent gibt bei jedem Start einen neuen zufälligen Token zurück.
    // Falls sich der Token ändert, hat der Agent neu gestartet → Seite neu laden.
    var knownStartupToken = null;

    function pollStartupToken() {
      fetch('/api/startup-token', {cache: 'no-store'})
        .then(function(r) { return r.json(); })
        .then(function(d) {
          if (!d || !d.token) return;
          if (knownStartupToken === null) {
            // Erster Aufruf: Token merken, kein Reload.
            knownStartupToken = d.token;
          } else if (knownStartupToken !== d.token) {
            // Token hat sich geändert → Agent wurde neu gestartet.
            window.location.reload();
          }
        })
        .catch(function() {}); // Agent offline → ignorieren
    }

    pollStartupToken();
    setInterval(pollStartupToken, 5000); // alle 5s prüfen
  </script>
</body>
</html>`
