package main

import "strings"

// serverOf returns the homeserver part of an mxid (`@ai:vojo.chat` → `vojo.chat`).
func serverOf(mxid string) string {
	if i := strings.IndexByte(mxid, ':'); i >= 0 {
		return mxid[i+1:]
	}
	return ""
}

// localpartOf returns the localpart of an mxid (`@ai:vojo.chat` → `ai`).
func localpartOf(mxid string) string {
	s := strings.TrimPrefix(mxid, "@")
	if i := strings.IndexByte(s, ':'); i >= 0 {
		return s[:i]
	}
	return s
}

// mentionsBot decides whether a message intentionally addresses the bot.
//
// Canonical path (MSC3952): the sender's client lists mentioned mxids in
// content["m.mentions"].user_ids. cinny ALWAYS writes this (RoomInput.tsx:491-492),
// and the presence of m.mentions suppresses legacy body-keyword push rules — so a
// plain-text "@ai" with no pill is intentionally NOT a trigger (F30).
//
// Fallbacks for non-cinny senders (Element/FluffyChat/bridges) that still pill:
//   - a matrix.to / matrix: pill href targeting the bot mxid in formatted_body;
//   - a reply whose parent we sent (resolved by the caller via replyParentIsBot).
//
// We deliberately do NOT scan body for the bot's localpart — that would re-create
// the unintentional-mention problem MSC3952 removed.
func mentionsBot(mc *MessageContent, botMXID string, replyParentIsBot bool) bool {
	if mc.Mentions != nil {
		for _, uid := range mc.Mentions.UserIDs { // UserIDs may be nil — range is safe (F29)
			if uid == botMXID {
				return true
			}
		}
	}
	if replyParentIsBot {
		return true
	}
	return pillTargetsBot(mc.FormattedBody, botMXID)
}

// stripBotMention removes the bot's own mention text from a trigger body before it is
// used as a web-search query, a prompt turn, a buffer entry, or telemetry. cinny writes
// the plain-text fallback of a mention pill as the bot's FULL mxid ("@ai:vojo.chat …"),
// and that literal mxid, sent verbatim to the grounding provider as the search query, made
// it treat "vojo.chat" as the SUBJECT entity — it searched "was the Vojo.chat messenger
// removed?", found nothing, and confabulated "no, it's available", the exact first-ask
// hallucination + same-question/different-answer the "Max" thread showed (the second ask
// happened to anchor on "макс" instead, hence two opposite grounded answers). Mention
// DETECTION already ran upstream via m.mentions (MSC3952), so dropping the body text never
// changes routing. We strip only the UNAMBIGUOUS mxid forms — the full mxid and a
// standalone "@localpart"; the human display name is deliberately left intact so a real
// question that names the product ("что умеет Vojo AI") is never mangled.
func stripBotMention(body, botMXID string) string {
	body = strings.ReplaceAll(body, botMXID, " ")
	at := "@" + localpartOf(botMXID)
	fields := strings.Fields(body)
	kept := fields[:0]
	for _, f := range fields {
		// Drop a standalone "@ai" pill fallback (with trailing address punctuation), but
		// keep "@aibot" or any word that merely contains it.
		if strings.EqualFold(strings.Trim(f, ",.:;!?–—-"), at) {
			continue
		}
		kept = append(kept, f)
	}
	out := strings.Join(kept, " ")
	return strings.TrimLeft(out, " ,:–—-") // leftover leading address punctuation ("@ai, …")
}

// pillTargetsBot looks for an <a href> mention pill addressing the bot in the
// HTML body. Matrix pills use either matrix.to/#/<mxid> or a matrix: URI.
func pillTargetsBot(formattedBody, botMXID string) bool {
	if formattedBody == "" {
		return false
	}
	// matrix.to URLs URL-encode the leading '@' as %40; cover both forms.
	needles := []string{
		"matrix.to/#/" + botMXID,
		"matrix.to/#/%40" + strings.TrimPrefix(botMXID, "@"),
		"matrix:u/" + strings.TrimPrefix(botMXID, "@"),
	}
	for _, n := range needles {
		if strings.Contains(formattedBody, n) {
			return true
		}
	}
	return false
}
