package agent

import (
	"log/slog"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/dto/converter"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	agentstore "github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

// MessageHandler owns agent message send, history, and search endpoints.
type MessageHandler struct {
	auth             AgentAuthenticator
	store            MessageStore
	rewindStore      RewindStore
	recoveryGC       MessageRecoveryGC
	fanout           MessageFanout
	runtime          RuntimeOverlay
	logger           *slog.Logger
	minDaemonVersion string
	workflowReplies  WorkflowReplyService
}

// The embedded camelCase DTO is authoritative. These aliases only preserve
// compatibility with released daemons and are derived from the same values.
type agentWorkspaceSnapshotCompatibilityResponse struct {
	dto.AgentWorkspaceSnapshotResponse
	LegacyHasMore    bool   `json:"has_more,omitempty"`
	LegacyNextCursor string `json:"next_cursor,omitempty"`
}

func workspaceSnapshotCompatibilityResponse(response dto.AgentWorkspaceSnapshotResponse) agentWorkspaceSnapshotCompatibilityResponse {
	return agentWorkspaceSnapshotCompatibilityResponse{
		AgentWorkspaceSnapshotResponse: response,
		LegacyHasMore:                  response.HasMore,
		LegacyNextCursor:               response.NextCursor,
	}
}

// Create persists an agent-authored message and emits the resulting delivery fan-out.
func (h *MessageHandler) Create(c *gin.Context) {
	agent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	if !requireAgentDaemonVersion(c, agent, h.minDaemonVersion) {
		return
	}
	var request dto.AgentSendMessageRequest
	if !bindJSON(c, &request) {
		return
	}
	reference, err := domain.ParseMessageTargetReference(
		strings.TrimSpace(request.Target),
	)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	if reference.Kind == domain.MessageTargetKindWorkflow {
		if h.workflowReplies == nil {
			c.JSON(http.StatusForbidden, agentError(message.ErrForbidden.Error()))
			return
		}
		if err := validateWorkflowReplyRequest(request); err != nil {
			c.JSON(HTTPStatus(err), agentError(err.Error()))
			return
		}
		result, err := h.sendWorkflowReplyWithFreshness(
			c.Request.Context(), agent, reference.WorkflowPlanID, request,
		)
		if err != nil {
			c.JSON(HTTPStatus(err), agentError(err.Error()))
			return
		}
		c.JSON(http.StatusOK, agentSendMessageResponseFromResult(result))
		return
	}
	params, err := writeMessageParamsFromReference(request, reference)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	if isFreshnessCapableSendRequest(request) {
		if err := validateFreshnessCapableSendRequest(request); err != nil {
			c.JSON(HTTPStatus(err), agentError(err.Error()))
			return
		}
		result, err := h.sendMessageWithFreshness(c.Request.Context(), agent, params)
		if err != nil {
			c.JSON(HTTPStatus(err), agentError(err.Error()))
			return
		}
		c.JSON(http.StatusOK, agentSendMessageResponseFromResult(result))
		return
	}
	messageID, err := h.sendMessage(c.Request.Context(), agent, params)
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, dto.AgentSendMessageResponse{MessageID: messageID})
}

func isFreshnessCapableSendRequest(request dto.AgentSendMessageRequest) bool {
	return request.Freshness != nil || strings.TrimSpace(request.ClientMessageID) != ""
}

func validateFreshnessCapableSendRequest(request dto.AgentSendMessageRequest) error {
	if strings.TrimSpace(request.ClientMessageID) == "" {
		return badRequest("client_message_id_required")
	}
	if request.Freshness == nil {
		return badRequest("freshness_required")
	}
	if strings.TrimSpace(request.Freshness.TargetKey) == "" {
		return badRequest("target_key_required")
	}
	if request.Freshness.SeenUpToSeq < 0 {
		return badRequest("seen_up_to_seq_invalid")
	}
	const maxSourceMessageIDs = 128
	if len(request.Freshness.SourceMessageIDs) > maxSourceMessageIDs {
		return badRequest("source_message_ids_too_many")
	}
	return nil
}

