package main

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"time"
)

// forwardClient is deliberately short-tempered. A bet or cash-out is only
// useful while the round is still open, so a request that cannot be delivered
// promptly should fail loudly rather than land after the moment has passed.
var forwardClient = &http.Client{Timeout: 3 * time.Second}

// forwardToLeader proxies a mutation to whichever instance drives the game.
//
// Only one instance holds a game's authoritative round state — who is in, at
// what stake, and at which tick each cash-out landed. Serving a bet from a
// follower's idle copy would either fail or, worse, create a second version of
// the round. So the follower relays the request and returns the leader's
// answer verbatim.
//
// The caller's Authorization header travels with it. That works because
// sessions live in Redis, so the leader can validate a token issued by any
// instance in the fleet.
//
// It reports whether the request was handled here.
func (s *server) forwardToLeader(w http.ResponseWriter, r *http.Request, game string, body []byte) bool {
	hub, ok := s.hubs[game]
	if !ok {
		writeErr(w, http.StatusNotFound, "no such game")
		return true
	}
	if hub.Leading() {
		return false // this instance owns the round; handle it locally
	}

	leader, err := s.node.LeaderOf(r.Context(), game)
	if err != nil {
		writeErr(w, http.StatusServiceUnavailable, "cannot locate the game leader")
		return true
	}
	if leader.Address == "" {
		// Between leaders: a lease has expired and the next campaign has not
		// landed yet. This resolves within a couple of seconds on its own.
		writeErr(w, http.StatusServiceUnavailable,
			"this game is changing hands; try again in a moment")
		return true
	}

	url := fmt.Sprintf("http://%s%s", leader.Address, r.URL.Path)
	req, err := http.NewRequestWithContext(r.Context(), r.Method, url, bytes.NewReader(body))
	if err != nil {
		writeErr(w, http.StatusInternalServerError, err.Error())
		return true
	}
	req.Header.Set("Content-Type", "application/json")
	if auth := r.Header.Get("Authorization"); auth != "" {
		req.Header.Set("Authorization", auth)
	}
	// Mark the hop so a routing mistake shows up as an explicit loop error
	// rather than as instances bouncing a request between themselves.
	if r.Header.Get("X-Arcade-Forwarded") != "" {
		writeErr(w, http.StatusLoopDetected,
			"request was forwarded twice; the cluster disagrees about the leader")
		return true
	}
	req.Header.Set("X-Arcade-Forwarded", s.node.ID)

	res, err := forwardClient.Do(req)
	if err != nil {
		writeErr(w, http.StatusServiceUnavailable,
			"the instance running this game did not respond")
		return true
	}
	defer res.Body.Close()

	payload, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
	if err != nil {
		writeErr(w, http.StatusBadGateway, "truncated response from the game leader")
		return true
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(res.StatusCode)
	_, _ = w.Write(payload)
	return true
}

// readBody buffers a request body so it can be both parsed locally and
// forwarded if this instance turns out not to own the game.
func readBody(r *http.Request) ([]byte, error) {
	defer r.Body.Close()
	return io.ReadAll(io.LimitReader(r.Body, 1<<20))
}
