//go:generate ${GOBIN}/mockgen -destination im_mock.go -package=im ${GO_MODULE}/infra/lark/client/im IM
package im

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/url"

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

	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/logger"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/utils/object"
)

type MessageType string

const (
	MessageTypeText        MessageType = "text"
	MessageTypeInteractive MessageType = "interactive"
	MessageTypeImage       MessageType = "image"
	MessageTypeFile        MessageType = "file"
	MessageTypeVideoChat   MessageType = "video_chat"
	MessageTypePost        MessageType = "post"
)

// Message is the interface for lark message
type Message interface {
	Type() MessageType
}

type IM interface {
	// SendMessageViaWebhook sends a message via webhook url.
	SendMessageViaWebhook(ctx context.Context, webhookURL string, msg Message) error
	// SendMessage sends a message to a chat/user
	//
	// receiveType: open_id/user_id/union_id/email/chat_id
	SendMessage(ctx context.Context, receiveType, receiveID string, msg Message) (*string /*message id*/, error)
	ReplyMessage(ctx context.Context, msgID string, msg Message, replyInThread bool) (*string /*message id*/, error)
	PatchMessage(ctx context.Context, msgID string, msg Message) error
	UrgentAppMessage(ctx context.Context, msgID string, userOpenIDs []string) error
	ListMessageReadUserOpenIDs(ctx context.Context, msgID string) ([]string, error)
	ListMessages(ctx context.Context, chatID string, startTime string, endTime string) (*larkim.ListMessageRespData, error)
	GenerateChatShareLink(ctx context.Context, chatID string, validityPeriod string) (string /* link */, error)

	UploadImage(ctx context.Context, image io.Reader) (*string /*image key*/, error)
	UploadFile(ctx context.Context, fileType string, fileName string, file io.Reader) (*string /*file key*/, error)

	ReactEmoji(ctx context.Context, msgID string, emoji string) (*string /*reaction id*/, error)
	UnreactEmoji(ctx context.Context, msgID string, reactionID string) error
}

func (c *client) UrgentAppMessage(ctx context.Context, msgID string, userOpenIDs []string) error {
	req := larkim.NewUrgentAppMessageReqBuilder().
		MessageId(msgID).
		UserIdType("open_id").
		UrgentReceivers(larkim.NewUrgentReceiversBuilder().UserIdList(userOpenIDs).Build()).
		Build()
	resp, err := c.larkCli.Im.Message.UrgentApp(ctx, req)
	if err != nil {
		return fmt.Errorf("mark message urgent in app failed: %w", err)
	}
	if !resp.Success() {
		return fmt.Errorf("mark message urgent in app failed: %s", resp.CodeError.String())
	}
	if resp.Data != nil && len(resp.Data.InvalidUserIdList) > 0 {
		return fmt.Errorf("mark message urgent in app rejected user IDs")
	}
	return nil
}

func (c *client) ListMessageReadUserOpenIDs(ctx context.Context, msgID string) ([]string, error) {
	result := make([]string, 0)
	pageToken := ""
	for {
		builder := larkim.NewReadUsersMessageReqBuilder().
			MessageId(msgID).
			UserIdType("open_id").
			PageSize(100)
		if pageToken != "" {
			builder.PageToken(pageToken)
		}
		resp, err := c.larkCli.Im.Message.ReadUsers(ctx, builder.Build())
		if err != nil {
			return nil, fmt.Errorf("list message read users failed: %w", err)
		}
		if !resp.Success() {
			return nil, fmt.Errorf("list message read users failed: %s", resp.CodeError.String())
		}
		if resp.Data == nil {
			return result, nil
		}
		for _, item := range resp.Data.Items {
			if item != nil && item.UserId != nil {
				result = append(result, *item.UserId)
			}
		}
		if resp.Data.HasMore == nil || !*resp.Data.HasMore || resp.Data.PageToken == nil || *resp.Data.PageToken == "" {
			return result, nil
		}
		pageToken = *resp.Data.PageToken
	}
}

func New(larkCli *lark.Client) IM {
	return &client{
		larkCli: larkCli,
	}
}

type client struct {
	larkCli *lark.Client
}

