package onboarding

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

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

const mockRequestClientMessageIDSuffix = ":request"

// MockExchangeHandler owns the Desktop onboarding-only saved conversation.
// Its dependencies are kept separate from the ordinary onboarding projection
// so the general handler cannot accidentally bypass Agent delivery.
type MockExchangeHandler struct {
	state       MockExchangeStateStore
	messages    MockMessageStore
	attachments MockAttachmentStore
	fanout      MockMessageFanout
	enabled     bool
}

// MockStart handles POST /api/v1/workspaces/:workspaceId/onboarding/mock-exchange/start.
// It creates the saved tutorial prompt and Human assignment transcript while
// deliberately producing no Agent runtime delivery.
func (handler *MockExchangeHandler) MockStart(r api.WorkspaceScopedJSON[dto.DesktopOnboardingMockStartRequest]) (api.Response, error) {
	if handler == nil || !handler.enabled || handler.state == nil || handler.messages == nil || handler.fanout == nil {
		return api.Response{}, api.NewHTTPError(http.StatusServiceUnavailable, "Desktop onboarding is unavailable")
	}
	body := r.Body
	if strings.TrimSpace(body.TutorialChannelID) == "" || strings.TrimSpace(body.AgentID) == "" || strings.TrimSpace(body.PromptContent) == "" || strings.TrimSpace(body.RequestContent) == "" || strings.TrimSpace(body.ClientMessageID) == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "tutorialChannelId, agentId, promptContent, requestContent, and clientMessageId are required")
	}
	target, err := handler.state.ResolveMockAgentTarget(r.Context(), r.UserID(), r.WorkspaceID(), body.TutorialChannelID, body.AgentID)
	if err != nil {
		return api.Response{}, mockExchangeError(err)
	}

	prompt, promptDelivery, err := handler.messages.SendDesktopOnboardingMockMessageForUser(
		r.Context(), r.User.User, r.WorkspaceID(), target.TutorialChannelID,
		body.PromptContent, nil, strings.TrimSpace(body.ClientMessageID), true,
	)
	if err != nil {
		return api.Response{}, err
	}
	if promptDelivery.StartedReplyThread == nil {
		return api.Response{}, api.NewHTTPError(http.StatusConflict, "Desktop onboarding mock thread was not created")
	}
	if !promptDelivery.IdempotentReplay {
		if err := handler.fanout.DeliverCommittedMessage(r.Context(), r.WorkspaceID(), prompt, promptDelivery); err != nil {
			return api.Response{}, err
		}
	}

	requestMessage, requestDelivery, err := handler.messages.SendDesktopOnboardingMockMessageForUser(
		r.Context(), r.User.User, r.WorkspaceID(), promptDelivery.StartedReplyThread.ThreadChannelID,
		body.RequestContent, &MockMention{AgentID: target.AgentID, Token: target.AgentName},
		strings.TrimSpace(body.ClientMessageID)+mockRequestClientMessageIDSuffix, false,
	)
	if err != nil {
		return api.Response{}, err
	}
	if !requestDelivery.IdempotentReplay {
		if err := handler.fanout.DeliverCommittedMessage(r.Context(), r.WorkspaceID(), requestMessage, requestDelivery); err != nil {
			return api.Response{}, err
		}
	}

	return api.OK(dto.DesktopOnboardingMockStartResponse{
		Message:        converter.ToAppMessage(prompt),
		ReplyThread:    converter.ToReplyThreadSummary(*promptDelivery.StartedReplyThread),
		RequestMessage: converter.ToAppMessage(requestMessage),
	}), nil
}

