package services

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"agent-desk/internal/models"
	"agent-desk/internal/pkg/enums"
	"agent-desk/internal/pkg/i18nx"
	"agent-desk/internal/pkg/utils"
	"agent-desk/internal/repositories"
	"agent-desk/internal/wxwork"

	"github.com/mlogclub/simple/common/strs"
	"github.com/mlogclub/simple/sqls"
	"github.com/silenceper/wechat/v2/work/kf/sendmsg"
)

const (
	wxWorkKFOutboxBatchSize = 20
	wxWorkKFOutboxMaxRetry  = 6
)

var WxWorkKFOutboundService = newWxWorkKFOutboundService()

func newWxWorkKFOutboundService() *wxWorkKFOutboundService {
	return &wxWorkKFOutboundService{}
}

type wxWorkKFOutboundService struct {
}

type wxWorkKFOutboundChunk struct {
	MessageType enums.IMMessageType
	Content     string
	AssetID     string
}

func (s *wxWorkKFOutboundService) DispatchPendingOutbox() int {
	if !wxwork.Enabled() {
		return 0
	}

	var totalCount int = 0
	for {
		count := s.doDispatchPendingOutbox(wxWorkKFOutboxBatchSize)

		totalCount += count
		if count > 0 {
			slog.Info("wxwork kf outbound dispatch loop",
				"batch_count", count,
				"total_count", totalCount,
			)
		}

		if count == 0 {
			break
		}
	}
	return totalCount
}

func (s *wxWorkKFOutboundService) doDispatchPendingOutbox(limit int) int {
	if !wxwork.Enabled() {
		return 0
	}
	if limit <= 0 {
		limit = wxWorkKFOutboxBatchSize
	}

	items := ChannelMessageOutboxService.ListPending(enums.ChannelTypeWxWorkKF, limit)
	if len(items) == 0 {
		return 0
	}

	successCount := 0
	for i := range items {
		if err := s.processOutbox(items[i].ID); err != nil {
			slog.Warn("process wxwork kf outbox failed",
				"outbox_id", items[i].ID,
				"conversation_id", items[i].ConversationID,
				"message_id", items[i].MessageID,
				"error", err,
			)
			continue
		}
		successCount++
	}
	return successCount
}