func (c *client) SendMessage(ctx context.Context, receiveType, receiveID string, msg Message) (*string /*message id*/, error) {
	logger := logger.FromContext(ctx)

	logger.V(4).Info("try to send a message", "message", msg, "receive_type", receiveType, "receive_id", receiveID)

	content, err := EncodeSendMessage(msg)
	if err != nil {
		return nil, err
	}

	req := larkim.NewCreateMessageReqBuilder().
		ReceiveIdType(receiveType).
		Body(&larkim.CreateMessageReqBody{
			ReceiveId: object.Pointer(receiveID),
			MsgType:   object.Pointer(string(msg.Type())),
			Content:   object.Pointer(content),
		}).
		Build()

	resp, err := c.larkCli.Im.Message.Create(ctx, req)
	if err != nil {
		return nil, fmt.Errorf("create message failed: %w", err)
	}
	if !resp.Success() {
		return nil, fmt.Errorf("create message failed: %s", resp.CodeError.String())
	}

	return resp.Data.MessageId, nil
}

func (c *client) SendMessageViaWebhook(ctx context.Context, webhookURL string, msg Message) error {
	logger := logger.FromContext(ctx)

	u, err := url.Parse(webhookURL)
	if err != nil {
		return fmt.Errorf("invalid webhook url: %w", err)
	}

	body := SendMessageCardBody{
		MsgType: msg.Type(),
		Card:    msg,
	}
	logger.V(4).Info("try to send a message via webhook", "body", body)

	resp, err := c.larkCli.Post(ctx, u.Path, body, larkcore.AccessTokenTypeNone)
	if err != nil {
		return fmt.Errorf("failed to send message via webhook: %w", err)
	}

	respErr := larkcore.CodeError{}
	err = json.Unmarshal(resp.RawBody, &respErr)
	if err != nil {
		return fmt.Errorf("failed to unmarshal response body: %w", err)
	}

	if respErr.Code != 0 {
		return fmt.Errorf("failed to send message via webhook: %s", string(resp.RawBody))
	}

	logger.Info("sent message via webhook", "status_code", resp.StatusCode, "response", string(resp.RawBody))
	return nil
}

// UpdateMessage updates a card message
//
// Ref: https://open.feishu.cn/document/server-docs/im-v1/message-card/patch
//
// NOTE: Note to set the config `update_mutli: true`, see https://open.feishu.cn/document/common-capabilities/message-card/message-cards-content/card-structure/card-configuration
func (c *client) PatchMessage(ctx context.Context, msgID string, msg Message) error {
	logger := logger.FromContext(ctx)

	logger.V(4).Info("try to patch a message", "message", msg, "msg_id", msgID)

	content, err := EncodeSendMessage(msg)
	if err != nil {
		return err
	}

	req := larkim.NewPatchMessageReqBuilder().
		MessageId(msgID).
		Body(&larkim.PatchMessageReqBody{
			Content: object.Pointer(content),
		}).
		Build()

	resp, err := c.larkCli.Im.Message.Patch(ctx, req)
	if err != nil {
		return fmt.Errorf("patch message failed: %w", err)
	}
	if !resp.Success() {
		return fmt.Errorf("patch message failed: %s", resp.CodeError.String())
	}

	return nil
}

func (c *client) ReplyMessage(ctx context.Context, msgID string, msg Message, replyInThread bool) (*string /*message id*/, error) {
	logger := logger.FromContext(ctx)

	logger.V(4).Info("try to reply a message", "message", msg, "msg_id", msgID)

	content, err := EncodeSendMessage(msg)
	if err != nil {
		return nil, err
	}

	req := larkim.NewReplyMessageReqBuilder().
		MessageId(msgID).
		Body(&larkim.ReplyMessageReqBody{
			Content:       &content,
			MsgType:       object.Pointer(string(msg.Type())),
			ReplyInThread: object.Pointer(replyInThread),
		}).
		Build()

	resp, err := c.larkCli.Im.Message.Reply(ctx, req)
	if err != nil {
		return nil, fmt.Errorf("reply message failed: %w", err)
	}
	if !resp.Success() {
		return nil, fmt.Errorf("reply message failed: %s", resp.CodeError.String())
	}

	return resp.Data.MessageId, nil
}

