package agent

import (
	"errors"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	"github.com/tidbcloud/AgentTeam/internal/runtimecontrol"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
)

// ReviewContextHandler owns the agent-runtime review-context endpoints used by
// the desktop MCP. The single-table model binds each review context to at most
// one thread; review_list/review_add/review_delete share one review context id
// (delete closes the agentteam).
type ReviewContextHandler struct {
	auth      AgentAuthenticator
	store     ReviewContextStore
	validator ReviewContextValidator
}

// GetConversation returns the conversation-scoped review context. Only thread
// subjects carry a review context in the new model.
func (h *ReviewContextHandler) GetConversation(c *gin.Context) {
	aa, subjectType, subjectID, ok := h.conversationContext(c)
	if !ok {
		return
	}
	if subjectType != agent.ScopeTypeThread {
		c.JSON(http.StatusOK, dto.AgentConversationReviewContextResponse{})
		return
	}
	if err := h.store.EnsureAgentThreadAccess(c.Request.Context(), aa.WorkspaceID, aa.AgentID, subjectID); err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	contexts, err := h.store.ListReviewContextsForThreadForAgent(c.Request.Context(), aa.WorkspaceID, subjectID)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	resp := dto.AgentConversationReviewContextResponse{}
	if len(contexts) > 0 {
		resp.Context = converter.ToReviewContext(&contexts[0])
	}
	c.JSON(http.StatusOK, resp)
}

// UpdateConversation validates and creates a thread-bound review context.
func (h *ReviewContextHandler) UpdateConversation(c *gin.Context) {
	aa, subjectType, subjectID, ok := h.conversationContext(c)
	if !ok {
		return
	}
	if subjectType != agent.ScopeTypeThread {
		c.JSON(http.StatusBadRequest, agentError("review context is only supported for reply threads; use a thread target"))
		return
	}
	if err := h.store.EnsureAgentThreadAccess(c.Request.Context(), aa.WorkspaceID, aa.AgentID, subjectID); err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	var request dto.AgentReviewContextRequest
	if !bindJSON(c, &request) {
		return
	}
	params, err := reviewParamsFromLegacyRequest(request)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	params.ThreadID = strings.TrimSpace(subjectID)
	if err := h.validateReviewContext(c.Request.Context(), aa, params); err != nil {
		if errors.Is(err, runtimecontrol.ErrMachineOffline) {
			c.JSON(http.StatusServiceUnavailable, agentError("Machine is offline"))
			return
		}
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	rc, err := h.store.CreateReviewContextForAgent(c.Request.Context(), aa, params)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, dto.AgentConversationReviewContextResponse{Context: converter.ToReviewContext(&rc)})
}

// GetTask lists review contexts attached to one canonical Task scope.
func (h *ReviewContextHandler) GetTask(c *gin.Context) {
	aa, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	taskScopeID := strings.TrimSpace(c.Param("taskScopeId"))
	if !strings.HasPrefix(taskScopeID, "tsk_") {
		c.JSON(http.StatusBadRequest, agentError("taskScopeId must be a canonical task scope"))
		return
	}
	contexts, err := h.store.ListReviewContextsForTaskForAgent(c.Request.Context(), aa, taskScopeID)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, gin.H{"contexts": converter.ToReviewContexts(contexts)})
}

// UpdateTask creates or atomically refreshes the authenticated agent's review
// context slot for a canonical Task scope.
func (h *ReviewContextHandler) UpdateTask(c *gin.Context) {
	aa, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	taskScopeID := strings.TrimSpace(c.Param("taskScopeId"))
	if !strings.HasPrefix(taskScopeID, "tsk_") {
		c.JSON(http.StatusBadRequest, agentError("taskScopeId must be a canonical task scope"))
		return
	}
	if err := h.store.EnsureAgentTaskAccess(c.Request.Context(), aa, taskScopeID); err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	var request dto.AgentReviewContextRequest
	if !bindJSON(c, &request) {
		return
	}
	if request.TaskScope != nil && (strings.TrimSpace(request.TaskScope.Kind) != agent.ScopeTypeTask || strings.TrimSpace(request.TaskScope.ScopeID) != taskScopeID) {
		c.JSON(http.StatusBadRequest, agentError("taskScope does not match the task route"))
		return
	}
	params, err := reviewParamsFromLegacyRequest(request)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	params.TaskScopeID = taskScopeID
	if err := h.validateReviewContext(c.Request.Context(), aa, params); err != nil {
		if errors.Is(err, runtimecontrol.ErrMachineOffline) {
			c.JSON(http.StatusServiceUnavailable, agentError("Machine is offline"))
			return
		}
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	rc, err := h.store.CreateReviewContextForAgent(c.Request.Context(), aa, params)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, dto.AgentConversationReviewContextResponse{Context: converter.ToReviewContext(&rc)})
}

