package lark

import (
	"context"
	"errors"
	"fmt"
	"unicode/utf8"

	gatewayv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-gateway/gateway/v1"
	sessionv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-gateway/session/v1"
	gatewayerrors "github.com/tidbcloud/aisupport/packages/sdk-go/infra/agentgateway/errors"
	infralarkim "github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/im"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/logger"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/utils/container/gmap"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/utils/sync/keymutex"
)

const (
	commentaryMessageSeparator  = "\n---\n"
	larkProcessingReactionEmoji = "OnIt"
)

type cachedCommentaryMessage struct {
	taskID    string
	messageID string
	content   string
}

type reactor struct {
	instanceID                   string
	commentaryMessageMergeBuffer int
	lark                         infralarkim.IM
	renderer                     Renderer
	commentaryMessages           *gmap.Map[string, cachedCommentaryMessage]
	emojiReactions               *gmap.Map[string, string]
	sessionLocks                 *keymutex.KeyMutex[string]
}

func newReactor(instanceID string, commentaryMessageMergeBuffer int, lark infralarkim.IM, renderers ...Renderer) *reactor {
	renderer := Renderer(defaultRenderer{})
	if len(renderers) > 0 {
		renderer = renderers[0]
	}
	return &reactor{
		instanceID:                   instanceID,
		commentaryMessageMergeBuffer: commentaryMessageMergeBuffer,
		lark:                         lark,
		renderer:                     renderer,
		commentaryMessages:           gmap.NewMap[string, cachedCommentaryMessage](),
		emojiReactions:               gmap.NewMap[string, string](),
		sessionLocks:                 keymutex.NewKeyMutex[string](),
	}
}

func (r *reactor) HandleAgentTaskEvent(ctx context.Context, event *gatewayv1.TaskEvent) error {
	if event.GetTaskId() == "" || event.GetSessionId() == "" || event.GetTaskSource().GetSourceMessageId() == "" ||
		event.GetSessionSource().GetSource().GetType() != sessionv1.MessageSourceType_MESSAGE_SOURCE_TYPE_LARK ||
		event.GetSessionSource().GetSource().GetInstanceId() != r.instanceID {
		return fmt.Errorf("%w: missing or mismatched source fields", gatewayerrors.ErrInvalidTaskEvent)
	}

	log := logger.FromContext(ctx,
		"instance_id", event.GetSessionSource().GetSource().GetInstanceId(),
		"task_id", event.GetTaskId(),
		"event_type", event.GetType().String(),
		"source_message_id", event.GetTaskSource().GetSourceMessageId(),
	)

	r.sessionLocks.Lock(event.GetSessionId())
	defer r.sessionLocks.Unlock(event.GetSessionId())

	var err error
	switch event.GetType() {
	case gatewayv1.TaskEventType_TASK_EVENT_TYPE_STARTING:
		err = r.handleTaskStarting(ctx, event)
	case gatewayv1.TaskEventType_TASK_EVENT_TYPE_AGENT_COMMENTARY:
		err = r.sendCommentary(ctx, event)
	case gatewayv1.TaskEventType_TASK_EVENT_TYPE_FINISHED:
		err = r.sendFinished(ctx, event)
	default:
		return fmt.Errorf("%w: unsupported type %s", gatewayerrors.ErrInvalidTaskEvent, event.GetType())
	}
	if err != nil {
		return err
	}
	log.Info("agent gateway task event handled")
	return nil
}

func (r *reactor) ClearCommentaryMessageBuffer(sessionID string) {
	r.sessionLocks.Lock(sessionID)
	defer r.sessionLocks.Unlock(sessionID)
	r.commentaryMessages.Delete(sessionID)
}

func (r *reactor) handleTaskStarting(ctx context.Context, event *gatewayv1.TaskEvent) error {
	reactionID, err := r.lark.ReactEmoji(ctx, event.GetTaskSource().GetSourceMessageId(), larkProcessingReactionEmoji)
	if err != nil {
		return fmt.Errorf("add task processing reaction: %w", err)
	}
	r.emojiReactions.Store(event.GetTaskId(), *reactionID)
	return nil
}

func (r *reactor) sendCommentary(ctx context.Context, event *gatewayv1.TaskEvent) error {
	cached, ok := r.commentaryMessages.Load(event.GetSessionId())
	if ok && cached.taskID == event.GetTaskId() {
		content := cached.content + commentaryMessageSeparator + event.GetContent()
		if utf8.RuneCountInString(content) <= r.commentaryMessageMergeBuffer {
			card, err := r.renderer.Commentary(event.GetTaskId(), content, event.GetContent())
			if err != nil {
				return fmt.Errorf("render commentary card: %w", err)
			}
			if err := r.lark.PatchMessage(ctx, cached.messageID, card); err != nil {
				return fmt.Errorf("patch commentary card: %w", err)
			}
			cached.content = content
			r.commentaryMessages.Store(event.GetSessionId(), cached)
			return nil
		}
	}

	card, err := r.renderer.Commentary(event.GetTaskId(), event.GetContent(), event.GetContent())
	if err != nil {
		return fmt.Errorf("render commentary card: %w", err)
	}
	messageID, err := r.replyCard(ctx, event.GetTaskSource().GetSourceMessageId(), card)
	if err != nil {
		return err
	}
	r.commentaryMessages.Store(event.GetSessionId(), cachedCommentaryMessage{
		taskID:    event.GetTaskId(),
		messageID: messageID,
		content:   event.GetContent(),
	})
	return nil
}

func (r *reactor) sendFinished(ctx context.Context, event *gatewayv1.TaskEvent) error {
	card, err := r.finishedCard(event)
	if err != nil {
		return err
	}

	if cached, ok := r.commentaryMessages.Load(event.GetSessionId()); ok && cached.taskID == event.GetTaskId() {
		r.commentaryMessages.Delete(event.GetSessionId())
	}
	reactionID, _ := r.emojiReactions.LoadAndDelete(event.GetTaskId())

	var cleanupErr error
	if reactionID != "" {
		if err := r.lark.UnreactEmoji(ctx, event.GetTaskSource().GetSourceMessageId(), reactionID); err != nil {
			cleanupErr = fmt.Errorf("remove task processing reaction: %w", err)
		}
	}
	_, replyErr := r.replyCard(ctx, event.GetTaskSource().GetSourceMessageId(), card)
	return errors.Join(cleanupErr, replyErr)
}

func (r *reactor) finishedCard(event *gatewayv1.TaskEvent) (infralarkim.Message, error) {
	switch event.GetStatus() {
	case sessionv1.TaskStatus_TASK_STATUS_SUCCEEDED:
		return r.renderer.Result(event.GetTaskId(), event.GetContent())
	case sessionv1.TaskStatus_TASK_STATUS_REJECTED:
		return r.renderer.Rejected(event.GetTaskId(), event.GetContent())
	case sessionv1.TaskStatus_TASK_STATUS_FAILED, sessionv1.TaskStatus_TASK_STATUS_TIMEOUT:
		return r.renderer.Failed(event.GetTaskId(), event.GetContent())
	default:
		return nil, fmt.Errorf("%w: unsupported finished status %s", gatewayerrors.ErrInvalidTaskEvent, event.GetStatus())
	}
}

func (r *reactor) replyCard(ctx context.Context, messageID string, card infralarkim.Message) (string, error) {
	replyMessageID, err := r.lark.ReplyMessage(ctx, messageID, card, true)
	if err != nil {
		return "", fmt.Errorf("reply task card: %w", err)
	}
	return *replyMessageID, nil
}
