package lark

import (
	"context"
	"encoding/json"
	"strings"

	larkevent "github.com/larksuite/oapi-sdk-go/v3/event"
	"github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
	"github.com/larksuite/oapi-sdk-go/v3/event/dispatcher/callback"
	larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
	"github.com/larksuite/oapi-sdk-go/v3/ws"

	"github.com/tidbcloud/alertstore/pkg/im"
)

// StartLongConnection receives Feishu events over an outbound WebSocket.
// The caller should run it in a goroutine: the SDK reconnects automatically
// and blocks for the lifetime of the process.
//
// handle returns a result only for card actions, which Feishu keeps open until
// something comes back to show whoever clicked. Every other event is answered
// immediately and the return value ignored.
func (c *Client) StartLongConnection(ctx context.Context, handle func(context.Context, *im.IncomingEvent) *im.CardActionResult) error {
	eventHandler := dispatcher.NewEventDispatcher("", "").
		OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
			handle(ctx, incomingMessageEvent(event))
			return nil
		}).
		OnP2MessageRecalledV1(func(ctx context.Context, event *larkim.P2MessageRecalledV1) error {
			handle(ctx, incomingRecallEvent(event))
			return nil
		}).
		OnP2CardActionTrigger(func(ctx context.Context, event *callback.CardActionTriggerEvent) (*callback.CardActionTriggerResponse, error) {
			return cardActionResponse(handle(ctx, incomingCardActionEvent(event))), nil
		})

	return ws.NewClient(c.appID, c.appSecret, ws.WithEventHandler(eventHandler)).Start(ctx)
}

func incomingCardActionEvent(event *callback.CardActionTriggerEvent) *im.IncomingEvent {
	incoming := &im.IncomingEvent{EventID: eventID(event.EventV2Base)}
	if event.Event == nil || event.Event.Action == nil {
		return incoming
	}

	action := &im.IncomingCardAction{Value: map[string]string{}}
	// Values survive the round trip as JSON, so anything that was not a string
	// when the card was built comes back as some other type. Cards are ours to
	// build, so a non-string here means a bug, not input to defend against.
	for k, v := range event.Event.Action.Value {
		if str, ok := v.(string); ok {
			action.Value[k] = str
		}
	}
	if event.Event.Operator != nil {
		action.UserID = event.Event.Operator.OpenID
	}
	if event.Event.Context != nil {
		action.MessageID = event.Event.Context.OpenMessageID
		action.ChannelID = event.Event.Context.OpenChatID
	}

	incoming.CardAction = action
	return incoming
}

// cardActionResponse turns a handler result into what Feishu shows the person
// who clicked. A nil result means the click produced nothing worth reporting.
func cardActionResponse(result *im.CardActionResult) *callback.CardActionTriggerResponse {
	if result == nil {
		return nil
	}

	resp := &callback.CardActionTriggerResponse{}
	if result.Toast != "" {
		toastType := "success"
		if result.ToastError {
			toastType = "error"
		}
		resp.Toast = &callback.Toast{Type: toastType, Content: result.Toast}
	}
	if result.Card != nil {
		resp.Card = &callback.Card{Type: "raw", Data: buildCardContent(result.Card)}
	}
	return resp
}

func incomingMessageEvent(event *larkim.P2MessageReceiveV1) *im.IncomingEvent {
	incoming := &im.IncomingEvent{EventID: eventID(event.EventV2Base)}
	if event.Event == nil || event.Event.Sender == nil || event.Event.Message == nil || stringValue(event.Event.Sender.SenderType) != "user" {
		return incoming
	}

	message := event.Event.Message
	incoming.Message = &im.IncomingMessage{
		MessageID: stringValue(message.MessageId),
		ChannelID: stringValue(message.ChatId),
		ThreadID:  stringValue(message.ThreadId),
		MsgType:   stringValue(message.MessageType),
	}
	if event.Event.Sender.SenderId != nil {
		incoming.Message.SenderID = stringValue(event.Event.Sender.SenderId.OpenId)
	}
	for _, mention := range message.Mentions {
		if mention == nil {
			continue
		}
		userID := ""
		if mention.Id != nil {
			userID = stringValue(mention.Id.OpenId)
		}
		incoming.Message.Mentions = append(incoming.Message.Mentions, im.Mention{UserID: userID, Name: stringValue(mention.Name)})
	}
	if incoming.Message.MsgType == "text" {
		var content struct {
			Text string `json:"text"`
		}
		if err := json.Unmarshal([]byte(stringValue(message.Content)), &content); err == nil {
			for _, mention := range message.Mentions {
				if mention != nil {
					content.Text = strings.ReplaceAll(content.Text, stringValue(mention.Key), "")
				}
			}
			incoming.Message.Text = strings.Join(strings.Fields(content.Text), " ")
		}
	}
	return incoming
}

func incomingRecallEvent(event *larkim.P2MessageRecalledV1) *im.IncomingEvent {
	incoming := &im.IncomingEvent{EventID: eventID(event.EventV2Base)}
	if event.Event != nil {
		incoming.Recall = &im.RecallEvent{MessageID: stringValue(event.Event.MessageId)}
	}
	return incoming
}

func eventID(base *larkevent.EventV2Base) string {
	if base == nil || base.Header == nil {
		return ""
	}
	return base.Header.EventID
}

func stringValue(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}