func (s *wxWorkKFOutboundService) processOutbox(outboxID int64) error {
	outbox := ChannelMessageOutboxService.Get(outboxID)
	if outbox == nil {
		return nil
	}
	if outbox.ChannelType != enums.ChannelTypeWxWorkKF {
		return nil
	}
	if outbox.SendStatus == string(enums.ChannelMessageOutboxStatusSent) {
		return nil
	}
	if outbox.NextRetryAt != nil && outbox.NextRetryAt.After(time.Now()) {
		return nil
	}

	slog.Info("processing wxwork kf outbox",
		"outbox_id", outbox.ID,
		"conversation_id", outbox.ConversationID,
		"message_id", outbox.MessageID,
		"send_status", outbox.SendStatus,
		"retry_count", outbox.RetryCount,
	)

	now := time.Now()
	if err := ChannelMessageOutboxService.Updates(outbox.ID, map[string]any{
		"send_status":      string(enums.ChannelMessageOutboxStatusSending),
		"updated_at":       now,
		"update_user_id":   outbox.UpdateUserID,
		"update_user_name": outbox.UpdateUserName,
	}); err != nil {
		return err
	}

	message := MessageService.Get(outbox.MessageID)
	if message == nil {
		return s.markOutboxFailed(outbox, "平台消息不存在")
	}
	conversation := ConversationService.Get(outbox.ConversationID)
	if conversation == nil {
		return s.markOutboxFailed(outbox, "平台会话不存在")
	}
	mapping := WxWorkKFConversationService.Take("conversation_id = ?", conversation.ID)
	if mapping == nil {
		return s.markOutboxFailed(outbox, "企业微信会话映射不存在")
	}
	if mapping.ChannelID <= 0 {
		return s.markOutboxFailed(outbox, "企业微信会话映射缺少渠道ID")
	}
	channel := ChannelService.Get(mapping.ChannelID)
	if channel == nil || channel.Status != enums.StatusOk || channel.ChannelType != enums.ChannelTypeWxWorkKF {
		return s.markOutboxFailed(outbox, "企业微信接入渠道不存在、未启用或类型不匹配")
	}
	if strings.TrimSpace(mapping.OpenKfID) == "" || strings.TrimSpace(mapping.ExternalUserID) == "" {
		return s.markOutboxFailed(outbox, "企业微信会话映射缺少发送必要参数")
	}
	chunks, buildErr := s.buildOutboundChunks(message)
	if buildErr != nil {
		return s.markOutboxFailed(outbox, buildErr.Error())
	}
	if len(chunks) == 0 {
		return s.markOutboxFailed(outbox, "当前消息无法转换为企业微信下行消息")
	}

	slog.Info("built wxwork outbound chunks",
		"outbox_id", outbox.ID,
		"conversation_id", conversation.ID,
		"message_id", message.ID,
		"sender_type", message.SenderType,
		"message_type", message.MessageType,
		"chunk_count", len(chunks),
		"open_kfid", mapping.OpenKfID,
		"external_userid", mapping.ExternalUserID,
	)

	wxMsgIDs := make([]string, 0, len(chunks))
	for i := range chunks {
		wxMsgID, sendErr := s.sendOutboundChunk(mapping, message, chunks[i], i)
		if sendErr != nil {
			return s.markOutboxFailed(outbox, sendErr.Error())
		}
		wxMsgIDs = append(wxMsgIDs, wxMsgID)
	}

	return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
		now = time.Now()
		if err := repositories.ChannelMessageOutboxRepository.Updates(ctx.Tx, outbox.ID, map[string]any{
			"send_status":      string(enums.ChannelMessageOutboxStatusSent),
			"sent_at":          now,
			"last_error":       "",
			"updated_at":       now,
			"update_user_id":   outbox.UpdateUserID,
			"update_user_name": outbox.UpdateUserName,
		}); err != nil {
			return err
		}
		if existing := repositories.WxWorkKFMessageRefRepository.Take(ctx.Tx, "message_id = ? AND direction = ?", message.ID, string(enums.WxWorkKFMessageDirectionOut)); existing == nil {
			for i := range wxMsgIDs {
				rawPayload := strings.TrimSpace(outbox.Payload)
				if len(chunks) > i {
					if payload, err := json.Marshal(map[string]any{
						"messageId":    message.ID,
						"chunkIndex":   i,
						"chunkType":    chunks[i].MessageType,
						"chunkText":    strings.TrimSpace(chunks[i].Content),
						"chunkAssetId": strings.TrimSpace(chunks[i].AssetID),
					}); err == nil {
						rawPayload = string(payload)
					}
				}
				if err := repositories.WxWorkKFMessageRefRepository.Create(ctx.Tx, &models.WxWorkKFMessageRef{
					ConversationID: conversation.ID,
					MessageID:      message.ID,
					WxMsgID:        strings.TrimSpace(wxMsgIDs[i]),
					Direction:      string(enums.WxWorkKFMessageDirectionOut),
					Origin:         0,
					OpenKfID:       mapping.OpenKfID,
					ExternalUserID: mapping.ExternalUserID,
					SendStatus:     string(enums.WxWorkKFMessageSendStatusSent),
					RawPayload:     rawPayload,
					Status:         enums.StatusOk,
					AuditFields: models.AuditFields{
						CreatedAt:      now,
						CreateUserID:   outbox.UpdateUserID,
						CreateUserName: outbox.UpdateUserName,
						UpdatedAt:      now,
						UpdateUserID:   outbox.UpdateUserID,
						UpdateUserName: outbox.UpdateUserName,
					},
				}); err != nil {
					return err
				}
			}
		}
		return ConversationEventLogService.CreateEvent(ctx, conversation.ID, enums.IMEventTypeWxWorkKFOutbound, message.SenderType, message.SenderID, fmt.Sprintf("企业微信消息发送成功，共%d条", len(wxMsgIDs)), "")
	})
}

func (s *wxWorkKFOutboundService) sendOutboundChunk(mapping *models.WxWorkKFConversation, message *models.Message, chunk wxWorkKFOutboundChunk, chunkIndex int) (string, error) {
	switch chunk.MessageType {
	case enums.IMMessageTypeText:
		return s.sendTextMessage(mapping, message, chunk.Content, chunkIndex)
	case enums.IMMessageTypeImage:
		return s.sendImageMessage(mapping, message, chunk, chunkIndex)
	default:
		return "", i18nx.Errorf("error.wxwork.unsupportedOutboundMessageType", chunk.MessageType)
	}
}

