//go:build !omit_whatsapp

package channels

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"mime"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/gorilla/websocket"
	"go.mau.fi/whatsmeow"
	waProto "go.mau.fi/whatsmeow/proto/waE2E"
	"go.mau.fi/whatsmeow/store/sqlstore"
	"go.mau.fi/whatsmeow/types"
	"go.mau.fi/whatsmeow/types/events"
	waLog "go.mau.fi/whatsmeow/util/log"
	"google.golang.org/protobuf/proto"
	_ "modernc.org/sqlite"
)

type WhatsAppBridgeService struct {
	addr        string
	stateDir    string
	printQR     bool
	httpServer  *http.Server
	client      *whatsmeow.Client
	container   *sqlstore.Container
	rawDB       *sql.DB
	cancel      context.CancelFunc
	wsUpgrader  websocket.Upgrader
	wsClients   map[*websocket.Conn]struct{}
	statusMu    sync.RWMutex
	status      WhatsAppBridgeStatus
	wsClientsMu sync.Mutex
	markReadFn  func(ctx context.Context, ids []types.MessageID, timestamp time.Time, chat, sender types.JID) error
	localOnly   bool
}

type whatsappBridgeWSMessage struct {
	Type          string   `json:"type"`
	To            string   `json:"to,omitempty"`
	From          string   `json:"from,omitempty"`
	Chat          string   `json:"chat,omitempty"`
	Content       string   `json:"content,omitempty"`
	ReplyToID     string   `json:"reply_to_id,omitempty"`
	ReplyToSender string   `json:"reply_to_sender,omitempty"`
	ID            string   `json:"id,omitempty"`
	FromName      string   `json:"from_name,omitempty"`
	Media         []string `json:"media,omitempty"`
}

func NewWhatsAppBridgeService(addr, stateDir string, printQR bool) *WhatsAppBridgeService {
	return &WhatsAppBridgeService{
		addr:     strings.TrimSpace(addr),
		stateDir: strings.TrimSpace(stateDir),
		printQR:  printQR,
		wsUpgrader: websocket.Upgrader{
			CheckOrigin: func(r *http.Request) bool { return true },
		},
		wsClients: map[*websocket.Conn]struct{}{},
		status: WhatsAppBridgeStatus{
			State:      "starting",
			BridgeAddr: strings.TrimSpace(addr),
			UpdatedAt:  time.Now().Format(time.RFC3339),
		},
	}
}

func (s *WhatsAppBridgeService) Start(ctx context.Context) error {
	if err := s.startRuntime(ctx); err != nil {
		return err
	}

	mux := http.NewServeMux()
	s.RegisterRoutes(mux, "")
	s.httpServer = &http.Server{
		Addr:    s.addr,
		Handler: mux,
	}

	ln, err := net.Listen("tcp", s.addr)
	if err != nil {
		return fmt.Errorf("listen whatsapp bridge: %w", err)
	}

	if err := s.httpServer.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
		return err
	}
	return nil
}

func (s *WhatsAppBridgeService) StartEmbedded(ctx context.Context) error {
	s.localOnly = true
	return s.startRuntime(ctx)
}

func (s *WhatsAppBridgeService) startRuntime(ctx context.Context) error {
	if strings.TrimSpace(s.addr) == "" {
		return fmt.Errorf("bridge address is required")
	}
	if strings.TrimSpace(s.stateDir) == "" {
		return fmt.Errorf("bridge state directory is required")
	}
	if err := os.MkdirAll(s.stateDir, 0o755); err != nil {
		return fmt.Errorf("create whatsapp state dir: %w", err)
	}
	if err := s.initClient(ctx); err != nil {
		return err
	}

	runCtx, cancel := context.WithCancel(ctx)
	s.cancel = cancel

	go func() {
		<-runCtx.Done()
		if s.httpServer != nil {
			shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			defer cancel()
			_ = s.httpServer.Shutdown(shutdownCtx)
		}
		s.closeWSClients()
		if s.client != nil {
			s.client.Disconnect()
		}
		if s.rawDB != nil {
			_ = s.rawDB.Close()
		}
	}()

	go func() {
		_ = s.connectClient(runCtx)
	}()
	return nil
}

