package v1

import (
	"bytes"
	"context"
	"encoding/json"
	"time"

	"github.com/pingcap-inc/pingcat/internal/pkg/code"
	v1 "github.com/pingcap-inc/pingcat/model/v1"

	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
)

//nolint:gocognit,gocyclo,cyclop // acceptable
func (s *ChatService) ReplyLarkMsg(ctx context.Context, me *v1.LarkMsgEvent) error {
	log.L(ctx).Info("ChatSrv.ReplyLarkMsg called")

	d := v1.MsgReqData{
		Msg: s.getLarkTextMsgContent(ctx, me.Message.Content),
	}
	pmsg := s.retrieveParentMsg(ctx, me)
	if pmsg != nil {
		d.PID = pmsg.ID
	}
	if pmsg == nil && me.Message.ParentID != "" {
		// merge the message with the parent message
		d.Msg = s.mergeMsgWithParentMsg(ctx, me)
	}
	msgID := me.Message.MessageID
	scene := v1.SceneLarkP2P
	if me.Message.ChatType == v1.ChatTypeGroup || me.Message.ChatType == v1.ChatTypeTopicGroup {
		scene = v1.SceneLarkGroup
	}
	inMsg, thmsgs := s.constructInMsg(ctx, d, scene, msgID)
	inMsgDB, err := s.store.Message().Create(ctx, inMsg)
	if err != nil {
		log.L(ctx).Error(errors.Wrap(err, "Error when create input msg in db").Error())

		return errors.WithCode(code.ErrDatabase, "")
	}
	larkReaction, err := s.markMsgOnIt(ctx, me)
	if err != nil {
		log.L(ctx).Error(errors.Wrap(err, "Error when mark msg OnIt via lark reaction").Error())
	}
	outMsgCont := ""
	var usage *v1.GPTTokenUsage
	isExceeded := false
	gptMsgs := s.convert2GPTMsg(ctx, thmsgs, inMsg)
	gptSrv := NewGPT(s.store)
	gptMsg, err := gptSrv.ChatCompletion(ctx, gptMsgs, d.Options)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrContextLengthExceeded {
			return errors.Wrap(err, "error when request openai api.")
		}
		if me.Message.ChatType == "p2p" {
			outMsgCont = "We've talked too much about this topic and overloaded my head. We can start a new one. If you still want to talk about this topic, you can summarize what we talked about just now and restart this topic, miaow~"
		} else {
			outMsgCont = "We've talked too much about this topic and overloaded my head. You can send a plain text message with @PingCAT but without any quote to start a new conversation, miaow~"
		}
		isExceeded = true
	} else {
		outMsgCont = gptMsg.Content
		usage = &gptMsg.Usage
	}
	larkReplyMsg, err := s.SendTextLarkMsg(ctx, me, outMsgCont)
	if err != nil {
		log.L(ctx).Error(errors.Wrap(err, "Error when send text lark msg").Error())

		return err
	}
	if larkReaction != nil {
		err = s.unmarkMsgOnIt(ctx, me, larkReaction)
		if err != nil {
			log.L(ctx).Error(errors.Wrap(err, "Error when unmark msg OnIt via lark reaction").Error())
		}
	}
	groupID := larkReplyMsg.ChatID
	if me.Message.ChatType == "p2p" {
		groupID = ""
	}
	outMsg := s.constructOutMsg(ctx, usage, outMsgCont, scene, larkReplyMsg.MessageID, inMsgDB, groupID, gptMsg.Model)
	if isExceeded && me.Message.ChatType == v1.ChatTypeP2P {
		outMsg.Semantic = v1.SemanticEndSession
	}
	_, err = s.store.Message().Create(ctx, outMsg)
	if err != nil {
		log.L(ctx).Error(errors.Wrap(err, "Error when create output msg in db").Error())

		return errors.WithCode(code.ErrDatabase, "")
	}

	return nil
}

func (s *ChatService) unmarkMsgOnIt(ctx context.Context, me *v1.LarkMsgEvent, r *v1.LarkReactionResData) error {
	larkAPISrv := &LarkAPIService{store: s.store}
	_, err := larkAPISrv.ReqLarkAPI(ctx, "DELETE", LarkOpenAPIBase+"/im/v1/messages/"+me.Message.MessageID+"/reactions/"+r.ReactionId, nil, nil)
	if err != nil {
		log.Error(err.Error())

		return err
	}

	return nil
}