func validateWorkflowReplyRequest(request dto.AgentSendMessageRequest) error {
	if len(request.AttachmentIDs) != 0 ||
		strings.TrimSpace(request.TeamInvokeTeamID) != "" {
		return badRequest("workflow replies support text only")
	}
	return validateFreshnessCapableSendRequest(request)
}

func agentSendMessageResponseFromResult(result message.SendMessageResult) dto.AgentSendMessageResponse {
	return dto.AgentSendMessageResponse{
		State:                result.State,
		MessageID:            result.MessageID,
		TargetKey:            result.TargetKey,
		ClientMessageID:      result.ClientMessageID,
		SeenUpToSeq:          result.SeenUpToSeq,
		AcceptedSeq:          result.AcceptedSeq,
		LatestSeq:            result.LatestSeq,
		NeedsRefreshUntilSeq: result.NeedsRefreshUntilSeq,
	}
}

// ResolveChannel maps a daemon target such as #channel to its canonical scope
// id. The route keeps its historical resolve-channel name, but the wire
// response is scope-only.
func (h *MessageHandler) ResolveChannel(c *gin.Context) {
	agent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	if !requireAgentDaemonVersion(c, agent, h.minDaemonVersion) {
		return
	}
	var request dto.AgentResolveChannelRequest
	if !bindJSON(c, &request) {
		return
	}
	target := strings.TrimSpace(request.Target)
	if target == "" {
		c.JSON(http.StatusBadRequest, agentError("target is required"))
		return
	}
	var scopeID string
	var err error
	switch purpose := strings.TrimSpace(request.Purpose); purpose {
	case "":
		reference, parseErr := domain.ParseMessageTargetReference(target)
		if parseErr != nil {
			err = badRequest(parseErr.Error())
			break
		}
		if reference.Kind == domain.MessageTargetKindWorkflow {
			if h.workflowReplies == nil {
				err = message.ErrForbidden
				break
			}
			scopeID, err = h.workflowReplies.ResolveAgentWorkflowReplyTargetScopeID(
				c.Request.Context(), agent, reference.WorkflowPlanID,
			)
			break
		}
		scopeID, err = h.store.ResolveWritableAgentTargetScopeID(c.Request.Context(), agent, target)
	case "review_context":
		scopeID, err = h.store.ResolveAgentReviewSubjectScopeID(c.Request.Context(), agent, target)
	default:
		c.JSON(http.StatusBadRequest, agentError("unsupported resolve purpose"))
		return
	}
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, dto.AgentResolveChannelResponse{ScopeID: scopeID, TargetKey: scopeID})
}

// GetWorkspace returns the agent-visible workspace actors and channel membership snapshot.
func (h *MessageHandler) GetWorkspace(c *gin.Context) {
	agent, ok := authenticateWorkspace(c, h.auth)
	if !ok {
		return
	}
	if !requireAgentWorkspaceDaemonVersion(c, agent, h.minDaemonVersion) {
		return
	}
	limit, err := ParseLimit(c.Query("limit"), 20)
	if err != nil {
		c.JSON(http.StatusBadRequest, dto.AgentWorkspaceSnapshotResponse{Error: err.Error(), Channels: []dto.WorkspaceChannel{}, Agents: []dto.WorkspaceActor{}, Humans: []dto.WorkspaceActor{}})
		return
	}
	if limit > 100 {
		limit = 100
	}
	request := agentstore.WorkspaceSnapshotRequest{
		Kind:   strings.TrimSpace(c.DefaultQuery("kind", "summary")),
		Query:  strings.TrimSpace(c.Query("query")),
		Limit:  limit,
		Cursor: strings.TrimSpace(c.Query("cursor")),
	}
	var snapshot agentstore.WorkspaceSnapshot
	if pager, ok := h.store.(WorkspaceSnapshotPager); ok {
		snapshot, err = pager.GetAgentWorkspaceSnapshotPage(c.Request.Context(), agent, request)
	} else {
		snapshot, err = h.store.GetAgentWorkspaceSnapshot(c.Request.Context(), agent)
	}
	if err != nil {
		c.JSON(HTTPStatus(err), dto.AgentWorkspaceSnapshotResponse{Error: err.Error(), Channels: []dto.WorkspaceChannel{}, Agents: []dto.WorkspaceActor{}, Humans: []dto.WorkspaceActor{}})
		return
	}
	api.OverlayWorkspaceActorsRuntime(snapshot.Agents, h.runtime)
	c.JSON(http.StatusOK, workspaceSnapshotCompatibilityResponse(dto.AgentWorkspaceSnapshotResponse{
		Channels:   converter.ToWorkspaceChannels(snapshot.Channels),
		Agents:     converter.ToWorkspaceActors(snapshot.Agents),
		Humans:     converter.ToWorkspaceActors(snapshot.Humans),
		Limits:     converter.ToWorkspaceSnapshotLimits(snapshot.Limits),
		Kind:       snapshot.Kind,
		Limit:      snapshot.Limit,
		HasMore:    snapshot.HasMore,
		NextCursor: snapshot.NextCursor,
	}))
}

