package web

import (
	"context"
	"net/http"
	"net/url"
	"path/filepath"
	"strconv"
	"strings"
	"time"

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

// Off-box (NAS) restic-SFTP backup handlers (Part B). Form POSTs that redirect to /backups with a flash.
// The SSH private key + known-host line are provided out-of-band by the operator (textareas) and written
// to 0600/0644 files by the backup Manager; they are NEVER echoed back, logged, or stored in settings.

// offboxRedirect sends the operator back to the backups page with a flash (success or error) message.
func offboxRedirect(w http.ResponseWriter, r *http.Request, msg string, isErr bool) {
	q := "flash"
	if isErr {
		q = "flash_error"
	}
	http.Redirect(w, r, "/backups?"+q+"="+url.QueryEscape(msg), http.StatusFound)
}

// offboxConfigHandler saves the off-box target + (out-of-band) SSH key + known_hosts.
func (s *Server) offboxConfigHandler(w http.ResponseWriter, r *http.Request) {
	if s.backupMgr == nil {
		offboxRedirect(w, r, "A mentéskezelő nem elérhető.", true)
		return
	}
	_ = r.ParseForm()
	host := strings.TrimSpace(r.FormValue("host"))
	user := strings.TrimSpace(r.FormValue("user"))
	repoPath := strings.TrimSpace(r.FormValue("repo_path"))
	port, _ := strconv.Atoi(strings.TrimSpace(r.FormValue("port")))
	if port == 0 {
		port = 22
	}
	sshKey := r.FormValue("ssh_key")
	knownHosts := r.FormValue("known_hosts")

	if host == "" || user == "" || repoPath == "" {
		offboxRedirect(w, r, "A NAS címe, a felhasználó és a tárhely útvonala kötelező.", true)
		return
	}
	if !strings.HasPrefix(repoPath, "/") {
		offboxRedirect(w, r, "A tárhely útvonalának abszolútnak kell lennie (/-rel kezdődjön).", true)
		return
	}
	// Validate BEFORE persisting — host/user/repo flow into the ssh command restic runs; reject anything
	// that could inject an ssh option (leading '-') or a metacharacter (the security boundary).
	if err := backup.ValidateOffboxTarget(&settings.OffboxTarget{Host: host, User: user, RepoPath: repoPath, Port: port}); err != nil {
		offboxRedirect(w, r, "Érvénytelen beállítás: "+err.Error(), true)
		return
	}
	// First-time config requires the SSH key + a pinned known-host line (no blind TOFU).
	existing := s.backupMgr.OffboxConfigured()
	if !existing && (strings.TrimSpace(sshKey) == "" || strings.TrimSpace(knownHosts) == "") {
		offboxRedirect(w, r, "Az első beállításhoz az SSH privát kulcs és a NAS ismert-host sora is kötelező.", true)
		return
	}

	// Write secrets out-of-band (0600 key/pw, 0644 known_hosts); never logged.
	if err := s.backupMgr.WriteOffboxSecrets(sshKey, knownHosts); err != nil {
		s.logger.Printf("[ERROR] [web] offbox secrets: %v", err)
		offboxRedirect(w, r, "A hitelesítő adatok mentése sikertelen.", true)
		return
	}

	prev := s.settings.GetOffboxTarget()
	tgt := &settings.OffboxTarget{
		Enabled: r.FormValue("enabled") == "on" || r.FormValue("enabled") == "true",
		Host:    host, Port: port, User: user, RepoPath: repoPath,
		Schedule: "daily",
	}
	if prev != nil { // preserve runtime status fields across an edit
		tgt.LastRun, tgt.LastStatus, tgt.LastError = prev.LastRun, prev.LastStatus, prev.LastError
		tgt.LastDuration, tgt.RepoSizeHuman, tgt.SnapshotCount = prev.LastDuration, prev.RepoSizeHuman, prev.SnapshotCount
		tgt.LastWarning = prev.LastWarning
		tgt.EscrowState = prev.EscrowState
	}
	// fork-4: enabling offsite stages the repo password to the agent for the R-escrow ceremony and marks
	// it PENDING — no offsite RUN proceeds until escrow is confirmed (atomicity). Re-editing an already
	// escrowed target keeps it escrowed (WriteOffboxSecrets leaves the password unchanged). A stage-push
	// failure does NOT mark escrowed; it is surfaced (the run gate still protects data).
	stageErr := ""
	if tgt.Enabled {
		if tgt.EscrowState != "escrowed" {
			tgt.EscrowState = "pending"
		}
		if client, cerr := s.agentClient(); cerr != nil {
			stageErr = " — a kulcs letéti előkészítése nem sikerült (az ügynök nem elérhető); próbáld újra."
			s.logger.Printf("[WARN] [web] offbox escrow stage: agent client: %v", cerr)
		} else if err := s.backupMgr.PushOffboxPasswordForEscrow(r.Context(), client.StageEscrowSecret); err != nil {
			stageErr = " — a kulcs letéti előkészítése nem sikerült; próbáld újra."
			s.logger.Printf("[WARN] [web] offbox escrow stage: %v", err) // err carries no secret
		}
	}
	if err := s.settings.SetOffboxTarget(tgt); err != nil {
		offboxRedirect(w, r, "A beállítás mentése sikertelen.", true)
		return
	}
	s.logger.Printf("[INFO] [web] off-box target configured: %s@%s:%s (port %d, enabled=%v, escrow=%s)", user, host, repoPath, port, tgt.Enabled, tgt.EscrowState)
	offboxRedirect(w, r, "A NAS mentési cél elmentve."+stageErr, stageErr != "")
}

// offboxConfirmEscrowHandler marks the offsite repo password as escrowed under R (fork-4). The operator
// calls this after a successful escrow-create ceremony; offsite runs stay gated until then. (The
// provisioning task should replace this with a hub-verified auto-confirm to remove the operator-forgets/
// operator-lies footgun.)
func (s *Server) offboxConfirmEscrowHandler(w http.ResponseWriter, r *http.Request) {
	if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() {
		offboxRedirect(w, r, "A NAS mentési cél nincs beállítva.", true)
		return
	}
	if err := s.settings.UpdateOffboxStatus(func(o *settings.OffboxTarget) { o.EscrowState = "escrowed" }); err != nil {
		offboxRedirect(w, r, "A beállítás mentése sikertelen.", true)
		return
	}
	s.logger.Printf("[INFO] [web] off-box escrow confirmed — offsite runs enabled")
	offboxRedirect(w, r, "A kulcs letétbe helyezése megerősítve — a NAS-mentés mostantól futhat.", false)
}

// offboxInjectPasswordHandler pre-places a RECOVERED repo password at the offbox password path (fork-4 DR
// seam) so a subsequent configure uses it and the existing offsite repo opens. Operator/DR only; the value
// is never logged. Body: {password, force?}.
func (s *Server) offboxInjectPasswordHandler(w http.ResponseWriter, r *http.Request) {
	if s.backupMgr == nil {
		offboxRedirect(w, r, "A mentéskezelő nem elérhető.", true)
		return
	}
	_ = r.ParseForm()
	pw := r.FormValue("password")
	force := r.FormValue("force") == "on" || r.FormValue("force") == "true"
	if strings.TrimSpace(pw) == "" {
		offboxRedirect(w, r, "A repo jelszó kötelező.", true)
		return
	}
	if err := s.backupMgr.InjectOffboxPassword(pw, force); err != nil {
		offboxRedirect(w, r, "A jelszó beállítása sikertelen: "+err.Error(), true)
		return
	}
	s.logger.Printf("[INFO] [web] off-box repo password injected (DR pre-place, force=%v)", force)
	offboxRedirect(w, r, "A helyreállított repo jelszó beállítva.", false)
}

// offboxToggleHandler flips an app's off-box inclusion.
func (s *Server) offboxToggleHandler(w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	app := strings.TrimSpace(r.FormValue("app"))
	on := r.FormValue("enabled") == "on" || r.FormValue("enabled") == "true"
	if app == "" {
		offboxRedirect(w, r, "Hiányzó alkalmazás.", true)
		return
	}
	if err := s.settings.SetAppOffbox(app, on); err != nil {
		offboxRedirect(w, r, "A beállítás mentése sikertelen.", true)
		return
	}
	offboxRedirect(w, r, "A NAS-mentés beállítása frissítve.", false)
}

// offboxRunHandler triggers an off-box backup now (async — it can run for minutes).
func (s *Server) offboxRunHandler(w http.ResponseWriter, r *http.Request) {
	if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() {
		offboxRedirect(w, r, "A NAS mentési cél nincs beállítva.", true)
		return
	}
	// fork-4 atomicity: refuse the run until the repo password is escrowed under R.
	if !s.backupMgr.OffboxRunnable() {
		offboxRedirect(w, r, "A NAS-mentés a kulcs letétbe helyezésére vár.", true)
		return
	}
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Hour)
		defer cancel()
		if err := s.backupMgr.RunOffboxBackup(ctx); err != nil {
			s.logger.Printf("[WARN] [web] manual off-box backup failed: %v", err)
		}
	}()
	offboxRedirect(w, r, "A NAS-mentés elindult (a futás után az állapot frissül).", false)
}

