// ui_host_delete.go — admin-band danger-zone host deletion (NS-01).
//
// Removes the host row from the store; FK cascades wipe schedules,
// jobs, snapshots metadata, source groups, alerts, host_credentials,
// host_repo_maintenance, host_repo_stats, and the schedule junction.
// Also closes the host's active WS connection so the agent's bearer
// stops being usable in the same tick (the bearer hash lives on the
// hosts row itself, so DeleteHost already revokes it for any future
// auth attempt — closing the live socket is the courtesy that drops
// the in-flight session).
//
// Audit-logged with action="host.deleted" so the trail records who
// performed the deletion and against which host.
package http

import (
	"encoding/json"
	"errors"
	"log/slog"
	stdhttp "net/http"
	"strings"
	"time"

	"github.com/oklog/ulid/v2"

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

func (s *Server) handleUIHostDelete(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	u := s.requireUIUser(w, r)
	if u == nil {
		return
	}
	host, ok := s.loadHostForUI(w, r)
	if !ok {
		return
	}
	if err := r.ParseForm(); err != nil {
		stdhttp.Error(w, "bad request", stdhttp.StatusBadRequest)
		return
	}
	confirm := strings.TrimSpace(r.PostForm.Get("confirm_hostname"))
	if confirm != host.Name {
		// Mismatch — bounce back to host detail with a flash via the
		// query string. The detail page doesn't render an error banner
		// today; rather than thread a new field through the page model
		// for one site, we rely on the JS confirm() the form already
		// shows, plus a 303 back to the host page so the operator can
		// see they're still there. Surfacing as a 400 with a tidy
		// message keeps the audit trail clean.
		stdhttp.Error(w,
			"hostname confirmation did not match — go back and re-type",
			stdhttp.StatusBadRequest)
		return
	}

	// Drop any live WS session before pulling the row so the agent
	// gets a clean close rather than discovering the rug-pull on the
	// next read. A nil Conn just means the agent was already offline.
	if s.deps.Hub != nil {
		if c := s.deps.Hub.Conn(host.ID); c != nil {
			_ = c.Close()
		}
	}

	if err := s.deps.Store.DeleteHost(r.Context(), host.ID); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			// Race: someone else deleted it between loadHostForUI and
			// here. Treat as success.
			stdhttp.Redirect(w, r, "/", stdhttp.StatusSeeOther)
			return
		}
		slog.Error("ui host delete: store", "host_id", host.ID, "err", err)
		stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
		return
	}

	uid := u.ID
	hostID := host.ID
	// Stash the host name in the audit payload so an operator reading
	// the trail later sees *which* host was removed even though the
	// row no longer exists.
	payload, _ := json.Marshal(struct {
		Name string `json:"name"`
	}{Name: host.Name})
	_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
		ID:         ulid.Make().String(),
		UserID:     &uid,
		Actor:      "user",
		Action:     "host.deleted",
		TargetKind: ptr("host"),
		TargetID:   &hostID,
		TS:         time.Now().UTC(),
		Payload:    payload,
	})

	if wantsHTML(r) {
		w.Header().Set("HX-Redirect", "/")
		w.WriteHeader(stdhttp.StatusNoContent)
		return
	}
	stdhttp.Redirect(w, r, "/", stdhttp.StatusSeeOther)
}
