package engineclient

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

	"galaxy/model/rest"
)

const (
	pathAdminInit       = "/api/v1/admin/init"
	pathAdminStatus     = "/api/v1/admin/status"
	pathAdminTurn       = "/api/v1/admin/turn"
	pathAdminRaceBanish = "/api/v1/admin/race/banish"
	pathPlayerCommand   = "/api/v1/command"
	pathPlayerOrder     = "/api/v1/order"
	pathPlayerReport    = "/api/v1/report"
	pathPlayerBattle    = "/api/v1/battle"
	pathHealthz         = "/healthz"
)

// Config configures one HTTP-backed engine client.
type Config struct {
	// CallTimeout bounds turn-generation-class operations: init, turn,
	// banish, command, order. Mirrors `BACKEND_ENGINE_CALL_TIMEOUT`.
	CallTimeout time.Duration

	// ProbeTimeout bounds inspect-style reads: status, report, healthz.
	// Mirrors `BACKEND_ENGINE_PROBE_TIMEOUT`.
	ProbeTimeout time.Duration
}

// Client is the engine HTTP client. The zero value is not usable — use
// NewClient.
type Client struct {
	callTimeout          time.Duration
	probeTimeout         time.Duration
	httpClient           *http.Client
	closeIdleConnections func()
}

// NewClient constructs a Client with an `otelhttp`-instrumented
// transport cloned from `http.DefaultTransport`. Close releases idle
// connections owned by the cloned transport.
func NewClient(cfg Config) (*Client, error) {
	transport, ok := http.DefaultTransport.(*http.Transport)
	if !ok {
		return nil, errors.New("engineclient: default transport is not *http.Transport")
	}
	cloned := transport.Clone()
	return newClient(cfg, &http.Client{Transport: otelhttp.NewTransport(cloned)}, cloned.CloseIdleConnections)
}

// NewClientWithHTTP constructs a Client around a caller-supplied
// `*http.Client`. Used in tests to inject `httptest`-backed transports.
func NewClientWithHTTP(cfg Config, hc *http.Client) (*Client, error) {
	return newClient(cfg, hc, nil)
}

func newClient(cfg Config, hc *http.Client, closeIdle func()) (*Client, error) {
	switch {
	case cfg.CallTimeout <= 0:
		return nil, errors.New("engineclient: call timeout must be positive")
	case cfg.ProbeTimeout <= 0:
		return nil, errors.New("engineclient: probe timeout must be positive")
	case hc == nil:
		return nil, errors.New("engineclient: http client must not be nil")
	}
	return &Client{
		callTimeout:          cfg.CallTimeout,
		probeTimeout:         cfg.ProbeTimeout,
		httpClient:           hc,
		closeIdleConnections: closeIdle,
	}, nil
}

// Close releases idle HTTP connections owned by the underlying
// transport. Safe to call multiple times.
func (c *Client) Close() error {
	if c == nil || c.closeIdleConnections == nil {
		return nil
	}
	c.closeIdleConnections()
	return nil
}

// Init calls `POST /api/v1/admin/init`.
func (c *Client) Init(ctx context.Context, baseURL string, request rest.InitRequest) (rest.StateResponse, error) {
	if err := validateBaseURL(baseURL); err != nil {
		return rest.StateResponse{}, err
	}
	body, err := json.Marshal(request)
	if err != nil {
		return rest.StateResponse{}, fmt.Errorf("engineclient init: encode request: %w", err)
	}
	payload, status, doErr := c.doRequest(ctx, http.MethodPost, baseURL+pathAdminInit, body, c.callTimeout)
	if doErr != nil {
		return rest.StateResponse{}, fmt.Errorf("%w: engine init: %w", ErrEngineUnreachable, doErr)
	}
	switch status {
	case http.StatusOK, http.StatusCreated:
		return decodeStateResponse(payload, "engine init")
	case http.StatusBadRequest:
		return rest.StateResponse{}, fmt.Errorf("%w: engine init: %s", ErrEngineValidation, summariseEngineError(payload, status))
	default:
		return rest.StateResponse{}, fmt.Errorf("%w: engine init: %s", ErrEngineUnreachable, summariseEngineError(payload, status))
	}
}