// ListHistory returns a bounded page of messages from a channel, DM, or reply thread.
func (h *MessageHandler) ListHistory(c *gin.Context) {
	agent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	if !requireAgentDaemonVersion(c, agent, h.minDaemonVersion) {
		return
	}
	channel := strings.TrimSpace(c.Query("channel"))
	if channel == "" && !usesScopeAPI(c) {
		c.JSON(http.StatusBadRequest, agentError("channel is required"))
		return
	}
	limit, err := ParseLimit(c.Query("limit"), 20)
	if err != nil {
		c.JSON(http.StatusBadRequest, agentError(err.Error()))
		return
	}
	if limit > 100 {
		limit = 100
	}
	scoped, ok := h.store.(ScopeAPIStore)
	if !ok {
		c.JSON(http.StatusConflict, agentErrorWithCode(agentDaemonUpgradeMessage, agentDaemonUpgradeCode))
		return
	}
	if usesLegacyHistoryCursor(c) {
		c.JSON(http.StatusConflict, agentErrorWithCode(agentDaemonUpgradeMessage, agentDaemonUpgradeCode))
		return
	}
	if strings.TrimSpace(c.Query("after_seq")) != "" {
		c.JSON(http.StatusBadRequest, agentError("scope history supports before_seq cursor; after_seq is not supported"))
		return
	}
	scopeID := scopeIDFromQuery(c)
	if scopeID == "" {
		var resolveErr error
		scopeID, resolveErr = h.store.ResolveReadableAgentTargetScopeID(c.Request.Context(), agent, channel)
		if resolveErr != nil {
			c.JSON(HTTPStatus(resolveErr), agentError(resolveErr.Error()))
			return
		}
	}
	beforeSeqValue, err := ParseOptionalSeq(c.Query("before_seq"))
	if err != nil {
		c.JSON(http.StatusBadRequest, agentError(err.Error()))
		return
	}
	beforeSeq := int64(0)
	if beforeSeqValue != nil {
		beforeSeq = *beforeSeqValue
	}
	page, err := scoped.ListScopeHistoryForAgent(c.Request.Context(), agent, message.ScopeAPIHistoryRequest{
		ScopeID:   scopeID,
		BeforeSeq: beforeSeq,
		Limit:     limit,
	})
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, dto.AgentHistoryResponse{
		TargetKey:                page.TargetKey,
		CoversThroughSeq:         page.CoversThroughSeq,
		Messages:                 converter.ToHistoryMessages(page.Messages),
		HasMore:                  page.HasMore,
		NextCursor:               page.NextCursor,
		CandidateBudgetExhausted: page.CandidateBudgetExhausted,
		LastReadSeq:              page.LastReadSeq,
	})
}