func (c *client) ListMessages(ctx context.Context, chatID string, startTime string, endTime string) (*larkim.ListMessageRespData, error) {
	result := &larkim.ListMessageRespData{}
	pageToken := ""
	seenPageTokens := make(map[string]struct{})
	for {
		builder := larkim.NewListMessageReqBuilder().
			ContainerIdType("chat").
			ContainerId(chatID).
			StartTime(startTime).
			EndTime(endTime).
			SortType(`ByCreateTimeAsc`).
			PageSize(50)
		if pageToken != "" {
			builder.PageToken(pageToken)
		}

		resp, err := c.larkCli.Im.Message.List(ctx, builder.Build())
		if err != nil {
			return nil, err
		}
		if !resp.Success() {
			return nil, fmt.Errorf("list messages failed: %s", resp.CodeError.String())
		}
		if resp.Data == nil {
			return result, nil
		}

		result.Items = append(result.Items, resp.Data.Items...)
		if resp.Data.HasMore == nil || !*resp.Data.HasMore || resp.Data.PageToken == nil || *resp.Data.PageToken == "" {
			return result, nil
		}
		nextPageToken := *resp.Data.PageToken
		if _, seen := seenPageTokens[nextPageToken]; seen {
			return nil, errors.New("list messages failed: repeated page token")
		}
		seenPageTokens[nextPageToken] = struct{}{}
		pageToken = nextPageToken
	}
}

func (c *client) GenerateChatShareLink(ctx context.Context, chatID string, linkPeriod string) (string /* link */, error) {
	req := larkim.NewLinkChatReqBuilder().
		ChatId(chatID).
		Body(&larkim.LinkChatReqBody{
			ValidityPeriod: object.Pointer(linkPeriod),
		}).
		Build()

	resp, err := c.larkCli.Im.Chat.Link(ctx, req)
	if err != nil {
		return "", fmt.Errorf("gen chat link failed: %w", err)
	}
	if !resp.Success() {
		return "", fmt.Errorf("reply message failed: %s", resp.CodeError.String())
	}

	return *resp.Data.ShareLink, nil
}

func (c *client) UploadImage(ctx context.Context, image io.Reader) (*string, error) {
	req := larkim.NewCreateImageReqBuilder().
		Body(larkim.NewCreateImageReqBodyBuilder().
			ImageType("message").
			Image(image).
			Build()).
		Build()
	resp, err := c.larkCli.Im.Image.Create(ctx, req)
	if err != nil {
		return nil, fmt.Errorf("upload image failed: %w", err)
	}
	if !resp.Success() {
		return nil, fmt.Errorf("upload image failed: code: %d, msg: %s", resp.Code, resp.Msg)
	}
	return resp.Data.ImageKey, nil
}

func (c *client) UploadFile(ctx context.Context, fileType string, fileName string, file io.Reader) (*string, error) {
	req := larkim.NewCreateFileReqBuilder().
		Body(larkim.NewCreateFileReqBodyBuilder().
			FileType(fileType).
			FileName(fileName).
			File(file).
			Build()).
		Build()
	resp, err := c.larkCli.Im.File.Create(ctx, req)
	if err != nil {
		return nil, fmt.Errorf("upload file failed: %w", err)
	}
	if !resp.Success() {
		return nil, fmt.Errorf("upload file failed: code: %d, msg: %s", resp.Code, resp.Msg)
	}
	return resp.Data.FileKey, nil
}

func (c *client) ReactEmoji(ctx context.Context, msgID string, emoji string) (*string, error) {
	req := larkim.NewCreateMessageReactionReqBuilder().
		MessageId(msgID).
		Body(larkim.NewCreateMessageReactionReqBodyBuilder().
			ReactionType(larkim.NewEmojiBuilder().
				EmojiType(emoji).
				Build()).
			Build()).
		Build()

	resp, err := c.larkCli.Im.MessageReaction.Create(ctx, req)
	if err != nil {
		return nil, fmt.Errorf("react emoji failed: %w", err)
	}
	if !resp.Success() {
		return nil, fmt.Errorf("react emoji failed: code: %d, msg: %s", resp.Code, resp.Msg)
	}
	return resp.Data.ReactionId, nil
}

func (c *client) UnreactEmoji(ctx context.Context, msgID string, reactionID string) error {
	req := larkim.NewDeleteMessageReactionReqBuilder().
		MessageId(msgID).
		ReactionId(reactionID).
		Build()

	resp, err := c.larkCli.Im.MessageReaction.Delete(ctx, req)
	if err != nil {
		return fmt.Errorf("unreact emoji failed: %w", err)
	}
	if !resp.Success() {
		return fmt.Errorf("unreact emoji failed: code: %d, msg: %s", resp.Code, resp.Msg)
	}
	return nil
}