func (s *ChatService) markMsgOnIt(ctx context.Context, me *v1.LarkMsgEvent) (*v1.LarkReactionResData, error) {
	reqb, err := json.Marshal(&v1.LarkReaction{
		ReactionType: struct {
			EmojiType string `json:"emoji_type"`
		}{
			EmojiType: "Typing",
		},
	})
	if err != nil {
		log.L(ctx).Errorf("error when marshal reaction request body, err: %s", err.Error())

		return nil, err
	}
	larkAPISrv := &LarkAPIService{store: s.store}
	rjsonraw, err := larkAPISrv.ReqLarkAPI(ctx, "POST", LarkOpenAPIBase+"/im/v1/messages/"+me.Message.MessageID+"/reactions", bytes.NewReader(reqb), nil)
	if err != nil {
		log.Error(err.Error())

		return nil, err
	}

	var r v1.LarkReactionResData
	if err = json.Unmarshal(rjsonraw, &r); err != nil {
		log.Error(err.Error())

		return nil, err
	}

	return &r, nil
}

func (s *ChatService) mergeMsgWithParentMsg(ctx context.Context, me *v1.LarkMsgEvent) string {
	larkAPISrv := &LarkAPIService{store: s.store}
	msgJsonRaw, err := larkAPISrv.ReqLarkAPI(ctx, "GET", LarkOpenAPIBase+"/im/v1/messages/"+me.Message.ParentID, nil, nil)
	if err != nil {
		log.Error(err.Error())

		return ""
	}
	var msg struct {
		Items []v1.LarkReplyMsgResData `json:"items"`
	}
	err = json.Unmarshal(msgJsonRaw, &msg)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return ""
	}
	if len(msg.Items) == 0 {
		return ""
	}

	pmsgTextCont := s.getLarkTextMsgContent(ctx, msg.Items[0].Body.Content)
	msgTextCont := s.getLarkTextMsgContent(ctx, me.Message.Content)

	return pmsgTextCont + "\n\n" + msgTextCont
}

func (s *ChatService) getLarkTextMsgContent(ctx context.Context, contentStr string) string {
	var msgTextCont MsgTextCont
	if err := json.Unmarshal([]byte(contentStr), &msgTextCont); err != nil {
		log.L(ctx).Error(err.Error())

		return ""
	}

	return msgTextCont.Text
}

func (s *ChatService) retrieveParentMsg(ctx context.Context, me *v1.LarkMsgEvent) *v1.Message {
	upid := me.Message.ParentID
	if upid != "" {
		m, err := s.store.Message().GetByID(ctx, upid)
		if err == nil && m != nil {
			return m
		}
	}

	if me.Message.ChatType == "group" || me.Message.ChatType == "topic_group" {
		return nil
	}

	lastm, err := s.store.Message().GetLastSessionMsg(ctx, me.Sender.SenderID.UserID)
	if err != nil {
		return nil
	}

	if lastm.Semantic == v1.SemanticEndSession || lastm.CreatedAt.Before(time.Now().Add(-time.Minute*15)) {
		return nil
	}

	return lastm
}