func (s *WhatsAppBridgeService) Stop() {
	if s.cancel != nil {
		s.cancel()
	}
}

func (s *WhatsAppBridgeService) RegisterRoutes(mux *http.ServeMux, basePath string) {
	if mux == nil {
		return
	}
	basePath = normalizeBridgeBasePath(basePath)
	mux.HandleFunc(basePath, s.ServeWS)
	mux.HandleFunc(joinBridgeRoute(basePath, "ws"), s.ServeWS)
	mux.HandleFunc(joinBridgeRoute(basePath, "status"), s.ServeStatus)
	mux.HandleFunc(joinBridgeRoute(basePath, "logout"), s.ServeLogout)
}

func (s *WhatsAppBridgeService) StatusSnapshot() WhatsAppBridgeStatus {
	s.statusMu.RLock()
	defer s.statusMu.RUnlock()
	return s.status
}

func (s *WhatsAppBridgeService) initClient(ctx context.Context) error {
	dbPath := filepath.Join(s.stateDir, "whatsmeow.sqlite")
	rawDB, err := sql.Open("sqlite", dbPath)
	if err != nil {
		return fmt.Errorf("open whatsapp sqlite store: %w", err)
	}
	if _, err := rawDB.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
		_ = rawDB.Close()
		return fmt.Errorf("enable whatsapp sqlite foreign keys: %w", err)
	}
	container := sqlstore.NewWithDB(rawDB, "sqlite", waLog.Noop)
	if err := container.Upgrade(ctx); err != nil {
		_ = rawDB.Close()
		return fmt.Errorf("upgrade whatsapp sqlite store: %w", err)
	}
	deviceStore, err := container.GetFirstDevice(ctx)
	if err != nil {
		_ = rawDB.Close()
		return fmt.Errorf("load whatsapp device store: %w", err)
	}
	client := whatsmeow.NewClient(deviceStore, waLog.Noop)
	client.EnableAutoReconnect = true
	client.AddEventHandler(s.handleWAEvent)

	s.rawDB = rawDB
	s.container = container
	s.client = client
	s.markReadFn = func(ctx context.Context, ids []types.MessageID, timestamp time.Time, chat, sender types.JID) error {
		return client.MarkRead(ctx, ids, timestamp, chat, sender)
	}
	if deviceStore.ID != nil {
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.LoggedIn = true
			st.UserJID = deviceStore.ID.String()
			st.State = "stored_session"
			st.LastEvent = "stored_session"
		})
	}
	return nil
}

func (s *WhatsAppBridgeService) connectClient(ctx context.Context) error {
	if s.client == nil {
		return fmt.Errorf("whatsapp bridge client is not initialized")
	}

	var qrChan <-chan whatsmeow.QRChannelItem
	var err error
	if s.client.Store.ID == nil {
		qrChan, err = s.client.GetQRChannel(ctx)
		if err != nil && !errors.Is(err, whatsmeow.ErrQRStoreContainsID) {
			s.updateStatus(func(st *WhatsAppBridgeStatus) {
				st.State = "error"
				st.LastError = err.Error()
				st.LastEvent = "qr_init_failed"
			})
			return err
		}
		if qrChan != nil {
			go s.consumeQRChannel(ctx, qrChan)
		}
	}

	if err := s.client.Connect(); err != nil {
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.State = "error"
			st.Connected = false
			st.LastError = err.Error()
			st.LastEvent = "connect_failed"
		})
		return fmt.Errorf("connect whatsapp bridge: %w", err)
	}
	return nil
}

func (s *WhatsAppBridgeService) consumeQRChannel(ctx context.Context, qrChan <-chan whatsmeow.QRChannelItem) {
	for {
		select {
		case <-ctx.Done():
			return
		case item, ok := <-qrChan:
			if !ok {
				return
			}
			switch item.Event {
			case "code":
				s.updateStatus(func(st *WhatsAppBridgeStatus) {
					st.State = "qr_ready"
					st.QRCode = item.Code
					st.QRAvailable = item.Code != ""
					st.LastEvent = "qr_ready"
				})
			default:
				s.updateStatus(func(st *WhatsAppBridgeStatus) {
					st.LastEvent = item.Event
					if item.Event == whatsmeow.QRChannelSuccess.Event {
						st.State = "paired"
						st.QRCode = ""
						st.QRAvailable = false
					}
				})
			}
		}
	}
}

