package web

import (
	"net/http"
	"strings"

	"github.com/skip2/go-qrcode"
	"golang.org/x/crypto/bcrypt"

	"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)

// Guest launcher share handlers (v0.165.0). See share.go for the security model. This file holds the
// HTTP surface: the pre-auth guest pages (/s/<token>) and the admin share-management POSTs
// (/launcher/share/*, session-authed).

// shareMinPassword is the minimum length of the OPTIONAL per-share password.
const shareMinPassword = 8

// setGuestHeaders stamps the guest launcher responses so search engines never index a capability URL,
// referrers never leak the token to an opened app, and no intermediary caches the page.
func (s *Server) setGuestHeaders(w http.ResponseWriter) {
	h := w.Header()
	h.Set("X-Robots-Tag", "noindex, nofollow")
	h.Set("Referrer-Policy", "no-referrer")
	h.Set("Cache-Control", "no-store")
}

// share404 answers exactly like the mux default 404 (http.NotFound) so a wrong or disabled token is
// byte-for-byte indistinguishable from any unknown route. The token is redacted from the log.
func (s *Server) share404(w http.ResponseWriter, r *http.Request) {
	s.logger.Printf("[WARN] [web] 404 Not Found: %s /s/<redacted>", r.Method)
	http.NotFound(w, r)
}

// GuestLauncherApp is one tile on the standalone guest launcher. It carries ONLY what a guest may see
// — never the internal state vocabulary (stopped/exited/degraded/unhealthy). Clickable apps render as
// links to their public URL; the rest are greyed with a calm, non-technical Hungarian label.
type GuestLauncherApp struct {
	DisplayName string
	Slug        string
	BrandColor  string
	Clickable   bool
	Href        string // set only when Clickable
	Label       string // set only when NOT Clickable
}

// guestLauncherApps maps the shared launcherApps() slice into the guest view. Clickable ⇒ the app is
// operational AND its public route is actually published (a healthy, reachable app). isOperational
// alone would let an unhealthy/restarting/degraded app through — its URL 404s at Traefik, so a guest
// tap would dead-end; routeUnpublished screens exactly those. The label rides the v0.164.0 ruling:
// StateStopped is a deliberate owner action ("A tulajdonos leállította"); any other non-clickable
// state is a transient the guest need not understand ("Átmenetileg nem elérhető").
func (s *Server) guestLauncherApps() []GuestLauncherApp {
	return buildGuestApps(s.launcherApps(), s.cfg.Customer.Domain)
}

// buildGuestApps is the pure mapping from the shared launcher slice to the guest view (no manager, no
// request) — the tested seam for the clickability rule and the guest label vocabulary.
func buildGuestApps(apps []LauncherApp, domain string) []GuestLauncherApp {
	out := make([]GuestLauncherApp, 0, len(apps))
	for _, a := range apps {
		g := GuestLauncherApp{DisplayName: a.DisplayName, Slug: a.Slug, BrandColor: a.BrandColor}
		switch {
		case isOperationalState(a.State) && !routeUnpublished(a.State):
			g.Clickable = true
			g.Href = "https://" + a.Subdomain + "." + domain + a.OpenPath
		case a.State == stacks.StateStopped:
			g.Label = "A tulajdonos leállította"
		default:
			g.Label = "Átmenetileg nem elérhető"
		}
		out = append(out, g)
	}
	return out
}

// shareGuestHandler serves GET /s/<token>: the standalone read-only launcher, or the password gate
// when a share password is set and no valid cookie is present. An unknown token → share404.
func (s *Server) shareGuestHandler(w http.ResponseWriter, r *http.Request) {
	token := strings.TrimPrefix(r.URL.Path, "/s/")
	stored := s.settings.GetLauncherShareToken()
	if !shareTokenMatches(stored, token) {
		s.share404(w, r)
		return
	}
	pwHash := s.settings.GetLauncherSharePasswordHash()
	if pwHash != "" && !s.shareCookieValid(r, stored, pwHash) {
		s.renderSharePasswordPage(w, r, "")
		return
	}
	s.renderShareGuestPage(w, r)
}

// shareGuestPasswordHandler handles POST /s/<token>: the optional share-password gate. On success it
// sets the signed, ~30-day gate cookie (bound to token|passwordHash). Pre-auth HMAC CSRF + a per-IP
// 5/1-min limiter (own map) protect it.
func (s *Server) shareGuestPasswordHandler(w http.ResponseWriter, r *http.Request) {
	token := strings.TrimPrefix(r.URL.Path, "/s/")
	stored := s.settings.GetLauncherShareToken()
	if !shareTokenMatches(stored, token) {
		s.share404(w, r)
		return
	}
	pwHash := s.settings.GetLauncherSharePasswordHash()
	if pwHash == "" {
		// No gate — a stray POST just returns to the page (renders directly).
		s.renderShareGuestPage(w, r)
		return
	}
	_ = r.ParseForm()
	if !s.validShareCSRF(r) {
		s.renderSharePasswordPage(w, r, "Érvénytelen űrlap — töltse újra az oldalt.")
		return
	}
	ip := clientIP(r)
	if s.shareRateLimited(ip) {
		s.logger.Printf("[WARN] [web] share password rate limited for %s", ip)
		s.renderSharePasswordPage(w, r, "Túl sok sikertelen próbálkozás, próbálja újra 1 perc múlva")
		return
	}
	if bcrypt.CompareHashAndPassword([]byte(pwHash), []byte(r.FormValue("password"))) != nil {
		s.shareRegisterFailure(ip)
		s.renderSharePasswordPage(w, r, "Hibás jelszó")
		return
	}
	s.shareClearFailures(ip)
	http.SetCookie(w, &http.Cookie{
		Name:     shareCookieName,
		Value:    s.shareCookieValue(stored, pwHash),
		Path:     "/s/",
		MaxAge:   int(shareCookieMaxAge.Seconds()),
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		Secure:   r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https",
	})
	// Redirect back to the same URL so a reload/back does not re-POST; the cookie now passes the gate.
	http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
}

// renderShareGuestPage renders the standalone guest launcher (own minimal <html>, no admin chrome).
func (s *Server) renderShareGuestPage(w http.ResponseWriter, r *http.Request) {
	s.setGuestHeaders(w)
	data := map[string]interface{}{
		"Domain": s.cfg.Customer.Domain,
		"Apps":   s.guestLauncherApps(),
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := s.tmpl.ExecuteTemplate(w, "launcher_shared", data); err != nil {
		s.logger.Printf("[ERROR] [web] Template error (launcher_shared): %v", err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
	}
}

// renderSharePasswordPage renders the standalone one-field password gate and sets the pre-auth CSRF
// cookie the POST validates.
func (s *Server) renderSharePasswordPage(w http.ResponseWriter, r *http.Request, errMsg string) {
	s.setGuestHeaders(w)
	csrf := s.setShareCSRFCookie(w, r)
	data := map[string]interface{}{
		"Action": r.URL.Path, // /s/<token> — the guest already holds this token in their URL bar
		"CSRF":   csrf,
		"Error":  errMsg,
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := s.tmpl.ExecuteTemplate(w, "launcher_share_password", data); err != nil {
		s.logger.Printf("[ERROR] [web] Template error (launcher_share_password): %v", err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
	}
}

// ── admin share management (session-authed via RequireAuth + session CSRF via CsrfProtect) ─────────

// launcherShareRedirect returns to /launcher with a Hungarian flash (reuses urlQueryEscape).
func (s *Server) launcherShareRedirect(w http.ResponseWriter, r *http.Request, flash string) {
	http.Redirect(w, r, "/launcher?flash="+urlQueryEscape(flash), http.StatusSeeOther)
}

// launcherShareQRHandler serves the share link as a ~256px PNG QR code (admin-authed; not exempted, so
// an unauthenticated request → login redirect). The token is never logged.
func (s *Server) launcherShareQRHandler(w http.ResponseWriter, r *http.Request) {
	token := s.settings.GetLauncherShareToken()
	if token == "" {
		http.NotFound(w, r)
		return
	}
	png, err := qrcode.Encode("https://"+r.Host+"/s/"+token, qrcode.Medium, 256)
	if err != nil {
		s.logger.Printf("[ERROR] [web] share: QR encode failed: %v", err)
		http.Error(w, "QR error", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "image/png")
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("X-Robots-Tag", "noindex, nofollow")
	_, _ = w.Write(png)
}

// launcherShareEnableHandler mints the first token (POST /launcher/share/enable).
func (s *Server) launcherShareEnableHandler(w http.ResponseWriter, r *http.Request) {
	if s.settings.GetLauncherShareToken() != "" {
		s.launcherShareRedirect(w, r, "A megosztás már be van kapcsolva.")
		return
	}
	tok, err := newShareToken()
	if err != nil {
		s.logger.Printf("[ERROR] [web] share: token generation failed: %v", err)
		s.launcherShareRedirect(w, r, "A megosztás bekapcsolása nem sikerült.")
		return
	}
	if err := s.settings.SetLauncherShareToken(tok); err != nil {
		s.logger.Printf("[ERROR] [web] share: saving token failed: %v", err)
		s.launcherShareRedirect(w, r, "A megosztás bekapcsolása nem sikerült.")
		return
	}
	s.logger.Printf("[INFO] [web] launcher share link enabled")
	s.launcherShareRedirect(w, r, "A megosztás bekapcsolva.")
}

// launcherShareRotateHandler mints a fresh token (POST /launcher/share/rotate). The old link 404s and
// every outstanding guest cookie is invalidated (both are bound to the token).
func (s *Server) launcherShareRotateHandler(w http.ResponseWriter, r *http.Request) {
	if s.settings.GetLauncherShareToken() == "" {
		s.launcherShareRedirect(w, r, "A megosztás nincs bekapcsolva.")
		return
	}
	tok, err := newShareToken()
	if err != nil {
		s.logger.Printf("[ERROR] [web] share: token generation failed: %v", err)
		s.launcherShareRedirect(w, r, "Az új link készítése nem sikerült.")
		return
	}
	if err := s.settings.SetLauncherShareToken(tok); err != nil {
		s.logger.Printf("[ERROR] [web] share: saving token failed: %v", err)
		s.launcherShareRedirect(w, r, "Az új link készítése nem sikerült.")
		return
	}
	s.logger.Printf("[INFO] [web] launcher share link rotated")
	s.launcherShareRedirect(w, r, "Új megosztási link készült. A korábbi link és minden korábbi belépés érvénytelen.")
}

// launcherShareDisableHandler clears the token AND the share password (POST /launcher/share/disable) —
// a clean slate so a later re-enable never inherits a stale gate. All /s/ paths then 404.
func (s *Server) launcherShareDisableHandler(w http.ResponseWriter, r *http.Request) {
	if err := s.settings.SetLauncherShareToken(""); err != nil {
		s.logger.Printf("[ERROR] [web] share: clearing token failed: %v", err)
		s.launcherShareRedirect(w, r, "A megosztás kikapcsolása nem sikerült.")
		return
	}
	if err := s.settings.SetLauncherSharePasswordHash(""); err != nil {
		s.logger.Printf("[WARN] [web] share: clearing share password on disable failed: %v", err)
	}
	s.logger.Printf("[INFO] [web] launcher share link disabled")
	s.launcherShareRedirect(w, r, "A megosztás kikapcsolva.")
}

// launcherSharePasswordHandler sets or clears the OPTIONAL per-share password (POST
// /launcher/share/password). action=clear removes it; otherwise a min-length password is bcrypt-hashed
// into its OWN settings field (never the admin hash). Either change invalidates outstanding cookies.
func (s *Server) launcherSharePasswordHandler(w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	if s.settings.GetLauncherShareToken() == "" {
		s.launcherShareRedirect(w, r, "A megosztás nincs bekapcsolva.")
		return
	}
	if r.FormValue("action") == "clear" {
		if err := s.settings.SetLauncherSharePasswordHash(""); err != nil {
			s.logger.Printf("[ERROR] [web] share: clearing share password failed: %v", err)
			s.launcherShareRedirect(w, r, "A jelszó törlése nem sikerült.")
			return
		}
		s.logger.Printf("[INFO] [web] launcher share password cleared")
		s.launcherShareRedirect(w, r, "A megosztási jelszó törölve.")
		return
	}
	pw := r.FormValue("password")
	if len(pw) < shareMinPassword {
		s.launcherShareRedirect(w, r, "A jelszónak legalább 8 karakter hosszúnak kell lennie.")
		return
	}
	hash, err := bcrypt.GenerateFromPassword([]byte(pw), 10)
	if err != nil {
		s.logger.Printf("[ERROR] [web] share: hashing share password failed: %v", err)
		s.launcherShareRedirect(w, r, "A jelszó beállítása nem sikerült.")
		return
	}
	if err := s.settings.SetLauncherSharePasswordHash(string(hash)); err != nil {
		s.logger.Printf("[ERROR] [web] share: saving share password failed: %v", err)
		s.launcherShareRedirect(w, r, "A jelszó beállítása nem sikerült.")
		return
	}
	s.logger.Printf("[INFO] [web] launcher share password set")
	s.launcherShareRedirect(w, r, "A megosztási jelszó beállítva. A korábbi belépések érvénytelenek.")
}
