package api

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

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

	"github.com/alexei/tinyforge/internal/auth"
	"github.com/alexei/tinyforge/internal/store"
	"github.com/alexei/tinyforge/internal/workload/plugin"
)

// parseOffset parses a pagination offset, clamping anything invalid or
// negative to 0. parseLimit (secrets.go) handles the limit half.
func parseOffset(raw string) int {
	n, err := strconv.Atoi(raw)
	if err != nil || n < 0 {
		return 0
	}
	return n
}

// rollbackCapableKinds is the single source of truth for which source kinds
// support reference-pinned redeploy. The image source resolves
// intent.Reference as the tag, so replaying a prior tag is a real rollback.
// static/dockerfile clone branch HEAD and cannot yet check out an arbitrary
// commit (a later phase); compose has no single artifact handle.
var rollbackCapableKinds = map[string]bool{"image": true}

// RollbackCapable reports whether a source kind supports one-click rollback.
// Used by both the list response (per-row `rollbackable` flag) and the
// rollback guard so the UI and the server never disagree.
func RollbackCapable(sourceKind string) bool { return rollbackCapableKinds[sourceKind] }

// listWorkloadDeploys handles GET /api/workloads/{id}/deploys. Read-only,
// open to any authenticated user (mirrors the per-workload events feed).
// Returns the structured deploy ledger newest-first with a server-computed
// `rollbackable` flag per row.
func (s *Server) listWorkloadDeploys(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if id == "" {
		respondError(w, http.StatusBadRequest, "workload id is required")
		return
	}

	q := r.URL.Query()
	limit := parseLimit(q.Get("limit"), 50, 200)
	offset := parseOffset(q.Get("offset"))

	rows, err := s.store.ListDeployHistory(id, limit, offset)
	if err != nil {
		slog.Error("failed to list deploy history", "workload", id, "error", err)
		respondError(w, http.StatusInternalServerError, "failed to list deploy history")
		return
	}
	for i := range rows {
		rows[i].Rollbackable = rows[i].Outcome == "success" &&
			rows[i].Reference != "" &&
			RollbackCapable(rows[i].SourceKind)
	}
	respondJSON(w, http.StatusOK, rows)
}

// rollbackWorkload handles POST /api/workloads/{id}/rollback. Admin-only
// (same gate as /deploy). Body: {"deploy_id": <id>}. It resolves the pinned
// reference from a prior successful, rollback-capable ledger row belonging
// to this workload and replays it as a `rollback`-reason deploy.
func (s *Server) rollbackWorkload(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")

	row, err := s.store.GetWorkloadByID(id)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "workload")
			return
		}
		respondError(w, http.StatusInternalServerError, "get workload")
		return
	}
	if row.SourceKind == "" {
		respondError(w, http.StatusBadRequest, "workload has no source_kind; cannot roll back")
		return
	}

	var body struct {
		DeployID int64 `json:"deploy_id"`
	}
	if !decodeJSONStrict(w, r, &body) {
		return
	}
	if body.DeployID <= 0 {
		respondError(w, http.StatusBadRequest, "deploy_id is required")
		return
	}

	entry, err := s.store.GetDeployHistory(body.DeployID)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "deploy history entry")
			return
		}
		respondError(w, http.StatusInternalServerError, "get deploy history")
		return
	}
	// No cross-workload replay: the entry must belong to the path workload.
	if entry.WorkloadID != id {
		respondError(w, http.StatusBadRequest, "deploy entry does not belong to this workload")
		return
	}
	if entry.Outcome != "success" {
		respondError(w, http.StatusBadRequest, "cannot roll back to a failed deploy")
		return
	}
	if entry.Reference == "" || !RollbackCapable(row.SourceKind) {
		respondError(w, http.StatusBadRequest, "this deploy is not rollback-capable")
		return
	}

	actor := "manual"
	if claims, ok := auth.ClaimsFromContext(r.Context()); ok && claims.Username != "" {
		actor = claims.Username
	}
	intent := plugin.DeploymentIntent{
		Reason:    "rollback",
		Reference: entry.Reference,
		Metadata: map[string]string{
			"note":        "rollback to " + entry.Reference,
			"rollback_of": strconv.FormatInt(entry.ID, 10),
		},
		TriggeredAt: time.Now().UTC(),
		TriggeredBy: actor,
	}
	if err := s.deployer.DispatchPlugin(r.Context(), toPluginWorkload(row), intent); err != nil {
		// Raw error stays in the server log; client gets a generic message
		// (the wrapped error can carry registry-auth bytes).
		slog.Warn("rollback dispatch failed", "workload", id, "actor", actor,
			"reference", entry.Reference, "error", err)
		respondError(w, http.StatusInternalServerError, "rollback failed; see server logs")
		return
	}
	respondJSON(w, http.StatusAccepted, map[string]any{
		"workload_id":  id,
		"reference":    entry.Reference,
		"rollback_of":  entry.ID,
		"triggered_by": actor,
	})
}