// Search returns message matches visible to the authenticated agent.
func (h *MessageHandler) Search(c *gin.Context) {
	agent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	if !requireAgentDaemonVersion(c, agent, h.minDaemonVersion) {
		return
	}
	query := strings.TrimSpace(c.Query("q"))
	if query == "" {
		c.JSON(http.StatusBadRequest, agentError("q is required"))
		return
	}
	limit, err := ParseLimit(c.Query("limit"), 20)
	if err != nil {
		c.JSON(http.StatusBadRequest, agentError(err.Error()))
		return
	}
	if limit > 50 {
		limit = 50
	}
	scoped, ok := h.store.(ScopeAPIStore)
	if !ok {
		c.JSON(http.StatusConflict, agentErrorWithCode(agentDaemonUpgradeMessage, agentDaemonUpgradeCode))
		return
	}
	if usesLegacySearchCursor(c) {
		c.JSON(http.StatusConflict, agentErrorWithCode(agentDaemonUpgradeMessage, agentDaemonUpgradeCode))
		return
	}
	scopeID := scopeIDFromQuery(c)
	channel := strings.TrimSpace(c.Query("channel"))
	if scopeID == "" && channel != "" {
		var resolveErr error
		scopeID, resolveErr = h.store.ResolveReadableAgentTargetScopeID(c.Request.Context(), agent, channel)
		if resolveErr != nil {
			c.JSON(HTTPStatus(resolveErr), agentError(resolveErr.Error()))
			return
		}
	}
	beforeSeq, err := ParseOptionalSeq(c.Query("before_seq"))
	if err != nil {
		c.JSON(http.StatusBadRequest, agentError(err.Error()))
		return
	}
	cursor := int64(0)
	if beforeSeq != nil {
		cursor = *beforeSeq
	}
	if cursor > 0 && strings.TrimSpace(scopeID) == "" {
		c.JSON(http.StatusBadRequest, agentError("scope_id is required with before_seq"))
		return
	}
	page, err := scoped.SearchScopeMessagesForAgent(c.Request.Context(), agent, message.ScopeAPISearchRequest{
		ScopeID:   scopeID,
		Query:     query,
		BeforeSeq: cursor,
		Limit:     limit,
	})
	if err != nil {
		c.JSON(HTTPStatus(err), agentError(err.Error()))
		return
	}
	c.JSON(http.StatusOK, dto.AgentSearchMessagesResponse{
		Messages:                 converter.ToHistoryMessages(page.Messages),
		NextCursor:               page.NextCursor,
		HasMore:                  page.HasMore,
		CandidateBudgetExhausted: page.CandidateBudgetExhausted,
	})
}

// ReadCheck returns whether the authenticated agent may read one scope message.
func (h *MessageHandler) ReadCheck(c *gin.Context) {
	agent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	if !requireAgentDaemonVersion(c, agent, h.minDaemonVersion) {
		return
	}
	scoped, ok := h.store.(ScopeAPIStore)
	if !ok {
		c.JSON(http.StatusNotImplemented, dto.AgentReadCheckResponse{Error: "scope read check is unavailable"})
		return
	}
	messageID := strings.TrimSpace(c.Param("messageId"))
	if messageID == "" {
		messageID = strings.TrimSpace(c.Query("message_id"))
	}
	if messageID == "" {
		c.JSON(http.StatusBadRequest, dto.AgentReadCheckResponse{Error: "message_id is required"})
		return
	}
	result, err := scoped.CanReadScopeMessageForAgent(c.Request.Context(), agent, message.ScopeAPIReadCheckRequest{
		MessageID:   messageID,
		RequestKind: firstQueryValue(c.Query("kind"), c.Query("request_kind")),
	})
	if err != nil {
		c.JSON(HTTPStatus(err), dto.AgentReadCheckResponse{Error: err.Error()})
		return
	}
	response := dto.AgentReadCheckResponse{Allowed: result.Allowed, Reason: result.Reason}
	if result.Message != nil {
		converted := converter.ToHistoryMessages([]message.HistoryMessage{*result.Message})
		if len(converted) == 1 {
			response.Message = &converted[0]
		}
	}
	c.JSON(http.StatusOK, response)
}