func (s *wxWorkKFOutboundService) sendTextMessage(mapping *models.WxWorkKFConversation, message *models.Message, content string, chunkIndex int) (string, error) {
	cli, err := wxwork.GetWorkCli().GetKF()
	if err != nil {
		return "", err
	}

	req := sendmsg.Text{}
	req.Message.ToUser = strings.TrimSpace(mapping.ExternalUserID)
	req.Message.OpenKFID = strings.TrimSpace(mapping.OpenKfID)
	req.Message.MsgID = s.buildOutboundClientMsgID(message.ID, chunkIndex)
	req.MsgType = "text"
	req.Text.Content = strings.TrimSpace(content)

	slog.Info("sending wxwork text message",
		"conversation_id", message.ConversationID,
		"message_id", message.ID,
		"chunk_index", chunkIndex,
		"client_msg_id", req.Message.MsgID,
		"open_kfid", req.Message.OpenKFID,
		"external_userid", req.Message.ToUser,
		"content_length", len([]rune(req.Text.Content)),
	)

	resp, err := cli.SendMsg(req)
	if err != nil {
		return "", err
	}
	if strings.TrimSpace(resp.MsgID) == "" {
		return "", i18nx.Errorf("error.e0114")
	}
	slog.Info("wxwork text message accepted",
		"conversation_id", message.ConversationID,
		"message_id", message.ID,
		"chunk_index", chunkIndex,
		"client_msg_id", req.Message.MsgID,
		"wx_msg_id", strings.TrimSpace(resp.MsgID),
		"open_kfid", req.Message.OpenKFID,
		"external_userid", req.Message.ToUser,
	)
	return strings.TrimSpace(resp.MsgID), nil
}

func (s *wxWorkKFOutboundService) sendImageMessage(mapping *models.WxWorkKFConversation, message *models.Message, chunk wxWorkKFOutboundChunk, chunkIndex int) (string, error) {
	if strings.TrimSpace(chunk.AssetID) == "" {
		return "", i18nx.Errorf("error.e0145")
	}

	asset := AssetService.GetByAssetID(chunk.AssetID)
	if asset == nil {
		return "", i18nx.Errorf("error.e0146")
	}
	fileReader, err := AssetService.OpenReader(asset)
	if err != nil {
		return "", err
	}
	defer func() {
		if fileReader != nil {
			_ = fileReader.Close()
		}
	}()

	slog.Info("sending wxwork image message",
		"conversation_id", message.ConversationID,
		"message_id", message.ID,
		"chunk_index", chunkIndex,
		"asset_id", asset.AssetID,
		"filename", asset.Filename,
		"storage_key", asset.StorageKey,
		"open_kfid", mapping.OpenKfID,
		"external_userid", mapping.ExternalUserID,
	)

	materialCli := wxwork.GetWorkCli().GetMaterial()
	uploadResp, err := materialCli.UploadTempFileFromReader(asset.Filename, "image", fileReader)
	if err != nil {
		return "", err
	}
	if strings.TrimSpace(uploadResp.MediaID) == "" {
		return "", i18nx.Errorf("error.e0113")
	}

	kfCli, err := wxwork.GetWorkCli().GetKF()
	if err != nil {
		return "", err
	}
	req := sendmsg.Image{
		Message: sendmsg.Message{
			ToUser:   strings.TrimSpace(mapping.ExternalUserID),
			OpenKFID: strings.TrimSpace(mapping.OpenKfID),
			MsgID:    s.buildOutboundClientMsgID(message.ID, chunkIndex),
		},
		MsgType: "image",
	}
	req.Image.MediaID = strings.TrimSpace(uploadResp.MediaID)

	resp, err := kfCli.SendMsg(req)
	if err != nil {
		return "", err
	}
	if strings.TrimSpace(resp.MsgID) == "" {
		return "", i18nx.Errorf("error.e0114")
	}
	slog.Info("wxwork image message accepted",
		"conversation_id", message.ConversationID,
		"message_id", message.ID,
		"chunk_index", chunkIndex,
		"client_msg_id", req.Message.MsgID,
		"wx_msg_id", strings.TrimSpace(resp.MsgID),
		"media_id", req.Image.MediaID,
		"asset_id", asset.AssetID,
		"open_kfid", req.Message.OpenKFID,
		"external_userid", req.Message.ToUser,
	)
	return strings.TrimSpace(resp.MsgID), nil
}