func (s *ChatService) SendTextLarkMsg(ctx context.Context, me *v1.LarkMsgEvent, msg string) (*v1.LarkReplyMsgResData, error) {
	atUser := ""
	if me.Message.ChatType == v1.ChatTypeGroup || me.Message.ChatType == v1.ChatTypeTopicGroup {
		sendU, err := s.store.User().GetByAnyID(ctx, me.Sender.SenderID.UserID)
		if err != nil {
			log.L(ctx).Error("User not found for " + me.Sender.SenderID.UserID + ", err: " + err.Error())

			return nil, errors.WithCode(code.ErrUserNotFound, "")
		}
		atUser = "<at user_id=\"" + me.Sender.SenderID.UserID + "\">" + sendU.EnName + "</at> "
	}

	cont, err := json.Marshal(MsgTextCont{
		Text: atUser + msg,
	})
	if err != nil {
		log.L(ctx).Errorf("error when marshal lark msg, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrEncodingJSON, "")
	}
	rmsg := &v1.LarkReplyMsg{
		MsgType: "text",
		Content: string(cont),
	}

	return s.replyLarkMsg(ctx, me, rmsg)
}

type MsgShareCont struct {
	ChatID string `json:"chat_id"`
}

func (s *ChatService) SendShareLarkChat(ctx context.Context, me *v1.LarkMsgEvent, chatid string) (*v1.LarkReplyMsgResData, error) {
	cont, err := json.Marshal(MsgShareCont{
		ChatID: chatid,
	})
	if err != nil {
		log.L(ctx).Errorf("error when marshal lark msg, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrEncodingJSON, "")
	}
	rmsg := &v1.LarkReplyMsg{
		MsgType: "share_chat",
		Content: string(cont),
	}

	return s.replyLarkMsg(ctx, me, rmsg)
}

func (s *ChatService) replyLarkMsg(ctx context.Context, me *v1.LarkMsgEvent, rmsg *v1.LarkReplyMsg) (*v1.LarkReplyMsgResData, error) {
	reqb, err := json.Marshal(rmsg)
	if err != nil {
		log.L(ctx).Errorf("error when marshal reply msg request body, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrEncodingJSON, "")
	}

	cs := &LarkAPIService{store: s.store}
	jsonraw, err := cs.ReqLarkAPI(ctx, "POST", LarkOpenAPIBase+"/im/v1/messages/"+me.Message.MessageID+"/reply", bytes.NewReader(reqb), nil)
	if err != nil {
		log.L(ctx).Errorf("error when call lark send msg api, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrHttpRequest, "")
	}

	var larkm v1.LarkReplyMsgResData
	if err = json.Unmarshal(jsonraw, &larkm); err != nil {
		log.L(ctx).Errorf("error when unmarshal lark msg, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrDecodingJSON, "")
	}

	return &larkm, nil
}

func (s *ChatService) BotSendSharedChatLarkMsg(ctx context.Context, idType, receiverid, chatid string) (*v1.LarkReplyMsgResData, error) {
	cont, err := json.Marshal(MsgShareCont{
		ChatID: chatid,
	})
	if err != nil {
		log.L(ctx).Errorf("error when marshal lark msg, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrEncodingJSON, "")
	}
	reqb, err := json.Marshal(&v1.LarkSendMsgReqData{
		MsgType:   "share_chat",
		Content:   string(cont),
		ReceiveId: receiverid,
	})
	if err != nil {
		log.L(ctx).Errorf("error when marshal send-msg request body, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrEncodingJSON, "")
	}

	return s.botSendLarkMsg(ctx, idType, reqb)
}

func (s *ChatService) BotSendTextLarkMsg(ctx context.Context, idType, uid, msg string) (*v1.LarkReplyMsgResData, error) {
	cont, err := json.Marshal(MsgTextCont{
		Text: msg,
	})
	if err != nil {
		log.L(ctx).Errorf("error when marshal lark msg, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrEncodingJSON, "")
	}

	reqb, err := json.Marshal(&v1.LarkSendMsgReqData{
		MsgType:   "text",
		Content:   string(cont),
		ReceiveId: uid,
	})
	if err != nil {
		log.L(ctx).Errorf("error when marshal send-msg request body, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrEncodingJSON, "")
	}

	return s.botSendLarkMsg(ctx, idType, reqb)
}

func (s *ChatService) botSendLarkMsg(ctx context.Context, idType string, reqb []byte) (*v1.LarkReplyMsgResData, error) {
	cs := &LarkAPIService{store: s.store}
	jsonraw, err := cs.ReqLarkAPI(ctx, "POST", LarkOpenAPIBase+"/im/v1/messages?receive_id_type="+idType, bytes.NewReader(reqb), nil)
	if err != nil {
		log.L(ctx).Errorf("error when call lark send msg api, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrHttpRequest, "")
	}

	var larkm v1.LarkReplyMsgResData
	if err = json.Unmarshal(jsonraw, &larkm); err != nil {
		log.L(ctx).Errorf("error when unmarshal lark msg, err: %s", err.Error())

		return nil, errors.WithCode(code.ErrDecodingJSON, "")
	}

	return &larkm, nil
}
