package conversation_msg

import (
	"context"
	"encoding/json"
	"github.com/jinzhu/copier"
	"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
	"github.com/openimsdk/openim-sdk-core/v3/pkg/db/model_struct"
	"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
	"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
	"github.com/openimsdk/protocol/sdkws"
	"github.com/openimsdk/tools/errs"
	"github.com/openimsdk/tools/log"
	"github.com/openimsdk/tools/utils/datautil"
	"github.com/patrickmn/go-cache"
	"time"
)

const (
	_ int = iota
	stateCodeSuccess
	stateCodeEnd
)

const (
	inputStatesSendTime   = time.Second * 10                            // input status sending interval time
	inputStatesTimeout    = inputStatesSendTime + inputStatesSendTime/2 // input status timeout
	inputStatesMsgTimeout = inputStatesSendTime / 2                     // message sending timeout
)

func newTyping(c *Conversation) *typing {
	e := &typing{
		conv:  c,
		send:  cache.New(inputStatesSendTime, inputStatesTimeout),
		state: cache.New(inputStatesTimeout, inputStatesTimeout),
	}
	e.platformIDs = make([]int32, 0, len(constant.PlatformID2Name))
	e.platformIDSet = make(map[int32]struct{})
	for id := range constant.PlatformID2Name {
		e.platformIDSet[int32(id)] = struct{}{}
		e.platformIDs = append(e.platformIDs, int32(id))
	}
	datautil.Sort(e.platformIDs, true)
	e.state.OnEvicted(func(key string, val interface{}) {
		var data inputStatesKey
		if err := json.Unmarshal([]byte(key), &data); err != nil {
			return
		}
		e.changes(data.ConversationID, data.UserID)
	})
	return e
}

type typing struct {
	send  *cache.Cache
	state *cache.Cache

	conv *Conversation

	platformIDs   []int32
	platformIDSet map[int32]struct{}
}

func (e *typing) ChangeInputStates(ctx context.Context, conversationID string, focus bool) error {
	if conversationID == "" {
		return errs.ErrArgs.WrapMsg("conversationID can't be empty")
	}
	conversation, err := e.conv.db.GetConversation(ctx, conversationID)
	if err != nil {
		return err
	}
	key := conversation.ConversationID
	if focus {
		if val, ok := e.send.Get(key); ok {
			if val.(int) == stateCodeSuccess {
				log.ZDebug(ctx, "typing stateCodeSuccess", "conversationID", conversationID, "focus", focus)
				return nil
			}
		}
		e.send.SetDefault(key, stateCodeSuccess)
	} else {
		if val, ok := e.send.Get(key); ok {
			if val.(int) == stateCodeEnd {
				log.ZDebug(ctx, "typing stateCodeEnd", "conversationID", conversationID, "focus", focus)
				return nil
			}
			e.send.SetDefault(key, stateCodeEnd)
		} else {
			log.ZDebug(ctx, "typing send not found", "conversationID", conversationID, "focus", focus)
			return nil
		}
	}
	ctx, cancel := context.WithTimeout(ctx, inputStatesMsgTimeout)
	defer cancel()
	if err := e.sendMsg(ctx, conversation, focus); err != nil {
		e.send.Delete(key)
		return err
	}
	return nil
}

