// Message triage: the pure decision of whether — and how — to answer one
// MESSAGE_CREATE. Ported from the Python bot's _response_for_message /
// _mentions_bot / _is_dm / _strip_mentions; it is plain string/JSON logic
// with no data dependency, so it lives in Go. Anything needing climate data
// (the actual grade) goes back to Python via Grader.

package gateway

import (
	"encoding/json"
	"fmt"
	"strings"
)

// helpText answers an empty query. Go owns this constant because it needs no
// data; the wording matches the Python original exactly.
const helpText = "Mention me with a city name — e.g. `@Thermograph Phoenix` — and I'll grade " +
	"today's weather against ~45 years of local history."

// snowflake decodes a Discord id that may arrive as a JSON string (what v10
// sends) or a bare number. The Python compared ids through str() coercion;
// this keeps that tolerance instead of betting the payload shape never
// wobbles.
type snowflake string

func (s *snowflake) UnmarshalJSON(b []byte) error {
	if string(b) == "null" {
		*s = ""
		return nil
	}
	var str string
	if err := json.Unmarshal(b, &str); err == nil {
		*s = snowflake(str)
		return nil
	}
	var n json.Number
	if err := json.Unmarshal(b, &n); err == nil {
		*s = snowflake(n.String())
		return nil
	}
	return fmt.Errorf("snowflake: cannot decode %s", b)
}

// gwMessage is the slice of a MESSAGE_CREATE payload the triage needs.
type gwMessage struct {
	ID        snowflake  `json:"id"`
	ChannelID snowflake  `json:"channel_id"`
	GuildID   snowflake  `json:"guild_id"`
	Content   string     `json:"content"`
	Author    gwAuthor   `json:"author"`
	Mentions  []gwAuthor `json:"mentions"`
}

type gwAuthor struct {
	ID  snowflake `json:"id"`
	Bot bool      `json:"bot"`
}

// action is triage's verdict for one message.
type action int

const (
	actSilent action = iota // no reply
	actHelp                 // reply with helpText
	actGrade                // call back into Python with the query
)

// mentionsBot reports whether the message @mentions our own user id.
func mentionsBot(m *gwMessage, botID string) bool {
	for _, u := range m.Mentions {
		if string(u.ID) == botID {
			return true
		}
	}
	return false
}

// isDM: a DM has no guild_id; a guild message always carries one.
func isDM(m *gwMessage) bool {
	return m.GuildID == ""
}

// stripMentions drops every form of the bot's own mention (<@id> and the
// legacy <@!id>) and collapses the surrounding whitespace, leaving just the
// user's query text.
func stripMentions(content, botID string) string {
	out := strings.ReplaceAll(content, "<@"+botID+">", " ")
	out = strings.ReplaceAll(out, "<@!"+botID+">", " ")
	return strings.Join(strings.Fields(out), " ")
}

// triage decides the reply for one MESSAGE_CREATE: silence, the help line, or
// a grade lookup for the returned query.
//
// Silent unless the message DMs the bot or @mentions it, and never for a bot
// author — which includes ourselves, the guard against a mention-loop (our
// reply quoting the mention must not trigger another reply). The text after
// the mention is treated as a city query; DMs are the query as-is (trimmed
// but not collapsed, matching the Python's .strip()).
func triage(m *gwMessage, botID string) (action, string) {
	if m.Author.Bot || string(m.Author.ID) == botID {
		return actSilent, ""
	}
	dm := isDM(m)
	if !dm && !mentionsBot(m, botID) {
		return actSilent, ""
	}
	var query string
	if dm {
		query = strings.TrimSpace(m.Content)
	} else {
		query = stripMentions(m.Content, botID)
	}
	if query == "" {
		return actHelp, ""
	}
	return actGrade, query
}