// Status calls `GET /api/v1/admin/status`.
func (c *Client) Status(ctx context.Context, baseURL string) (rest.StateResponse, error) {
	if err := validateBaseURL(baseURL); err != nil {
		return rest.StateResponse{}, err
	}
	payload, status, doErr := c.doRequest(ctx, http.MethodGet, baseURL+pathAdminStatus, nil, c.probeTimeout)
	if doErr != nil {
		return rest.StateResponse{}, fmt.Errorf("%w: engine status: %w", ErrEngineUnreachable, doErr)
	}
	switch status {
	case http.StatusOK:
		return decodeStateResponse(payload, "engine status")
	case http.StatusBadRequest:
		return rest.StateResponse{}, fmt.Errorf("%w: engine status: %s", ErrEngineValidation, summariseEngineError(payload, status))
	default:
		return rest.StateResponse{}, fmt.Errorf("%w: engine status: %s", ErrEngineUnreachable, summariseEngineError(payload, status))
	}
}

// Turn calls `PUT /api/v1/admin/turn`.
func (c *Client) Turn(ctx context.Context, baseURL string) (rest.StateResponse, error) {
	if err := validateBaseURL(baseURL); err != nil {
		return rest.StateResponse{}, err
	}
	payload, status, doErr := c.doRequest(ctx, http.MethodPut, baseURL+pathAdminTurn, nil, c.callTimeout)
	if doErr != nil {
		return rest.StateResponse{}, fmt.Errorf("%w: engine turn: %w", ErrEngineUnreachable, doErr)
	}
	switch status {
	case http.StatusOK:
		return decodeStateResponse(payload, "engine turn")
	case http.StatusBadRequest:
		return rest.StateResponse{}, fmt.Errorf("%w: engine turn: %s", ErrEngineValidation, summariseEngineError(payload, status))
	default:
		return rest.StateResponse{}, fmt.Errorf("%w: engine turn: %s", ErrEngineUnreachable, summariseEngineError(payload, status))
	}
}

// BanishRace calls `POST /api/v1/admin/race/banish` with body
// `{race_name}`. Engine returns 204 on success.
func (c *Client) BanishRace(ctx context.Context, baseURL, raceName string) error {
	if err := validateBaseURL(baseURL); err != nil {
		return err
	}
	if strings.TrimSpace(raceName) == "" {
		return errors.New("engineclient banish: race name must not be empty")
	}
	body, err := json.Marshal(rest.BanishRequest{RaceName: raceName})
	if err != nil {
		return fmt.Errorf("engineclient banish: encode: %w", err)
	}
	payload, status, doErr := c.doRequest(ctx, http.MethodPost, baseURL+pathAdminRaceBanish, body, c.callTimeout)
	if doErr != nil {
		return fmt.Errorf("%w: engine banish: %w", ErrEngineUnreachable, doErr)
	}
	switch status {
	case http.StatusNoContent, http.StatusOK:
		return nil
	case http.StatusBadRequest:
		return fmt.Errorf("%w: engine banish: %s", ErrEngineValidation, summariseEngineError(payload, status))
	default:
		return fmt.Errorf("%w: engine banish: %s", ErrEngineUnreachable, summariseEngineError(payload, status))
	}
}

// ExecuteCommands calls `PUT /api/v1/command` with payload forwarded
// verbatim. The engine response body is returned verbatim; on 4xx the
// body is returned alongside ErrEngineValidation so callers can
// forward the per-command error.
func (c *Client) ExecuteCommands(ctx context.Context, baseURL string, payload json.RawMessage) (json.RawMessage, error) {
	return c.forwardPlayerWrite(ctx, baseURL, pathPlayerCommand, payload, "engine command")
}

// PutOrders calls `PUT /api/v1/order` with the same forwarding
// semantics as ExecuteCommands.
func (c *Client) PutOrders(ctx context.Context, baseURL string, payload json.RawMessage) (json.RawMessage, error) {
	return c.forwardPlayerWrite(ctx, baseURL, pathPlayerOrder, payload, "engine order")
}