func (e *typing) sendMsg(ctx context.Context, conversation *model_struct.LocalConversation, focus bool) error {
	s := sdk_struct.MsgStruct{}
	err := e.conv.initBasicInfo(ctx, &s, constant.UserMsgType, constant.Typing)
	if err != nil {
		return err
	}
	s.RecvID = conversation.UserID
	s.GroupID = conversation.GroupID
	s.SessionType = conversation.ConversationType
	var typingElem sdk_struct.TypingElem
	if focus {
		typingElem.MsgTips = "yes"
	} else {
		typingElem.MsgTips = "no"
	}
	s.Content = utils.StructToJsonString(typingElem)
	options := make(map[string]bool, 6)
	utils.SetSwitchFromOptions(options, constant.IsHistory, false)
	utils.SetSwitchFromOptions(options, constant.IsPersistent, false)
	utils.SetSwitchFromOptions(options, constant.IsSenderSync, false)
	utils.SetSwitchFromOptions(options, constant.IsConversationUpdate, false)
	utils.SetSwitchFromOptions(options, constant.IsSenderConversationUpdate, false)
	utils.SetSwitchFromOptions(options, constant.IsUnreadCount, false)
	utils.SetSwitchFromOptions(options, constant.IsOfflinePush, false)
	var wsMsgData sdkws.MsgData
	copier.Copy(&wsMsgData, s)
	wsMsgData.Content = []byte(s.Content)
	wsMsgData.CreateTime = s.CreateTime
	wsMsgData.Options = options
	var sendMsgResp sdkws.UserSendMsgResp
	err = e.conv.LongConnMgr.SendReqWaitResp(ctx, &wsMsgData, constant.SendMsg, &sendMsgResp)
	if err != nil {
		log.ZError(ctx, "typing msg to server failed", err, "message", s)
		return err
	}
	return nil
}

type inputStatesKey struct {
	ConversationID string `json:"cid,omitempty"`
	UserID         string `json:"uid,omitempty"`
	PlatformID     int32  `json:"pid,omitempty"`
}

func (e *typing) getStateKey(conversationID string, userID string, platformID int32) string {
	data, err := json.Marshal(inputStatesKey{ConversationID: conversationID, UserID: userID, PlatformID: platformID})
	if err != nil {
		panic(err)
	}
	return string(data)
}

func (e *typing) onNewMsg(ctx context.Context, msg *sdkws.MsgData) {
	var enteringElem sdk_struct.TypingElem
	if err := json.Unmarshal(msg.Content, &enteringElem); err != nil {
		log.ZError(ctx, "typing onNewMsg Unmarshal failed", err, "message", msg)
		return
	}
	if msg.SendID == e.conv.loginUserID {
		return
	}
	if _, ok := e.platformIDSet[msg.SenderPlatformID]; !ok {
		return
	}
	now := time.Now().UnixMilli()
	expirationTimestamp := msg.SendTime + int64(inputStatesSendTime/time.Millisecond)
	if msg.SendTime > now || expirationTimestamp <= now {
		return
	}
	var sourceID string
	if msg.GroupID == "" {
		sourceID = msg.SendID
	} else {
		sourceID = msg.GroupID
	}
	conversationID := e.conv.getConversationIDBySessionType(sourceID, int(msg.SessionType))
	key := e.getStateKey(conversationID, msg.SendID, msg.SenderPlatformID)
	if enteringElem.MsgTips == "yes" {
		d := time.Duration(expirationTimestamp-now) * time.Millisecond
		if v, t, ok := e.state.GetWithExpiration(key); ok {
			if t.UnixMilli() >= expirationTimestamp {
				return
			}
			e.state.Set(key, v, d)
		} else {
			e.state.Set(key, struct{}{}, d)
			e.changes(conversationID, msg.SendID)
		}
	} else {
		if _, ok := e.state.Get(key); ok {
			e.state.Delete(key)
		}
	}
}

type InputStatesChangedData struct {
	ConversationID string  `json:"conversationID"`
	UserID         string  `json:"userID"`
	PlatformIDs    []int32 `json:"platformIDs"`
}

func (e *typing) changes(conversationID string, userID string) {
	data := InputStatesChangedData{ConversationID: conversationID, UserID: userID, PlatformIDs: e.GetInputStates(conversationID, userID)}
	e.conv.ConversationListener().OnConversationUserInputStatusChanged(utils.StructToJsonString(data))
}

func (e *typing) GetInputStates(conversationID string, userID string) []int32 {
	platformIDs := make([]int32, 0, 1)
	for _, platformID := range e.platformIDs {
		key := e.getStateKey(conversationID, userID, platformID)
		if _, ok := e.state.Get(key); ok {
			platformIDs = append(platformIDs, platformID)
		}
	}
	return platformIDs
}