func (s *WhatsAppBridgeService) handleWAEvent(evt interface{}) {
	switch v := evt.(type) {
	case *events.Connected:
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.State = "connected"
			st.Connected = true
			st.LoggedIn = s.client != nil && s.client.Store.ID != nil
			st.QRCode = ""
			st.QRAvailable = false
			st.LastEvent = "connected"
			if s.client != nil && s.client.Store.ID != nil {
				st.UserJID = s.client.Store.ID.String()
			}
		})
	case *events.Disconnected:
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.Connected = false
			if st.LoggedIn {
				st.State = "disconnected"
			} else {
				st.State = "waiting_qr"
			}
			st.LastEvent = "disconnected"
		})
	case *events.PairSuccess:
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.State = "paired"
			st.LoggedIn = true
			st.UserJID = v.ID.String()
			st.Platform = v.Platform
			st.QRCode = ""
			st.QRAvailable = false
			st.LastEvent = "pair_success"
		})
	case *events.LoggedOut:
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.State = "logged_out"
			st.Connected = false
			st.LoggedIn = false
			st.UserJID = ""
			st.QRCode = ""
			st.QRAvailable = false
			st.LastEvent = "logged_out"
		})
	case *events.StreamReplaced:
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.State = "stream_replaced"
			st.Connected = false
			st.LastEvent = "stream_replaced"
		})
	case *events.ClientOutdated:
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.State = "client_outdated"
			st.Connected = false
			st.LastError = "whatsapp web client outdated"
			st.LastEvent = "client_outdated"
		})
	case *events.ConnectFailure:
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.State = "connect_failed"
			st.Connected = false
			st.LastError = v.Reason.String()
			st.LastEvent = "connect_failure"
		})
	case *events.TemporaryBan:
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.State = "temporary_ban"
			st.Connected = false
			st.LastError = v.String()
			st.LastEvent = "temporary_ban"
		})
	case *events.Message:
		if v.Info.IsFromMe {
			return
		}
		isGroup := v.Info.Chat.Server == types.GroupServer
		mentionedSelf, replyToMe := s.matchCurrentUserContext(v.Message)
		payload := whatsappBridgeWSMessage{
			Type:     "message",
			From:     v.Info.Sender.ToNonAD().String(),
			Chat:     v.Info.Chat.ToNonAD().String(),
			Content:  extractWhatsAppMessageText(v.Message),
			ID:       v.Info.ID,
			FromName: v.Info.PushName,
		}
		s.broadcastWSMap(map[string]interface{}{
			"type":           payload.Type,
			"from":           payload.From,
			"chat":           payload.Chat,
			"content":        payload.Content,
			"id":             payload.ID,
			"from_name":      payload.FromName,
			"is_group":       isGroup,
			"mentioned_self": mentionedSelf,
			"reply_to_me":    replyToMe,
		})
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.InboundCount++
			st.LastInboundAt = time.Now().Format(time.RFC3339)
			st.LastInboundFrom = payload.From
			st.LastInboundText = truncateString(strings.TrimSpace(payload.Content), 120)
			st.LastEvent = "message_inbound"
		})
		s.markIncomingReadReceipt(v.Info.Chat.ToNonAD(), v.Info.Sender.ToNonAD(), v.Info.ID, v.Info.Timestamp)
	}
}

