// repo_ops.go — operator-triggered Run-now for repo-level operations:
// prune, check, unlock. Backed by the same dispatchJobWithPayload
// pipeline as backup, with an extra step for prune: push admin creds
// first if they're set, refuse loudly if they aren't.
package http

import (
	"errors"
	"log/slog"
	stdhttp "net/http"
	"strconv"

	"github.com/go-chi/chi/v5"

	"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
	"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
)

// handleRunRepoPrune — POST /api/hosts/{id}/repo/prune (and the HTMX
// twin outside /api). Pushes the host's admin credentials down the WS,
// then dispatches a prune command.run with RequiresAdminCreds=true.
func (s *Server) handleRunRepoPrune(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	user, ok := s.requireUser(r)
	if !ok {
		if wantsHTML(r) {
			stdhttp.Redirect(w, r, "/login", stdhttp.StatusSeeOther)
			return
		}
		writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorised", "")
		return
	}
	hostID := chi.URLParam(r, "id")
	if hostID == "" {
		s.runOpError(w, r, stdhttp.StatusBadRequest, "missing_id", "")
		return
	}

	// Push admin creds first. ErrNotFound → operator hasn't set them
	// yet. Other errors → likely the host is offline or a decrypt fail.
	if err := s.pushAdminCredsToAgent(r.Context(), hostID); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			s.runOpError(w, r, stdhttp.StatusBadRequest, "admin_creds_required",
				"set admin credentials on the Repo page before running prune")
			return
		}
		// Hub.Send failure (offline) or decrypt failure — surface a
		// generic offline message so the operator retries when the
		// agent is back.
		slog.Warn("prune: push admin creds failed", "host_id", hostID, "err", err)
		s.runOpError(w, r, stdhttp.StatusServiceUnavailable, "host_offline",
			"agent is not currently connected; try again when it reconnects")
		return
	}

	res, status, code, msg := s.dispatchJobWithPayload(r.Context(), user, hostID, api.JobPrune, nil,
		api.CommandRunPayload{RequiresAdminCreds: true})
	if code != "" {
		s.runOpError(w, r, status, code, msg)
		return
	}
	s.runOpRedirect(w, r, res)
}

// handleRunRepoCheck — POST /api/hosts/{id}/repo/check. Pulls
// check_subset_pct from host_repo_maintenance for the host (operator
// can override via ?subset=N query param, clamped 0..100). Dispatches
// with the chosen subset in CommandRunPayload.Args[0].
func (s *Server) handleRunRepoCheck(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	user, ok := s.requireUser(r)
	if !ok {
		if wantsHTML(r) {
			stdhttp.Redirect(w, r, "/login", stdhttp.StatusSeeOther)
			return
		}
		writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorised", "")
		return
	}
	hostID := chi.URLParam(r, "id")
	if hostID == "" {
		s.runOpError(w, r, stdhttp.StatusBadRequest, "missing_id", "")
		return
	}

	m, err := s.deps.Store.GetRepoMaintenance(r.Context(), hostID)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			// Maintenance row should auto-seed at enrollment. If it's
			// missing, surface a clear error rather than guessing 0%.
			s.runOpError(w, r, stdhttp.StatusInternalServerError, "no_maintenance_row",
				"host has no repo-maintenance config; was the host fully enrolled?")
			return
		}
		s.runOpError(w, r, stdhttp.StatusInternalServerError, "internal", "")
		return
	}
	subset := m.CheckSubsetPct
	if q := r.URL.Query().Get("subset"); q != "" {
		if n, err2 := strconv.Atoi(q); err2 == nil {
			if n < 0 {
				n = 0
			}
			if n > 100 {
				n = 100
			}
			subset = n
		}
		// Non-numeric ?subset silently falls back to DB value.
	}

	res, status, code, msg := s.dispatchJobWithPayload(r.Context(), user, hostID, api.JobCheck, nil,
		api.CommandRunPayload{Args: []string{strconv.Itoa(subset)}})
	if code != "" {
		s.runOpError(w, r, status, code, msg)
		return
	}
	s.runOpRedirect(w, r, res)
}

// handleRunRepoUnlock — POST /api/hosts/{id}/repo/unlock. No admin
// creds required — restic unlock works with the everyday user.
func (s *Server) handleRunRepoUnlock(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	user, ok := s.requireUser(r)
	if !ok {
		if wantsHTML(r) {
			stdhttp.Redirect(w, r, "/login", stdhttp.StatusSeeOther)
			return
		}
		writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorised", "")
		return
	}
	hostID := chi.URLParam(r, "id")
	if hostID == "" {
		s.runOpError(w, r, stdhttp.StatusBadRequest, "missing_id", "")
		return
	}

	res, status, code, msg := s.dispatchJobWithPayload(r.Context(), user, hostID, api.JobUnlock, nil,
		api.CommandRunPayload{})
	if code != "" {
		s.runOpError(w, r, status, code, msg)
		return
	}
	s.runOpRedirect(w, r, res)
}

// runOpRedirect: HTMX → HX-Redirect to /jobs/{id}; JSON → 202 + JSON
// body. Mirrors handleRunSourceGroup's tail.
func (s *Server) runOpRedirect(w stdhttp.ResponseWriter, r *stdhttp.Request, res runNowResponse) {
	if wantsHTML(r) {
		w.Header().Set("HX-Redirect", "/jobs/"+res.JobID)
		w.WriteHeader(stdhttp.StatusNoContent)
		return
	}
	writeJSON(w, stdhttp.StatusAccepted, res)
}

// runOpError: HTMX → plain-text status; JSON → standard envelope.
// Mirrors runGroupError.
func (s *Server) runOpError(w stdhttp.ResponseWriter, r *stdhttp.Request, status int, code, msg string) {
	if wantsHTML(r) {
		stdhttp.Error(w, msg, status)
		return
	}
	writeJSONError(w, status, code, msg)
}
