package http

import (
	"encoding/json"
	stdhttp "net/http"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/oklog/ulid/v2"

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

// snapshotDiffRequest is the JSON body for POST .../snapshots/diff.
// Either short or long snapshot IDs are accepted (restic's diff
// command takes both).
type snapshotDiffRequest struct {
	SnapshotA string `json:"snapshot_a"`
	SnapshotB string `json:"snapshot_b"`
}

// handleSnapshotDiff dispatches a JobDiff. Output streams as
// log.stream lines to the standard live job page; the operator reads
// the diff text directly there. Behaves like the run-now endpoints:
// 503 if the host is offline, 400 if the IDs are missing, 422 if
// they're not in the host's snapshot list (we don't want operators
// running diffs against arbitrary snapshot strings).
func (s *Server) handleSnapshotDiff(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	user, ok := s.requireUser(r)
	if !ok {
		writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorised", "")
		return
	}
	hostID := chi.URLParam(r, "id")
	host, err := s.deps.Store.GetHost(r.Context(), hostID)
	if err != nil {
		writeJSONError(w, stdhttp.StatusNotFound, "host_not_found", "")
		return
	}

	var req snapshotDiffRequest
	// HTMX form posts arrive as application/x-www-form-urlencoded;
	// the JSON shape is also accepted for REST callers.
	ct := r.Header.Get("Content-Type")
	if strings.HasPrefix(ct, "application/x-www-form-urlencoded") {
		if err := r.ParseForm(); err != nil {
			writeJSONError(w, stdhttp.StatusBadRequest, "invalid_form", err.Error())
			return
		}
		req.SnapshotA = strings.TrimSpace(r.PostForm.Get("snapshot_a"))
		req.SnapshotB = strings.TrimSpace(r.PostForm.Get("snapshot_b"))
	} else {
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeJSONError(w, stdhttp.StatusBadRequest, "invalid_json", err.Error())
			return
		}
		req.SnapshotA = strings.TrimSpace(req.SnapshotA)
		req.SnapshotB = strings.TrimSpace(req.SnapshotB)
	}
	if req.SnapshotA == "" || req.SnapshotB == "" {
		writeJSONError(w, stdhttp.StatusBadRequest, "missing_snapshot",
			"snapshot_a and snapshot_b are both required")
		return
	}
	if req.SnapshotA == req.SnapshotB {
		writeJSONError(w, stdhttp.StatusUnprocessableEntity, "same_snapshot",
			"diff requires two different snapshots")
		return
	}

	// Validate the IDs are known to this host. Match on long ID, short
	// ID, or any prefix match — operators sometimes paste a 6-char
	// shortened form.
	snaps, err := s.deps.Store.ListSnapshotsByHost(r.Context(), host.ID)
	if err != nil {
		writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
		return
	}
	resolveID := func(idOrShort string) string {
		for _, s := range snaps {
			if s.ID == idOrShort || s.ShortID == idOrShort {
				return s.ID
			}
		}
		// Prefix fallback (operator pasted 6 chars of a long id).
		for _, s := range snaps {
			if strings.HasPrefix(s.ID, idOrShort) {
				return s.ID
			}
		}
		return ""
	}
	a := resolveID(req.SnapshotA)
	b := resolveID(req.SnapshotB)
	if a == "" || b == "" {
		writeJSONError(w, stdhttp.StatusUnprocessableEntity, "snapshot_not_found",
			"one or both snapshot ids are not in this host's snapshot list")
		return
	}

	if !s.deps.Hub.Connected(host.ID) {
		writeJSONError(w, stdhttp.StatusServiceUnavailable, "host_offline",
			"agent is not connected; try again when it reconnects")
		return
	}

	jobID := ulid.Make().String()
	now := time.Now().UTC()
	if err := s.deps.Store.CreateJob(r.Context(), store.Job{
		ID: jobID, HostID: host.ID, Kind: string(api.JobDiff),
		ActorKind: "user", ActorID: &user.ID, CreatedAt: now,
	}); err != nil {
		writeJSONError(w, stdhttp.StatusInternalServerError, "internal", err.Error())
		return
	}
	env, err := api.Marshal(api.MsgCommandRun, jobID, api.CommandRunPayload{
		JobID: jobID, Kind: api.JobDiff,
		Diff: &api.DiffPayload{SnapshotA: a, SnapshotB: b},
	})
	if err != nil {
		writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "")
		return
	}
	if err := s.deps.Hub.Send(r.Context(), host.ID, env); err != nil {
		writeJSONError(w, stdhttp.StatusServiceUnavailable, "host_offline", err.Error())
		return
	}
	_ = s.deps.Store.AppendAudit(r.Context(), store.AuditEntry{
		ID:         ulid.Make().String(),
		UserID:     &user.ID,
		Actor:      "user",
		Action:     "host.snapshot_diff",
		TargetKind: ptr("host"),
		TargetID:   &host.ID,
		TS:         now,
	})

	jobURL := "/jobs/" + jobID
	if r.Header.Get("HX-Request") == "true" {
		w.Header().Set("HX-Redirect", jobURL)
		w.WriteHeader(stdhttp.StatusNoContent)
		return
	}
	writeJSON(w, stdhttp.StatusAccepted, map[string]string{
		"job_id":  jobID,
		"job_url": jobURL,
	})
}