func (s *WhatsAppBridgeService) handleWS(w http.ResponseWriter, r *http.Request) {
	if !websocket.IsWebSocketUpgrade(r) {
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]string{
			"message": "whatsapp bridge websocket endpoint",
		})
		return
	}
	conn, err := s.wsUpgrader.Upgrade(w, r, nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	s.wsClientsMu.Lock()
	s.wsClients[conn] = struct{}{}
	s.wsClientsMu.Unlock()
	defer func() {
		s.wsClientsMu.Lock()
		delete(s.wsClients, conn)
		s.wsClientsMu.Unlock()
		_ = conn.Close()
	}()

	for {
		var msg whatsappBridgeWSMessage
		if err := conn.ReadJSON(&msg); err != nil {
			return
		}
		if strings.TrimSpace(msg.Type) != "message" {
			continue
		}
		if err := s.sendOutboundMessage(r.Context(), msg.To, msg.Content, msg.Media, msg.ReplyToID, msg.ReplyToSender); err != nil {
			_ = conn.WriteJSON(map[string]string{
				"type":  "error",
				"error": err.Error(),
			})
			continue
		}
	}
}

func (s *WhatsAppBridgeService) ServeWS(w http.ResponseWriter, r *http.Request) {
	s.wrapHandler(s.handleWS)(w, r)
}

func (s *WhatsAppBridgeService) wrapHandler(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if s.localOnly && !isLocalRequest(r) {
			http.Error(w, "forbidden", http.StatusForbidden)
			return
		}
		next(w, r)
	}
}

func (s *WhatsAppBridgeService) handleStatus(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(s.StatusSnapshot())
}

func (s *WhatsAppBridgeService) ServeStatus(w http.ResponseWriter, r *http.Request) {
	s.wrapHandler(s.handleStatus)(w, r)
}

func (s *WhatsAppBridgeService) handleLogout(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	if s.client == nil {
		http.Error(w, "whatsapp bridge client is not initialized", http.StatusServiceUnavailable)
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
	defer cancel()
	if err := s.client.Logout(ctx); err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}
	s.updateStatus(func(st *WhatsAppBridgeStatus) {
		st.State = "logged_out"
		st.Connected = false
		st.LoggedIn = false
		st.UserJID = ""
		st.QRCode = ""
		st.QRAvailable = false
		st.LastEvent = "logout"
	})
	go func() {
		_ = s.connectClient(context.Background())
	}()
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(s.StatusSnapshot())
}

func (s *WhatsAppBridgeService) sendTextMessage(ctx context.Context, rawTo, content, replyToID, replyToSender string) error {
	if s.client == nil {
		return fmt.Errorf("whatsapp client not initialized")
	}
	if strings.TrimSpace(content) == "" {
		return fmt.Errorf("message content is required")
	}
	to, err := normalizeWhatsAppRecipientJID(rawTo)
	if err != nil {
		return err
	}
	text := strings.TrimSpace(content)
	msg := &waProto.Message{
		Conversation: &text,
	}
	applyWhatsAppReplyContext(msg, to, strings.TrimSpace(replyToID), strings.TrimSpace(replyToSender))
	_, err = s.client.SendMessage(ctx, to, msg)
	if err == nil {
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.OutboundCount++
			st.LastOutboundAt = time.Now().Format(time.RFC3339)
			st.LastOutboundTo = to.String()
			st.LastOutboundText = truncateString(text, 120)
			st.LastEvent = "message_outbound"
		})
	}
	return err
}

func (s *WhatsAppBridgeService) sendOutboundMessage(ctx context.Context, rawTo, content string, mediaPaths []string, replyToID, replyToSender string) error {
	if len(mediaPaths) == 0 {
		return s.sendTextMessage(ctx, rawTo, content, replyToID, replyToSender)
	}
	to, err := normalizeWhatsAppRecipientJID(rawTo)
	if err != nil {
		return err
	}
	caption := strings.TrimSpace(content)
	for idx, mediaPath := range mediaPaths {
		msg, err := s.buildMediaMessage(ctx, to, strings.TrimSpace(mediaPath), caption, replyToID, replyToSender)
		if err != nil {
			return err
		}
		if _, err := s.client.SendMessage(ctx, to, msg); err != nil {
			return err
		}
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.OutboundCount++
			st.LastOutboundAt = time.Now().Format(time.RFC3339)
			st.LastOutboundTo = to.String()
			st.LastOutboundText = truncateString(strings.TrimSpace(content), 120)
			st.LastEvent = "message_outbound"
		})
		if idx == 0 {
			caption = ""
		}
	}
	return nil
}

