//go:generate ${GOBIN}/mockgen -source lark.go -destination lark_mock.go -package=lark
//go:generate ${GOBIN}/mockgen -destination gateway_client_mock.go -package=lark github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/agent-gateway/gateway/v1 GatewayServiceClient
package lark

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

	larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"

	gatewayapi "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/agent-gateway/gateway/v1"
	gatewayv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/agent-gateway/gateway/v1"
	infralarkim "github.com/tidbcloud/aisupport/packages/sdk-go/infra/lark/client/im"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/logger"
	objutil "github.com/tidbcloud/aisupport/packages/sdk-go/infra/utils/object"
)

const larkChatTypeP2P = "p2p"

// LarkConfig controls Lark message admission and Agent Gateway message context.
type LarkConfig struct {
	// Empty delegates Agent selection to Gateway.
	AgentID                      string
	InstanceID                   string
	WorkingDirectory             string
	BotOpenID                    string
	AllowGroupChat               bool
	MaxMessageSize               int
	CommentaryMessageMergeBuffer int
}

// SenderEmailResolver resolves the Lark user ID carried by a message event to
// the email address required by Agent Gateway.
type SenderEmailResolver func(context.Context, string) (string, error)

// Renderer builds cards for commentary and terminal task events.
type Renderer interface {
	Commentary(taskID, content, latestMessage string) (infralarkim.Message, error)
	Result(taskID, content string) (infralarkim.Message, error)
	Rejected(taskID, content string) (infralarkim.Message, error)
	Failed(taskID, content string) (infralarkim.Message, error)
}

// Lark accepts Lark message events and renders Agent Gateway task events.
type Lark interface {
	HandleLarkMessage(context.Context, *larkim.P2MessageReceiveV1) error
	HandleLarkTaskEvent(context.Context, *gatewayv1.TaskEvent) error
}

type lark struct {
	config             LarkConfig
	gateway            gatewayapi.GatewayServiceClient
	resolveSenderEmail SenderEmailResolver
	renderer           Renderer
	reactor            *reactor
}

// Option customizes a Lark adapter.
type Option func(*lark)

// WithRenderer replaces the default Agent Gateway card renderer.
func WithRenderer(renderer Renderer) Option {
	return func(lark *lark) {
		lark.renderer = renderer
	}
}

// NewLark constructs a Lark adapter with caller-owned Gateway and Lark clients.
func NewLark(
	gateway gatewayapi.GatewayServiceClient,
	larkClient infralarkim.IM,
	resolveSenderEmail SenderEmailResolver,
	config LarkConfig,
	options ...Option,
) Lark {
	adapter := &lark{
		config:             config,
		gateway:            gateway,
		resolveSenderEmail: resolveSenderEmail,
		renderer:           defaultRenderer{},
	}
	for _, option := range options {
		option(adapter)
	}
	adapter.reactor = newReactor(config.InstanceID, config.CommentaryMessageMergeBuffer, larkClient, adapter.renderer)
	return adapter
}

func (l *lark) HandleLarkMessage(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
	message := event.Event.Message
	sender := event.Event.Sender
	messageID := objutil.Deref(message.MessageId, "")
	chatID := objutil.Deref(message.ChatId, "")
	senderUserID := objutil.Deref(sender.SenderId.UserId, "")
	senderOpenID := objutil.Deref(sender.SenderId.OpenId, "")
	log := logger.FromContext(ctx,
		"instance_id", l.config.InstanceID,
		"source_message_id", messageID,
		"chat_id", chatID,
		"sender_open_id", senderOpenID,
	)

	if objutil.Deref(sender.SenderType, "") == "app" {
		log.V(4).Info("ignored lark message", "reason", "app_sender")
		return nil
	}
	messageType := infralarkim.MessageType(objutil.Deref(message.MessageType, ""))
	if messageType != infralarkim.MessageTypeText && messageType != infralarkim.MessageTypePost {
		log.V(4).Info("ignored lark message", "reason", "non_text")
		return nil
	}
	if objutil.Deref(message.ChatType, "") != larkChatTypeP2P &&
		(!l.config.AllowGroupChat || !mentionsBot(message.Mentions, l.config.BotOpenID)) {
		log.V(4).Info("ignored lark message", "reason", "group_policy")
		return nil
	}
	text, err := infralarkim.DecodePlainText(messageType, objutil.Deref(message.Content, ""))
	if err != nil {
		return err
	}
	for _, mention := range message.Mentions {
		if mention != nil {
			text = strings.ReplaceAll(text, objutil.Deref(mention.Key, ""), "")
		}
	}
	if utf8.RuneCountInString(text) > l.config.MaxMessageSize {
		log.V(4).Info("ignored lark message", "reason", "message_too_long", "input_len", utf8.RuneCountInString(text))
		return nil
	}
	senderEmail, err := l.resolveSenderEmail(ctx, senderUserID)
	if err != nil {
		return err
	}

	resp, err := l.gateway.ProcessLarkMessage(ctx, &gatewayapi.ProcessLarkMessageRequest{
		Context: &gatewayapi.MessageContext{
			InstanceId:       l.config.InstanceID,
			AgentId:          l.config.AgentID,
			WorkingDirectory: l.config.WorkingDirectory,
		},
		Message: &gatewayv1.LarkMessage{
			MessageId:   messageID,
			RootId:      objutil.Deref(message.RootId, ""),
			ThreadId:    objutil.Deref(message.ThreadId, ""),
			ChatId:      chatID,
			ChatType:    objutil.Deref(message.ChatType, ""),
			MessageType: objutil.Deref(message.MessageType, ""),
			Content:     objutil.Deref(message.Content, ""),
			SenderType:  objutil.Deref(sender.SenderType, ""),
			SenderEmail: senderEmail,
			Mentions:    larkMentions(message.Mentions),
		},
	})
	if err != nil {
		return fmt.Errorf("process lark message with agent gateway: %w", err)
	}
	l.reactor.ClearCommentaryMessageBuffer(resp.GetTask().GetSessionId())

	log.Info("lark message submitted to agent gateway",
		"task_id", resp.GetTask().GetTaskId(),
		"disposition", resp.GetDisposition().String(),
	)
	return nil
}

func (l *lark) HandleLarkTaskEvent(ctx context.Context, event *gatewayv1.TaskEvent) error {
	return l.reactor.HandleAgentTaskEvent(ctx, event)
}

func mentionsBot(mentions []*larkim.MentionEvent, botOpenID string) bool {
	for _, mention := range mentions {
		if mention != nil && mention.Id != nil && objutil.Deref(mention.Id.OpenId, "") == botOpenID {
			return true
		}
	}
	return false
}

func larkMentions(mentions []*larkim.MentionEvent) []*gatewayv1.LarkMention {
	result := make([]*gatewayv1.LarkMention, 0, len(mentions))
	for _, mention := range mentions {
		if mention == nil {
			continue
		}
		openID := ""
		if mention.Id != nil {
			openID = objutil.Deref(mention.Id.OpenId, "")
		}
		result = append(result, &gatewayv1.LarkMention{Key: objutil.Deref(mention.Key, ""), OpenId: openID})
	}
	return result
}

var _ Lark = (*lark)(nil)