// GetOrder calls `GET /api/v1/order?player=<raceName>&turn=<turn>` and
// returns the engine response body verbatim. A `204 No Content` body
// is signalled by `(nil, http.StatusNoContent, nil)` so callers can
// surface "no stored order" without parsing the empty payload.
// Other non-`200` statuses come back wrapped in `ErrEngineValidation`
// (4xx) or `ErrEngineUnreachable` (everything else), matching the
// existing player-write conventions.
func (c *Client) GetOrder(ctx context.Context, baseURL, raceName string, turn int) (json.RawMessage, int, error) {
	if err := validateBaseURL(baseURL); err != nil {
		return nil, 0, err
	}
	if strings.TrimSpace(raceName) == "" {
		return nil, 0, errors.New("engineclient order get: race name must not be empty")
	}
	if turn < 0 {
		return nil, 0, fmt.Errorf("engineclient order get: turn must not be negative, got %d", turn)
	}
	values := url.Values{}
	values.Set("player", raceName)
	values.Set("turn", strconv.Itoa(turn))
	target := baseURL + pathPlayerOrder + "?" + values.Encode()
	body, status, doErr := c.doRequest(ctx, http.MethodGet, target, nil, c.probeTimeout)
	if doErr != nil {
		return nil, 0, fmt.Errorf("%w: engine order get: %w", ErrEngineUnreachable, doErr)
	}
	switch status {
	case http.StatusOK:
		if len(body) == 0 {
			return nil, status, fmt.Errorf("%w: engine order get: empty response body", ErrEngineProtocolViolation)
		}
		return json.RawMessage(body), status, nil
	case http.StatusNoContent:
		return nil, status, nil
	case http.StatusBadRequest, http.StatusConflict:
		return json.RawMessage(body), status, fmt.Errorf("%w: engine order get: %s", ErrEngineValidation, summariseEngineError(body, status))
	default:
		return nil, status, fmt.Errorf("%w: engine order get: %s", ErrEngineUnreachable, summariseEngineError(body, status))
	}
}

// GetReport calls `GET /api/v1/report?player=<raceName>&turn=<turn>`
// and returns the engine response body verbatim.
func (c *Client) GetReport(ctx context.Context, baseURL, raceName string, turn int) (json.RawMessage, error) {
	if err := validateBaseURL(baseURL); err != nil {
		return nil, err
	}
	if strings.TrimSpace(raceName) == "" {
		return nil, errors.New("engineclient report: race name must not be empty")
	}
	if turn < 0 {
		return nil, fmt.Errorf("engineclient report: turn must not be negative, got %d", turn)
	}
	values := url.Values{}
	values.Set("player", raceName)
	values.Set("turn", strconv.Itoa(turn))
	target := baseURL + pathPlayerReport + "?" + values.Encode()
	body, status, doErr := c.doRequest(ctx, http.MethodGet, target, nil, c.probeTimeout)
	if doErr != nil {
		return nil, fmt.Errorf("%w: engine report: %w", ErrEngineUnreachable, doErr)
	}
	switch status {
	case http.StatusOK:
		if len(body) == 0 {
			return nil, fmt.Errorf("%w: engine report: empty response body", ErrEngineProtocolViolation)
		}
		return json.RawMessage(body), nil
	case http.StatusBadRequest:
		return json.RawMessage(body), fmt.Errorf("%w: engine report: %s", ErrEngineValidation, summariseEngineError(body, status))
	default:
		return nil, fmt.Errorf("%w: engine report: %s", ErrEngineUnreachable, summariseEngineError(body, status))
	}
}

// FetchBattle calls `GET /api/v1/battle/<turn>/<battleID>` and returns
// the engine response body verbatim alongside the engine status code.
// 200 carries the BattleReport JSON; 404 means the battle is unknown
// and the body may be empty. Other 4xx statuses come back wrapped in
// ErrEngineValidation, everything else in ErrEngineUnreachable.
func (c *Client) FetchBattle(ctx context.Context, baseURL string, turn int, battleID string) (json.RawMessage, int, error) {
	if err := validateBaseURL(baseURL); err != nil {
		return nil, 0, err
	}
	if turn < 0 {
		return nil, 0, fmt.Errorf("engineclient battle get: turn must not be negative, got %d", turn)
	}
	if strings.TrimSpace(battleID) == "" {
		return nil, 0, errors.New("engineclient battle get: battle id must not be empty")
	}
	target := baseURL + pathPlayerBattle + "/" + strconv.Itoa(turn) + "/" + url.PathEscape(battleID)
	body, status, doErr := c.doRequest(ctx, http.MethodGet, target, nil, c.probeTimeout)
	if doErr != nil {
		return nil, 0, fmt.Errorf("%w: engine battle get: %w", ErrEngineUnreachable, doErr)
	}
	switch status {
	case http.StatusOK:
		if len(body) == 0 {
			return nil, status, fmt.Errorf("%w: engine battle get: empty response body", ErrEngineProtocolViolation)
		}
		return json.RawMessage(body), status, nil
	case http.StatusNotFound:
		return nil, status, nil
	case http.StatusBadRequest, http.StatusConflict:
		return json.RawMessage(body), status, fmt.Errorf("%w: engine battle get: %s", ErrEngineValidation, summariseEngineError(body, status))
	default:
		return nil, status, fmt.Errorf("%w: engine battle get: %s", ErrEngineUnreachable, summariseEngineError(body, status))
	}
}

