// Package edge is the load harness's client of the gateway edge protocol: the
// Connect Execute envelope carrying FlatBuffers payloads, plus the Subscribe live
// stream, over h2c. It exposes typed wrappers for the operations the driver
// exercises, decoding responses into plain Go structs so the scenario layer never
// touches FlatBuffers directly.
package edge

import (
	"context"
	"crypto/tls"
	"net"
	"net/http"
	"time"

	"connectrpc.com/connect"
	"golang.org/x/net/http2"

	edgev1 "scrabble/gateway/proto/edge/v1"
	"scrabble/gateway/proto/edge/v1/edgev1connect"
)

// Message types the driver uses, mirroring gateway/internal/transcode's catalog.
const (
	msgSubmitPlay = "game.submit_play"
	msgPass       = "game.pass"
	msgExchange   = "game.exchange"
	msgEvaluate   = "game.evaluate"
	msgState      = "game.state"
	msgHistory    = "game.history"
	msgGamesList  = "games.list"
	msgCheckWord  = "game.check_word"
	msgNudge      = "chat.nudge"
	msgChatPost   = "chat.post"
	msgDraftSave  = "draft.save"
	msgDraftGet   = "draft.get"
	msgProfileGet = "profile.get"
	msgProfileUpd = "profile.update"
	msgStatsGet   = "stats.get"
	msgInvCreate  = "invitation.create"
	msgInvAccept  = "invitation.accept"
	msgInvList    = "invitation.list"
	msgEnqueue    = "lobby.enqueue"
)

// Client speaks the edge protocol to a single gateway base URL over h2c. The harness
// builds one Client per virtual player, so each player owns its h2c connection (its
// Subscribe stream and Execute calls share it) the way a real client does; a single
// Client is safe for that player's own concurrent goroutines.
type Client struct {
	rpc edgev1connect.GatewayClient
}

// New builds a Client for baseURL (for example http://gateway:8081). The transport
// speaks HTTP/2 cleartext (h2c) to match the gateway, dialling plaintext TCP rather
// than TLS. Each virtual player gets its own Client (hence its own connection), so the
// load mirrors real clients instead of multiplexing every player over one transport.
func New(baseURL string) *Client {
	hc := &http.Client{
		Transport: &http2.Transport{
			AllowHTTP: true,
			DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
				var d net.Dialer
				return d.DialContext(ctx, network, addr)
			},
		},
	}
	return &Client{rpc: edgev1connect.NewGatewayClient(hc, baseURL)}
}

// Result is the decoded Execute envelope: Code is "ok" or a stable domain error
// code (a non-ok Code is a domain outcome, not a transport failure); Payload is the
// FlatBuffers response body (empty on error).
type Result struct {
	Code    string
	Payload []byte
}

// execute runs one operation as token (empty for an unauthenticated op). A transport
// or connection error is returned as err; a domain rejection is reported in
// Result.Code with a nil err.
func (c *Client) execute(ctx context.Context, token, msgType string, payload []byte) (Result, error) {
	req := connect.NewRequest(&edgev1.ExecuteRequest{MessageType: msgType, Payload: payload})
	if token != "" {
		req.Header().Set("Authorization", "Bearer "+token)
	}
	resp, err := c.rpc.Execute(ctx, req)
	if err != nil {
		return Result{Code: connectCode(err)}, err
	}
	return Result{Code: resp.Msg.ResultCode, Payload: resp.Msg.Payload}, nil
}

// connectCode renders a transport error as a short code for the report (e.g.
// "rate_limited" for HTTP 429, "unavailable", "deadline"), so the gateway-hammer can
// tally limiter rejections without inspecting full errors.
func connectCode(err error) string {
	switch connect.CodeOf(err) {
	case connect.CodeResourceExhausted:
		return "rate_limited"
	case connect.CodeUnauthenticated:
		return "unauthenticated"
	case connect.CodeUnavailable:
		return "unavailable"
	case connect.CodeDeadlineExceeded:
		return "deadline"
	default:
		return "transport_error"
	}
}

// Event is one decoded live event: its kind and raw FlatBuffers payload (the driver
// reacts to kind alone — your_turn / match_found drive a state fetch).
type Event struct {
	Kind string
}

// Subscribe opens the live-event stream as token and invokes onEvent for each event
// until the context is cancelled or the stream ends. It blocks; run it in its own
// goroutine. Stream errors are returned for the caller to count and (optionally)
// reconnect.
func (c *Client) Subscribe(ctx context.Context, token string, onEvent func(Event)) error {
	req := connect.NewRequest(&edgev1.SubscribeRequest{})
	req.Header().Set("Authorization", "Bearer "+token)
	stream, err := c.rpc.Subscribe(ctx, req)
	if err != nil {
		return err
	}
	defer stream.Close()
	for stream.Receive() {
		if onEvent != nil {
			onEvent(Event{Kind: stream.Msg().Kind})
		}
	}
	return stream.Err()
}

// pollInterval bounds how often a player re-checks one game's state; exported for the
// scenario's pacing math so a virtual player stays under the per-user rate limit.
const DefaultPollInterval = 3 * time.Second