// MockReply handles POST /api/v1/workspaces/:workspaceId/onboarding/mock-exchange/reply.
// It persists the localized saved Agent response and HTML attachment for the
// exact active tutorial request. Client-side delay controls when this endpoint
// is called; the server does not hold a request or start an in-memory timer.
func (handler *MockExchangeHandler) MockReply(r api.WorkspaceScopedJSON[dto.DesktopOnboardingMockReplyRequest]) (api.Response, error) {
	if handler == nil || !handler.enabled || handler.state == nil || handler.messages == nil || handler.attachments == nil || handler.fanout == nil {
		return api.Response{}, api.NewHTTPError(http.StatusServiceUnavailable, "Desktop onboarding is unavailable")
	}
	body := r.Body
	if strings.TrimSpace(body.ThreadScopeID) == "" || strings.TrimSpace(body.AgentID) == "" || strings.TrimSpace(body.RequestMessageID) == "" || strings.TrimSpace(body.ReplyClientMessageID) == "" {
		return api.Response{}, api.NewHTTPError(http.StatusBadRequest, "threadScopeId, agentId, requestMessageId, and replyClientMessageId are required")
	}
	target, err := handler.state.ResolveMockReplyTarget(
		r.Context(), r.UserID(), r.WorkspaceID(), body.ThreadScopeID, body.AgentID,
		body.RequestMessageID, body.ReplyClientMessageID,
	)
	if err != nil {
		return api.Response{}, mockExchangeError(err)
	}
	if target.ExistingReplyMessageID != "" {
		if err := handler.recordMockReply(r, target.AgentID, target.RequestMessageID, target.ExistingReplyMessageID); err != nil {
			return api.Response{}, err
		}
		return api.OK(dto.DesktopOnboardingMockReplyResponse{ReplyMessageID: target.ExistingReplyMessageID, Completed: true}), nil
	}

	localized := onboardingMockContent(r.Gin.GetHeader("Accept-Language"))
	attachmentID, err := handler.attachments.UploadDesktopOnboardingMockAttachment(
		r.Context(), target.AgentMemberID, r.WorkspaceID(), target.ThreadScopeID,
		MockAttachment{Filename: "index.html", ContentType: mockAttachmentContentType, Blob: localized.HTML},
	)
	if err != nil {
		return api.Response{}, err
	}
	delivery, err := handler.messages.SendDesktopOnboardingMockAgentReply(
		r.Context(), MockAgentIdentity{
			AgentID: target.AgentID, WorkspaceID: r.WorkspaceID(), MemberID: target.AgentMemberID, MachineID: target.AgentMachineID,
		}, target.ThreadTargetRef, localized.ReplyMarkdown, []string{attachmentID}, strings.TrimSpace(body.ReplyClientMessageID),
	)
	if err != nil {
		// A concurrent retry may have committed the deterministic reply after our
		// initial bounded lookup. Re-resolve before surfacing an avoidable error.
		replayed, replayErr := handler.state.ResolveMockReplyTarget(
			r.Context(), r.UserID(), r.WorkspaceID(), body.ThreadScopeID, body.AgentID,
			body.RequestMessageID, body.ReplyClientMessageID,
		)
		if replayErr != nil || replayed.ExistingReplyMessageID == "" {
			return api.Response{}, err
		}
		target = replayed
		if err := handler.recordMockReply(r, target.AgentID, target.RequestMessageID, target.ExistingReplyMessageID); err != nil {
			return api.Response{}, err
		}
		return api.OK(dto.DesktopOnboardingMockReplyResponse{ReplyMessageID: target.ExistingReplyMessageID, Completed: true}), nil
	}
	replyMessageID := strings.TrimSpace(delivery.MessageID)
	if replyMessageID == "" && delivery.AppMessage != nil {
		replyMessageID = strings.TrimSpace(delivery.AppMessage.ID)
	}
	if replyMessageID == "" {
		return api.Response{}, api.NewHTTPError(http.StatusInternalServerError, "Desktop onboarding mock reply was not persisted")
	}
	if err := handler.recordMockReply(r, target.AgentID, target.RequestMessageID, replyMessageID); err != nil {
		return api.Response{}, err
	}
	if !delivery.IdempotentReplay && delivery.AppMessage != nil {
		if err := handler.fanout.DeliverCommittedMessage(r.Context(), r.WorkspaceID(), *delivery.AppMessage, delivery); err != nil {
			return api.Response{}, err
		}
	}
	return api.OK(dto.DesktopOnboardingMockReplyResponse{ReplyMessageID: replyMessageID, Completed: true}), nil
}

func (handler *MockExchangeHandler) recordMockReply(r api.WorkspaceScopedJSON[dto.DesktopOnboardingMockReplyRequest], agentID string, requestMessageID string, replyMessageID string) error {
	return handler.state.RecordVisibleReply(r.Context(), VisibleReplyEvidence{
		WorkspaceID: r.WorkspaceID(), AgentID: agentID, MessageID: requestMessageID,
		ReplyMessageID: replyMessageID, CompleteImmediately: true,
	})
}

func mockExchangeError(err error) error {
	if errors.Is(err, ErrMockExchangeUnavailable) {
		return api.NewHTTPError(http.StatusConflict, "Desktop onboarding mock exchange is no longer available")
	}
	return err
}