func (s *wxWorkKFOutboundService) markOutboxFailed(outbox *models.ChannelMessageOutbox, errMsg string) error {
	if outbox == nil {
		return nil
	}
	slog.Warn("mark wxwork kf outbox failed",
		"outbox_id", outbox.ID,
		"conversation_id", outbox.ConversationID,
		"message_id", outbox.MessageID,
		"retry_count", outbox.RetryCount+1,
		"error", strings.TrimSpace(errMsg),
	)
	now := time.Now()
	retryCount := outbox.RetryCount + 1
	nextRetryAt := s.nextRetryAt(retryCount)
	status := string(enums.ChannelMessageOutboxStatusFailed)
	if retryCount >= wxWorkKFOutboxMaxRetry {
		nextRetryAt = nil
	}
	return ChannelMessageOutboxService.Updates(outbox.ID, map[string]any{
		"send_status":      status,
		"retry_count":      retryCount,
		"next_retry_at":    nextRetryAt,
		"last_error":       strings.TrimSpace(errMsg),
		"updated_at":       now,
		"update_user_id":   outbox.UpdateUserID,
		"update_user_name": outbox.UpdateUserName,
	})
}

func (s *wxWorkKFOutboundService) nextRetryAt(retryCount int) *time.Time {
	delay := time.Minute
	switch {
	case retryCount <= 1:
		delay = 30 * time.Second
	case retryCount == 2:
		delay = time.Minute
	case retryCount == 3:
		delay = 2 * time.Minute
	default:
		delay = 5 * time.Minute
	}
	t := time.Now().Add(delay)
	return &t
}

func (s *wxWorkKFOutboundService) buildOutboundClientMsgID(messageID int64, chunkIndex int) string {
	return fmt.Sprintf("outbox_wxwork_kf_%d_%d", messageID, chunkIndex)
}

type wxWorkKFOutboundPayload struct {
	ConversationID int64               `json:"conversationId"`
	MessageID      int64               `json:"messageId"`
	MessageType    enums.IMMessageType `json:"messageType"`
	Content        string              `json:"content"`
	Payload        string              `json:"payload"`
	SenderID       int64               `json:"senderId"`
}

func (s *wxWorkKFOutboundService) parseOutboxPayload(raw string) (*wxWorkKFOutboundPayload, error) {
	payload := &wxWorkKFOutboundPayload{}
	if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), payload); err != nil {
		return nil, err
	}
	return payload, nil
}

func (s *wxWorkKFOutboundService) buildOutboundChunks(message *models.Message) ([]wxWorkKFOutboundChunk, error) {
	if message == nil {
		return nil, i18nx.Errorf("error.e0186")
	}
	switch message.MessageType {
	case enums.IMMessageTypeText:
		content := strings.TrimSpace(message.Content)
		if content == "" {
			return nil, i18nx.Errorf("error.e0217")
		}
		return []wxWorkKFOutboundChunk{{MessageType: enums.IMMessageTypeText, Content: content}}, nil
	case enums.IMMessageTypeHTML:
		return s.buildHTMLChunks(message.Content)
	default:
		return nil, i18nx.Errorf("error.wxwork.currentUnsupportedOutboundMessageType", message.MessageType)
	}
}

func (s *wxWorkKFOutboundService) buildHTMLChunks(content string) ([]wxWorkKFOutboundChunk, error) {
	contentChunks, err := utils.SplitHTMLContentChunks(content)
	if err != nil {
		return nil, err
	}
	chunks := make([]wxWorkKFOutboundChunk, 0, len(contentChunks))
	for _, chunk := range contentChunks {
		switch chunk.Type {
		case utils.ContentChunkTypeText:
			if strs.IsNotBlank(chunk.Content) {
				chunks = append(chunks, wxWorkKFOutboundChunk{
					MessageType: enums.IMMessageTypeText,
					Content:     chunk.Content,
				})
			}
		case utils.ContentChunkTypeImage:
			if strs.IsBlank(chunk.AssetID) {
				chunks = append(chunks, wxWorkKFOutboundChunk{
					MessageType: enums.IMMessageTypeText,
					Content:     "[图片]",
				})
			} else {
				chunks = append(chunks, wxWorkKFOutboundChunk{
					MessageType: enums.IMMessageTypeImage,
					AssetID:     chunk.AssetID,
				})
			}
		}
	}
	if len(chunks) == 0 {
		return nil, i18nx.Errorf("error.e0029")
	}
	return chunks, nil
}