func (s *WhatsAppBridgeService) buildMediaMessage(ctx context.Context, to types.JID, mediaPath, caption, replyToID, replyToSender string) (*waProto.Message, error) {
	if s.client == nil {
		return nil, fmt.Errorf("whatsapp client not initialized")
	}
	mediaPath = strings.TrimSpace(mediaPath)
	if mediaPath == "" {
		return nil, fmt.Errorf("media path is required")
	}
	data, err := os.ReadFile(mediaPath)
	if err != nil {
		return nil, fmt.Errorf("read media file: %w", err)
	}
	kind, mimeType := detectWhatsAppMediaType(mediaPath, data)
	uploadType := whatsmeow.MediaDocument
	switch kind {
	case "image":
		uploadType = whatsmeow.MediaImage
	case "video":
		uploadType = whatsmeow.MediaVideo
	case "audio":
		uploadType = whatsmeow.MediaAudio
	}
	resp, err := s.client.Upload(ctx, data, uploadType)
	if err != nil {
		return nil, fmt.Errorf("upload media: %w", err)
	}
	fileLength := resp.FileLength
	fileName := filepath.Base(mediaPath)
	switch kind {
	case "image":
		msg := &waProto.Message{
			ImageMessage: &waProto.ImageMessage{
				Caption:       proto.String(strings.TrimSpace(caption)),
				Mimetype:      proto.String(mimeType),
				URL:           proto.String(resp.URL),
				DirectPath:    proto.String(resp.DirectPath),
				MediaKey:      resp.MediaKey,
				FileEncSHA256: resp.FileEncSHA256,
				FileSHA256:    resp.FileSHA256,
				FileLength:    proto.Uint64(fileLength),
			},
		}
		applyWhatsAppReplyContext(msg, to, strings.TrimSpace(replyToID), strings.TrimSpace(replyToSender))
		return msg, nil
	case "video":
		msg := &waProto.Message{
			VideoMessage: &waProto.VideoMessage{
				Caption:       proto.String(strings.TrimSpace(caption)),
				Mimetype:      proto.String(mimeType),
				URL:           proto.String(resp.URL),
				DirectPath:    proto.String(resp.DirectPath),
				MediaKey:      resp.MediaKey,
				FileEncSHA256: resp.FileEncSHA256,
				FileSHA256:    resp.FileSHA256,
				FileLength:    proto.Uint64(fileLength),
			},
		}
		applyWhatsAppReplyContext(msg, to, strings.TrimSpace(replyToID), strings.TrimSpace(replyToSender))
		return msg, nil
	case "audio":
		msg := &waProto.Message{
			AudioMessage: &waProto.AudioMessage{
				Mimetype:      proto.String(mimeType),
				URL:           proto.String(resp.URL),
				DirectPath:    proto.String(resp.DirectPath),
				MediaKey:      resp.MediaKey,
				FileEncSHA256: resp.FileEncSHA256,
				FileSHA256:    resp.FileSHA256,
				FileLength:    proto.Uint64(fileLength),
			},
		}
		applyWhatsAppReplyContext(msg, to, strings.TrimSpace(replyToID), strings.TrimSpace(replyToSender))
		return msg, nil
	default:
		msg := &waProto.Message{
			DocumentMessage: &waProto.DocumentMessage{
				Caption:       proto.String(strings.TrimSpace(caption)),
				Mimetype:      proto.String(mimeType),
				Title:         proto.String(fileName),
				FileName:      proto.String(fileName),
				URL:           proto.String(resp.URL),
				DirectPath:    proto.String(resp.DirectPath),
				MediaKey:      resp.MediaKey,
				FileEncSHA256: resp.FileEncSHA256,
				FileSHA256:    resp.FileSHA256,
				FileLength:    proto.Uint64(fileLength),
			},
		}
		applyWhatsAppReplyContext(msg, to, strings.TrimSpace(replyToID), strings.TrimSpace(replyToSender))
		return msg, nil
	}
}