// DeleteByID deletes a single review context by its id (DELETE
// /review-contexts/:contextId). This is what review_delete calls, closing the
// list→add→delete agentteam on a single id.
func (h *ReviewContextHandler) DeleteByID(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	contextID := strings.TrimSpace(c.Param("contextId"))
	if contextID == "" {
		c.JSON(http.StatusBadRequest, agentError("context id is required"))
		return
	}
	if err := h.store.DeleteReviewContextForAgent(c.Request.Context(), authenticatedAgent, contextID); err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, dto.AgentConversationReviewContextResponse{})
}

func (h *ReviewContextHandler) GetThread(c *gin.Context)    { h.handleThread(c, h.GetConversation) }
func (h *ReviewContextHandler) UpdateThread(c *gin.Context) { h.handleThread(c, h.UpdateConversation) }

// DeleteThread clears the calling agent's review contexts for a reply thread.
func (h *ReviewContextHandler) DeleteThread(c *gin.Context) {
	aa, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	threadChannelID := strings.TrimSpace(c.Param("threadChannelId"))
	if threadChannelID == "" {
		c.JSON(http.StatusBadRequest, agentError("threadChannelId is required"))
		return
	}
	if err := h.store.EnsureAgentThreadAccess(c.Request.Context(), aa.WorkspaceID, aa.AgentID, threadChannelID); err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	contexts, err := h.store.ListReviewContextsForThreadForAgent(c.Request.Context(), aa.WorkspaceID, threadChannelID)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	// Best-effort: an agent may delete only its own review contexts.
	for _, rc := range contexts {
		_ = h.store.DeleteReviewContextForAgent(c.Request.Context(), aa, rc.ID)
	}
	c.JSON(http.StatusOK, dto.AgentConversationReviewContextResponse{})
}

const (
	defaultAgentReviewContextListLimit = 20
	maxAgentReviewContextListLimit     = 100
)

type agentReviewContextListCursor struct {
	Version   int       `json:"v"`
	ThreadID  string    `json:"threadId"`
	UpdatedAt time.Time `json:"updatedAt"`
	ID        string    `json:"id"`
}

func parseAgentReviewContextListOptions(c *gin.Context) (agent.ReviewContextListOptions, error) {
	limit := defaultAgentReviewContextListLimit
	if raw := strings.TrimSpace(c.Query("limit")); raw != "" {
		parsed, err := strconv.Atoi(raw)
		if err != nil || parsed < 1 || parsed > maxAgentReviewContextListLimit {
			return agent.ReviewContextListOptions{}, badRequest("limit must be between 1 and 100")
		}
		limit = parsed
	}
	options := agent.ReviewContextListOptions{Limit: limit}
	rawCursor := strings.TrimSpace(c.Query("cursor"))
	if rawCursor == "" {
		return options, nil
	}
	var cursor agentReviewContextListCursor
	if err := decodeAgentCollectionCursor(rawCursor, "review context", &cursor); err != nil || cursor.Version != 1 || cursor.ThreadID != strings.TrimSpace(c.Param("threadChannelId")) || cursor.UpdatedAt.IsZero() || strings.TrimSpace(cursor.ID) == "" {
		return agent.ReviewContextListOptions{}, badRequest("invalid review context cursor")
	}
	options.CursorUpdatedAt = cursor.UpdatedAt.UTC()
	options.CursorID = strings.TrimSpace(cursor.ID)
	return options, nil
}

// ListThread returns one bounded page of review contexts bound to a reply
// thread. The ordering is updatedAt DESC, id DESC and the cursor is strict.
func (h *ReviewContextHandler) ListThread(c *gin.Context) {
	authenticatedAgent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	threadChannelID := strings.TrimSpace(c.Param("threadChannelId"))
	if threadChannelID == "" {
		c.JSON(http.StatusBadRequest, agentError("threadChannelId is required"))
		return
	}
	if err := h.store.EnsureAgentThreadAccess(c.Request.Context(), authenticatedAgent.WorkspaceID, authenticatedAgent.AgentID, threadChannelID); err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	options, err := parseAgentReviewContextListOptions(c)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	page, err := h.store.ListReviewContextsForThreadForAgentPage(c.Request.Context(), authenticatedAgent.WorkspaceID, threadChannelID, options)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	nextCursor := ""
	if page.HasMore && len(page.Contexts) > 0 {
		last := page.Contexts[len(page.Contexts)-1]
		nextCursor, err = encodeAgentCollectionCursor(agentReviewContextListCursor{Version: 1, ThreadID: threadChannelID, UpdatedAt: last.UpdatedAt.UTC(), ID: last.ID})
		if err != nil {
			c.JSON(http.StatusInternalServerError, agentError(err.Error()))
			return
		}
	}
	c.JSON(http.StatusOK, gin.H{
		"contexts":   converter.ToReviewContexts(page.Contexts),
		"hasMore":    page.HasMore,
		"nextCursor": nextCursor,
	})
}

func (h *ReviewContextHandler) handleThread(c *gin.Context, handler gin.HandlerFunc) {
	c.Params = append(c.Params,
		gin.Param{Key: "subjectType", Value: agent.ScopeTypeThread},
		gin.Param{Key: "subjectId", Value: strings.TrimSpace(c.Param("threadChannelId"))},
	)
	handler(c)
}
