// The REST half of answering a message: replies go out over plain HTTP with
// the bot token, not the gateway socket (the gateway is receive-only for us).

package gateway

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"strconv"
	"time"
)

const discordAPIBase = "https://discord.com/api/v10"

// replyBodyLimit caps how much of a failed reply's response body we echo into
// logs.
const replyBodyLimit = 4096

// injectReplyFields adds message_reference + allowed_mentions to an otherwise
// verbatim message body. Only the top level is decoded — every value that
// came from Python (embeds and all) passes through as raw bytes, unparsed, so
// the grading/embed contract stays entirely on the Python side.
//
// allowed_mentions is a SECURITY control, not decoration: the graded reply
// echoes the user's query text, so without {"parse":[]} a crafted query could
// turn our reply into an @everyone/role ping. Only the person who asked is
// pinged, via the reply reference.
func injectReplyFields(body []byte, messageID string) ([]byte, error) {
	var top map[string]json.RawMessage
	if err := json.Unmarshal(body, &top); err != nil {
		return nil, err
	}
	if messageID != "" {
		ref, err := json.Marshal(map[string]string{"message_id": messageID})
		if err != nil {
			return nil, err
		}
		top["message_reference"] = ref
		top["allowed_mentions"] = json.RawMessage(`{"parse":[],"replied_user":true}`)
	}
	return json.Marshal(top)
}

// reply posts body to the triggering message's channel as a proper reply. A
// failed reply is logged and swallowed — it must never kill the read loop.
func (b *Bot) reply(ctx context.Context, channelID, messageID string, body json.RawMessage) {
	if channelID == "" {
		return
	}
	payload, err := injectReplyFields(body, messageID)
	if err != nil {
		log.Printf("gateway: reply body is not a JSON object: %v", err)
		return
	}
	url := b.rest + "/channels/" + channelID + "/messages"
	for attempt := 0; ; attempt++ {
		req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
		if err != nil {
			log.Printf("gateway: reply failed: %v", err)
			return
		}
		req.Header.Set("Authorization", "Bot "+b.token)
		req.Header.Set("Content-Type", "application/json")
		resp, err := b.http.Do(req)
		if err != nil {
			log.Printf("gateway: reply failed: %v", err)
			return
		}
		raw, _ := io.ReadAll(io.LimitReader(resp.Body, replyBodyLimit))
		resp.Body.Close()
		if resp.StatusCode == http.StatusTooManyRequests && attempt == 0 {
			// One retry honouring the advertised wait — parity with the
			// Python REST helper's 429 handling.
			if !sleepCtx(ctx, retryAfter(resp.Header, raw)) {
				return
			}
			continue
		}
		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
			log.Printf("gateway: reply failed: status %d: %s", resp.StatusCode, raw)
		}
		return
	}
}

// retryAfter extracts Discord's requested wait from a 429 (JSON retry_after
// in seconds, falling back to the Retry-After header), clamped so a bogus
// server value can't park the handler for minutes.
func retryAfter(h http.Header, body []byte) time.Duration {
	seconds := 1.0
	var d struct {
		RetryAfter float64 `json:"retry_after"`
	}
	if err := json.Unmarshal(body, &d); err == nil && d.RetryAfter > 0 {
		seconds = d.RetryAfter
	} else if v, err := strconv.ParseFloat(h.Get("Retry-After"), 64); err == nil && v > 0 {
		seconds = v
	}
	// 5s matches the Python REST helper's _MAX_BACKOFF_S. It also bounds the cost
	// of a bogus/hostile retry_after: replies run on a small fixed worker pool, so
	// a parked handler holds one of very few slots and mentions start being
	// dropped that much sooner.
	if seconds > 5 {
		seconds = 5
	}
	return time.Duration(seconds * float64(time.Second))
}

// handleMessage triages one MESSAGE_CREATE and sends whatever reply it calls
// for. Runs in its own goroutine (see dispatch) so a slow grade lookup can
// never stall heartbeats or the read loop.
func (b *Bot) handleMessage(ctx context.Context, m *gwMessage, botID string) {
	act, query := triage(m, botID)
	switch act {
	case actSilent:
	case actHelp:
		body, err := json.Marshal(map[string]string{"content": helpText})
		if err != nil {
			return
		}
		b.reply(ctx, string(m.ChannelID), string(m.ID), body)
	case actGrade:
		// The callback owns all grading; its JSON is relayed VERBATIM (no
		// parsing, no reshaping) so the bot's grades can never drift from
		// the API's. A failed callback stays silent — better no reply than a
		// made-up one.
		raw, err := b.api.Grade(ctx, query)
		if err != nil {
			log.Printf("gateway: grade callback failed for %q: %v", query, err)
			return
		}
		b.reply(ctx, string(m.ChannelID), string(m.ID), raw)
	}
}