func detectWhatsAppMediaType(path string, data []byte) (kind string, mimeType string) {
	ext := strings.ToLower(filepath.Ext(path))
	mimeType = mime.TypeByExtension(ext)
	if mimeType == "" && len(data) > 0 {
		mimeType = http.DetectContentType(data)
	}
	if mimeType == "" {
		mimeType = "application/octet-stream"
	}
	switch {
	case strings.HasPrefix(mimeType, "image/"):
		return "image", mimeType
	case strings.HasPrefix(mimeType, "video/"):
		return "video", mimeType
	case strings.HasPrefix(mimeType, "audio/"):
		return "audio", mimeType
	default:
		return "document", mimeType
	}
}

func (s *WhatsAppBridgeService) matchCurrentUserContext(msg *waProto.Message) (mentionedSelf bool, replyToMe bool) {
	if s.client == nil || s.client.Store.ID == nil || msg == nil {
		return false, false
	}
	ctx := extractWhatsAppContextInfo(msg)
	if ctx == nil {
		return false, false
	}
	own := s.client.Store.ID.ToNonAD().String()
	for _, mentioned := range ctx.GetMentionedJID() {
		if normalizeComparableJID(mentioned) == own {
			mentionedSelf = true
			break
		}
	}
	replyParticipant := normalizeComparableJID(ctx.GetParticipant())
	if replyParticipant != "" && replyParticipant == own {
		replyToMe = true
	}
	return mentionedSelf, replyToMe
}

func extractWhatsAppContextInfo(msg *waProto.Message) *waProto.ContextInfo {
	switch {
	case msg == nil:
		return nil
	case msg.GetExtendedTextMessage() != nil:
		return msg.GetExtendedTextMessage().GetContextInfo()
	case msg.GetImageMessage() != nil:
		return msg.GetImageMessage().GetContextInfo()
	case msg.GetVideoMessage() != nil:
		return msg.GetVideoMessage().GetContextInfo()
	case msg.GetAudioMessage() != nil:
		return msg.GetAudioMessage().GetContextInfo()
	case msg.GetDocumentMessage() != nil:
		return msg.GetDocumentMessage().GetContextInfo()
	case msg.GetDocumentWithCaptionMessage() != nil && msg.GetDocumentWithCaptionMessage().GetMessage() != nil:
		return extractWhatsAppContextInfo(msg.GetDocumentWithCaptionMessage().GetMessage())
	default:
		return nil
	}
}

func normalizeComparableJID(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	jid, err := types.ParseJID(raw)
	if err == nil {
		return jid.ToNonAD().String()
	}
	return raw
}

func applyWhatsAppReplyContext(msg *waProto.Message, chatJID types.JID, replyToID, replyToSender string) {
	if msg == nil || strings.TrimSpace(replyToID) == "" {
		return
	}
	ctx := &waProto.ContextInfo{
		StanzaID: proto.String(strings.TrimSpace(replyToID)),
	}
	if chatJID.Server == types.GroupServer {
		ctx.RemoteJID = proto.String(chatJID.ToNonAD().String())
		if sender := normalizeComparableJID(replyToSender); sender != "" {
			ctx.Participant = proto.String(sender)
		}
	}
	switch {
	case msg.GetExtendedTextMessage() != nil:
		msg.GetExtendedTextMessage().ContextInfo = ctx
	case msg.GetImageMessage() != nil:
		msg.GetImageMessage().ContextInfo = ctx
	case msg.GetVideoMessage() != nil:
		msg.GetVideoMessage().ContextInfo = ctx
	case msg.GetAudioMessage() != nil:
		msg.GetAudioMessage().ContextInfo = ctx
	case msg.GetDocumentMessage() != nil:
		msg.GetDocumentMessage().ContextInfo = ctx
	default:
		msg.ExtendedTextMessage = &waProto.ExtendedTextMessage{
			Text:        proto.String(msg.GetConversation()),
			ContextInfo: ctx,
		}
		msg.Conversation = nil
	}
}