// offboxRestoreHandler restores an app's off-box data to a scratch dir (non-destructive — does NOT
// overwrite live data; the operator inspects the restored files).
func (s *Server) offboxRestoreHandler(w http.ResponseWriter, r *http.Request) {
	if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() {
		offboxRedirect(w, r, "A NAS mentési cél nincs beállítva.", true)
		return
	}
	_ = r.ParseForm()
	app := strings.TrimSpace(r.FormValue("app"))
	if app == "" {
		offboxRedirect(w, r, "Hiányzó alkalmazás.", true)
		return
	}
	// Part B: fast-path refuse a concurrent op, then run async on a BACKGROUND context. The old code
	// bounded on r.Context()+30m — a proxy read-timeout then CANCELED the SFTP restore mid-flight
	// (worse than F4: not just an error page, an aborted restore). Background ctx fixes that.
	if s.backupMgr.IsRunning() {
		offboxRedirect(w, r, "Egy mentési/visszaállítási művelet már fut.", true)
		return
	}
	dest := filepath.Join(s.cfg.Paths.DataDir, "offbox-restore", app)
	s.backupMgr.BeginRestoreOp("offbox-restore", app)
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
		defer cancel()
		if err := s.backupMgr.RestoreOffbox(ctx, app, dest); err != nil {
			s.logger.Printf("[ERROR] [web] off-box restore %s (async): %v", app, err)
			s.backupMgr.EndRestoreOp(false, "A visszaállítás sikertelen: "+err.Error())
			return
		}
		s.logger.Printf("[INFO] [web] off-box restore %s completed (async) → %s", app, dest)
		s.backupMgr.EndRestoreOp(true, "A(z) "+app+" visszaállítva ide (ellenőrzésre): "+dest)
	}()
	offboxRedirect(w, r, "A NAS-visszaállítás elindult — az állapot itt frissül.", false)
}