// ListScopeMembers returns private-safe member display rows for one scope.
func (h *MessageHandler) ListScopeMembers(c *gin.Context) {
	agent, ok := authenticate(c, h.auth)
	if !ok {
		return
	}
	if !requireAgentScopeMembersDaemonVersion(c, agent, h.minDaemonVersion) {
		return
	}
	scoped, ok := h.store.(ScopeAPIStore)
	if !ok {
		c.JSON(http.StatusNotImplemented, dto.AgentScopeMembersResponse{Error: "scope member display is unavailable", Members: []dto.AgentScopeMemberDisplayRow{}})
		return
	}
	scopeID := strings.TrimSpace(c.Param("scopeId"))
	if scopeID == "" {
		scopeID = scopeIDFromQuery(c)
	}
	if scopeID == "" {
		c.JSON(http.StatusBadRequest, dto.AgentScopeMembersResponse{Error: "scope_id is required", Members: []dto.AgentScopeMemberDisplayRow{}})
		return
	}
	limit, err := ParseLimit(c.Query("limit"), 20)
	if err != nil {
		c.JSON(http.StatusBadRequest, dto.AgentScopeMembersResponse{Error: err.Error(), Members: []dto.AgentScopeMemberDisplayRow{}})
		return
	}
	if limit > 100 {
		limit = 100
	}
	page, err := scoped.ListScopeMemberDisplayRowsForAgent(c.Request.Context(), agent, message.ScopeMemberListRequest{
		ScopeID:    scopeID,
		Limit:      limit,
		Cursor:     strings.TrimSpace(c.Query("cursor")),
		Query:      strings.TrimSpace(c.Query("query")),
		MemberType: strings.TrimSpace(firstQueryValue(c.Query("member_type"), c.Query("role"))),
		Visibility: strings.TrimSpace(c.Query("visibility")),
	})
	if err != nil {
		c.JSON(HTTPStatus(err), dto.AgentScopeMembersResponse{Error: err.Error(), Members: []dto.AgentScopeMemberDisplayRow{}})
		return
	}
	response := dto.AgentScopeMembersResponse{Members: make([]dto.AgentScopeMemberDisplayRow, 0, len(page.Members)), Limit: page.Limit, HasMore: page.HasMore, NextCursor: page.NextCursor}
	for _, row := range page.Members {
		response.Members = append(response.Members, dto.AgentScopeMemberDisplayRow{
			MemberID:      row.MemberID,
			MemberType:    row.MemberType,
			PublicID:      row.PublicID,
			DisplayName:   row.DisplayName,
			Visibility:    row.Visibility,
			OwnerMemberID: row.OwnerMemberID,
		})
	}
	c.JSON(http.StatusOK, response)
}

func (h *MessageHandler) warn(message string, args ...any) {
	if h.logger != nil {
		h.logger.Warn(message, args...)
	}
}

func firstQueryValue(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func scopeIDFromQuery(c *gin.Context) string {
	return strings.TrimSpace(c.Query("scope_id"))
}

func usesScopeAPI(c *gin.Context) bool {
	return scopeIDFromQuery(c) != ""
}

func usesLegacySearchCursor(c *gin.Context) bool {
	return strings.TrimSpace(c.Query("before_global_seq")) != "" ||
		strings.TrimSpace(c.Query("after_global_seq")) != "" ||
		strings.TrimSpace(c.Query("before")) != "" ||
		strings.TrimSpace(c.Query("after")) != ""
}

func usesLegacyHistoryCursor(c *gin.Context) bool {
	return strings.TrimSpace(c.Query("before_global_seq")) != "" ||
		strings.TrimSpace(c.Query("after_global_seq")) != "" ||
		strings.TrimSpace(c.Query("before")) != "" ||
		strings.TrimSpace(c.Query("after")) != ""
}