func (s *WhatsAppBridgeService) markIncomingReadReceipt(chat, sender types.JID, id types.MessageID, timestamp time.Time) {
	if s == nil || s.markReadFn == nil || id == "" || chat.IsEmpty() {
		return
	}
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		effectiveSender := types.EmptyJID
		if chat.Server == types.GroupServer {
			effectiveSender = sender
		}
		if err := s.markReadFn(ctx, []types.MessageID{id}, timestamp, chat, effectiveSender); err != nil {
			s.updateStatus(func(st *WhatsAppBridgeStatus) {
				st.LastError = "mark_read_failed: " + err.Error()
				st.LastEvent = "mark_read_failed"
			})
			return
		}
		s.updateStatus(func(st *WhatsAppBridgeStatus) {
			st.ReadReceiptCount++
			st.LastReadAt = time.Now().Format(time.RFC3339)
			st.LastEvent = "mark_read"
		})
	}()
}

func (s *WhatsAppBridgeService) updateStatus(mut func(*WhatsAppBridgeStatus)) {
	s.statusMu.Lock()
	defer s.statusMu.Unlock()
	mut(&s.status)
	s.status.UpdatedAt = time.Now().Format(time.RFC3339)
}

func (s *WhatsAppBridgeService) broadcastWS(payload whatsappBridgeWSMessage) {
	s.wsClientsMu.Lock()
	defer s.wsClientsMu.Unlock()
	for conn := range s.wsClients {
		_ = conn.WriteJSON(payload)
	}
}

func (s *WhatsAppBridgeService) broadcastWSMap(payload map[string]interface{}) {
	s.wsClientsMu.Lock()
	defer s.wsClientsMu.Unlock()
	for conn := range s.wsClients {
		_ = conn.WriteJSON(payload)
	}
}

func (s *WhatsAppBridgeService) closeWSClients() {
	s.wsClientsMu.Lock()
	defer s.wsClientsMu.Unlock()
	for conn := range s.wsClients {
		_ = conn.Close()
		delete(s.wsClients, conn)
	}
}

func (s *WhatsAppBridgeService) ServeLogout(w http.ResponseWriter, r *http.Request) {
	s.wrapHandler(s.handleLogout)(w, r)
}

func normalizeWhatsAppRecipientJID(raw string) (types.JID, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return types.EmptyJID, fmt.Errorf("recipient is required")
	}
	if strings.Contains(raw, "@") {
		jid, err := types.ParseJID(raw)
		if err != nil {
			return types.EmptyJID, fmt.Errorf("parse recipient jid: %w", err)
		}
		return jid.ToNonAD(), nil
	}
	if strings.Contains(raw, "-") {
		return types.NewJID(raw, types.GroupServer), nil
	}
	return types.NewJID(raw, types.DefaultUserServer), nil
}

func extractWhatsAppMessageText(msg *waProto.Message) string {
	if msg == nil {
		return ""
	}
	switch {
	case strings.TrimSpace(msg.GetConversation()) != "":
		return msg.GetConversation()
	case msg.GetExtendedTextMessage() != nil && strings.TrimSpace(msg.GetExtendedTextMessage().GetText()) != "":
		return msg.GetExtendedTextMessage().GetText()
	case msg.GetImageMessage() != nil && strings.TrimSpace(msg.GetImageMessage().GetCaption()) != "":
		return msg.GetImageMessage().GetCaption()
	case msg.GetVideoMessage() != nil && strings.TrimSpace(msg.GetVideoMessage().GetCaption()) != "":
		return msg.GetVideoMessage().GetCaption()
	case msg.GetDocumentMessage() != nil && strings.TrimSpace(msg.GetDocumentMessage().GetCaption()) != "":
		return msg.GetDocumentMessage().GetCaption()
	case msg.GetAudioMessage() != nil:
		return "[audio]"
	case msg.GetStickerMessage() != nil:
		return "[sticker]"
	case msg.GetImageMessage() != nil:
		return "[image]"
	case msg.GetVideoMessage() != nil:
		return "[video]"
	case msg.GetDocumentMessage() != nil:
		return "[document]"
	default:
		return ""
	}
}