// Healthz calls `GET /healthz`. Returns nil on 2xx.
func (c *Client) Healthz(ctx context.Context, baseURL string) error {
	if err := validateBaseURL(baseURL); err != nil {
		return err
	}
	body, status, doErr := c.doRequest(ctx, http.MethodGet, baseURL+pathHealthz, nil, c.probeTimeout)
	if doErr != nil {
		return fmt.Errorf("%w: engine healthz: %w", ErrEngineUnreachable, doErr)
	}
	if status/100 == 2 {
		return nil
	}
	return fmt.Errorf("%w: engine healthz: %s", ErrEngineUnreachable, summariseEngineError(body, status))
}

func (c *Client) forwardPlayerWrite(ctx context.Context, baseURL, requestPath string, payload json.RawMessage, opLabel string) (json.RawMessage, error) {
	if err := validateBaseURL(baseURL); err != nil {
		return nil, err
	}
	if len(bytes.TrimSpace(payload)) == 0 {
		return nil, fmt.Errorf("%s: payload must not be empty", opLabel)
	}
	body, status, doErr := c.doRequest(ctx, http.MethodPut, baseURL+requestPath, []byte(payload), c.callTimeout)
	if doErr != nil {
		return nil, fmt.Errorf("%w: %s: %w", ErrEngineUnreachable, opLabel, doErr)
	}
	switch status {
	case http.StatusOK, http.StatusAccepted:
		return json.RawMessage(body), nil
	case http.StatusBadRequest, http.StatusConflict:
		return json.RawMessage(body), fmt.Errorf("%w: %s: %s", ErrEngineValidation, opLabel, summariseEngineError(body, status))
	default:
		return nil, fmt.Errorf("%w: %s: %s", ErrEngineUnreachable, opLabel, summariseEngineError(body, status))
	}
}

func (c *Client) doRequest(ctx context.Context, method, target string, body []byte, timeout time.Duration) ([]byte, int, error) {
	reqCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	var reader io.Reader
	if body != nil {
		reader = bytes.NewReader(body)
	}
	req, err := http.NewRequestWithContext(reqCtx, method, target, reader)
	if err != nil {
		return nil, 0, fmt.Errorf("build request: %w", err)
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req.Header.Set("Accept", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, 0, err
	}
	defer func() { _ = resp.Body.Close() }()

	payload, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, resp.StatusCode, fmt.Errorf("read body: %w", err)
	}
	return payload, resp.StatusCode, nil
}

func validateBaseURL(baseURL string) error {
	if strings.TrimSpace(baseURL) == "" {
		return errors.New("engineclient: baseURL must not be empty")
	}
	if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
		return fmt.Errorf("engineclient: baseURL %q must start with http:// or https://", baseURL)
	}
	return nil
}

func decodeStateResponse(body []byte, op string) (rest.StateResponse, error) {
	if len(bytes.TrimSpace(body)) == 0 {
		return rest.StateResponse{}, fmt.Errorf("%w: %s: empty body", ErrEngineProtocolViolation, op)
	}
	var out rest.StateResponse
	if err := json.Unmarshal(body, &out); err != nil {
		return rest.StateResponse{}, fmt.Errorf("%w: %s: %v", ErrEngineProtocolViolation, op, err)
	}
	return out, nil
}

func summariseEngineError(body []byte, status int) string {
	if len(body) == 0 {
		return fmt.Sprintf("status=%d", status)
	}
	trimmed := strings.TrimSpace(string(body))
	if len(trimmed) > 256 {
		trimmed = trimmed[:256] + "…"
	}
	return fmt.Sprintf("status=%d body=%s", status, trimmed)
}
