package agent

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

	"github.com/gin-gonic/gin"

	"security-center-backend/internal/model"
	"security-center-backend/internal/pkg/response"
)

// Rotate handles POST /agent/api-keys/:id/rotate (P2-T8): mints a replacement
// key carrying over the old key's name/scope/owner, then revokes the old one.
// The new plaintext is returned exactly once, in this response only.
func (h *Handler) Rotate(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid api key id")
		return
	}
	claims := currentUser(c)
	if claims == nil {
		response.Error(c, http.StatusUnauthorized, "not authenticated")
		return
	}

	ctx := c.Request.Context()
	var old *model.APIKey
	if slices.Contains(claims.Roles, "admin") {
		old, err = h.repo.FindByID(ctx, id)
	} else {
		old, err = h.repo.FindByIDForOwner(ctx, id, claims.UserID)
	}
	if err != nil {
		response.Error(c, http.StatusInternalServerError, "failed to load api key")
		return
	}
	if old == nil {
		response.Error(c, http.StatusNotFound, "api key not found")
		return
	}
	if old.Status == model.APIKeyStatusRevoked {
		response.Error(c, http.StatusConflict, "api key already revoked")
		return
	}

	// Renewal policy: keep the original lifecycle — when the old key had an
	// expiry, the new one lives for the same duration starting now
	// (now + (old.ExpiresAt - old.CreatedAt)); a permanent old key yields a
	// permanent new one, but only when permanent keys are allowed.
	var expiresAt *time.Time
	if old.ExpiresAt != nil {
		renewed := time.Now().Add(old.ExpiresAt.Sub(old.CreatedAt))
		expiresAt = &renewed
	} else if !h.cfg.AllowPermanentKey {
		response.Error(c, http.StatusBadRequest, "rotating a permanent api key is not allowed")
		return
	}

	// Same plaintext rule as Create (generatePlaintextKey in create.go):
	// "<prefix><slug(agentName)>-<16 random bytes hex>"; only the sha256 hash
	// and the first 16 chars are persisted.
	plaintext, err := h.generatePlaintextKey(old.AgentName)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, "failed to generate api key")
		return
	}
	newKey := &model.APIKey{
		KeyPrefix:       plaintext[:16],
		KeyHash:         sha256Hex(plaintext),
		AgentName:       old.AgentName,
		OwnerUserID:     old.OwnerUserID,
		PermissionScope: append(model.StringArray(nil), old.PermissionScope...),
		Status:          model.APIKeyStatusActive,
		ExpiresAt:       expiresAt,
	}
	if err := h.repo.Create(ctx, newKey); err != nil {
		response.Error(c, http.StatusInternalServerError, "failed to create rotated api key")
		return
	}

	// Revoke the old key only after the replacement exists. If this fails the
	// new key is kept (no rollback): the caller gets an error and can retry,
	// which beats deleting the just-minted key out from under them.
	if err := h.repo.Revoke(ctx, old.ID, old.OwnerUserID); err != nil {
		slog.Warn("api key rotated but revoking the old key failed",
			"old_key_id", old.ID, "new_key_id", newKey.ID, "error", err)
		response.Error(c, http.StatusInternalServerError, "api key rotated but failed to revoke the old key")
		return
	}

	response.Success(c, &CreateAPIKeyResponse{
		APIKeyResponse: *toAPIKeyResponse(newKey),
		APIKey:         plaintext,
	})
}
