package testenv

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"
)

// PublicRESTClient exposes the public REST surface of the gateway
// (`/api/v1/public/*`). Tests use it for unauthenticated registration
// flows.
type PublicRESTClient struct {
	BaseURL string
	HTTP    *http.Client
}

// NewPublicRESTClient constructs a client targeting baseURL.
func NewPublicRESTClient(baseURL string) *PublicRESTClient {
	return &PublicRESTClient{
		BaseURL: strings.TrimRight(baseURL, "/"),
		HTTP:    &http.Client{Timeout: 30 * time.Second},
	}
}

// SendEmailCodeResponse mirrors the wire shape of
// `POST /api/v1/public/auth/send-email-code`.
type SendEmailCodeResponse struct {
	ChallengeID string `json:"challenge_id"`
}

// ConfirmEmailCodeResponse mirrors the wire shape of
// `POST /api/v1/public/auth/confirm-email-code`.
type ConfirmEmailCodeResponse struct {
	DeviceSessionID string `json:"device_session_id"`
}

// SendEmailCode triggers an email-code challenge. The `locale` value
// is sent through the public REST contract as the `Accept-Language`
// header (gateway derives `preferred_language` from it; the body
// schema rejects unknown fields).
func (c *PublicRESTClient) SendEmailCode(ctx context.Context, email string, locale string) (*SendEmailCodeResponse, *http.Response, error) {
	body := map[string]any{"email": email}
	headers := http.Header{}
	if locale != "" {
		headers.Set("Accept-Language", locale)
	}
	resp, raw, err := c.doWithHeaders(ctx, http.MethodPost, "/api/v1/public/auth/send-email-code", body, headers)
	if err != nil {
		return nil, raw, err
	}
	if raw.StatusCode/100 != 2 {
		return nil, raw, fmt.Errorf("send-email-code: status %d: %s", raw.StatusCode, string(resp))
	}
	var out SendEmailCodeResponse
	if err := json.Unmarshal(resp, &out); err != nil {
		return nil, raw, err
	}
	return &out, raw, nil
}

// ConfirmEmailCode confirms a challenge and registers a device
// session.
func (c *PublicRESTClient) ConfirmEmailCode(ctx context.Context, challengeID, code, clientPublicKey, timeZone string) (*ConfirmEmailCodeResponse, *http.Response, error) {
	body := map[string]any{
		"challenge_id":      challengeID,
		"code":              code,
		"client_public_key": clientPublicKey,
		"time_zone":         timeZone,
	}
	resp, raw, err := c.do(ctx, http.MethodPost, "/api/v1/public/auth/confirm-email-code", body)
	if err != nil {
		return nil, raw, err
	}
	if raw.StatusCode/100 != 2 {
		return nil, raw, fmt.Errorf("confirm-email-code: status %d: %s", raw.StatusCode, string(resp))
	}
	var out ConfirmEmailCodeResponse
	if err := json.Unmarshal(resp, &out); err != nil {
		return nil, raw, err
	}
	return &out, raw, nil
}

func (c *PublicRESTClient) do(ctx context.Context, method, path string, body any) ([]byte, *http.Response, error) {
	return c.doWithHeaders(ctx, method, path, body, nil)
}

func (c *PublicRESTClient) doWithHeaders(ctx context.Context, method, path string, body any, headers http.Header) ([]byte, *http.Response, error) {
	var reader io.Reader
	if body != nil {
		buf, err := json.Marshal(body)
		if err != nil {
			return nil, nil, err
		}
		reader = bytes.NewReader(buf)
	}
	req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reader)
	if err != nil {
		return nil, nil, err
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	for k, vs := range headers {
		for _, v := range vs {
			req.Header.Add(k, v)
		}
	}
	resp, err := c.HTTP.Do(req)
	if err != nil {
		return nil, nil, err
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, resp, err
	}
	return raw, resp, nil
}

// BackendInternalClient hits backend's `/api/v1/internal/*` endpoints
// directly. Per ARCHITECTURE.md the trust boundary is the network, so
// integration tests act as a trusted gateway-equivalent caller.
type BackendInternalClient struct {
	BaseURL string
	HTTP    *http.Client
}

// NewBackendInternalClient targets backend's HTTP base URL.
func NewBackendInternalClient(baseURL string) *BackendInternalClient {
	return &BackendInternalClient{
		BaseURL: strings.TrimRight(baseURL, "/"),
		HTTP:    &http.Client{Timeout: 30 * time.Second},
	}
}

// Do issues an internal request. The caller decodes the body.
func (c *BackendInternalClient) Do(ctx context.Context, method, path string, body any) ([]byte, *http.Response, error) {
	var reader io.Reader
	if body != nil {
		buf, err := json.Marshal(body)
		if err != nil {
			return nil, nil, err
		}
		reader = bytes.NewReader(buf)
	}
	req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reader)
	if err != nil {
		return nil, nil, err
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	resp, err := c.HTTP.Do(req)
	if err != nil {
		return nil, nil, err
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, resp, err
	}
	return raw, resp, nil
}

// BackendUserClient hits backend's `/api/v1/user/*` endpoints
// directly with `X-User-ID` set, mirroring what gateway does after
// authenticated traffic verification. Used by scenarios whose
// message_type is not registered in gateway's downstream router
// (lobby create, soft delete, etc.).
type BackendUserClient struct {
	BaseURL string
	UserID  string
	HTTP    *http.Client
}

// NewBackendUserClient targets backend's HTTP base URL with userID
// pre-bound.
func NewBackendUserClient(baseURL, userID string) *BackendUserClient {
	return &BackendUserClient{
		BaseURL: strings.TrimRight(baseURL, "/"),
		UserID:  userID,
		HTTP:    &http.Client{Timeout: 30 * time.Second},
	}
}

// Do issues a user-scoped backend request.
func (c *BackendUserClient) Do(ctx context.Context, method, path string, body any) ([]byte, *http.Response, error) {
	var reader io.Reader
	if body != nil {
		buf, err := json.Marshal(body)
		if err != nil {
			return nil, nil, err
		}
		reader = bytes.NewReader(buf)
	}
	req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reader)
	if err != nil {
		return nil, nil, err
	}
	req.Header.Set("X-User-ID", c.UserID)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	resp, err := c.HTTP.Do(req)
	if err != nil {
		return nil, nil, err
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, resp, err
	}
	return raw, resp, nil
}

// BackendAdminClient hits backend's admin surface directly with HTTP
// Basic Auth. Per ARCHITECTURE.md §14 the admin surface is on the
// backend HTTP listener (not gateway), so tests address it directly.
type BackendAdminClient struct {
	BaseURL  string
	Username string
	Password string
	HTTP     *http.Client
}

// NewBackendAdminClient targets backend's HTTP base URL with the
// supplied credentials.
func NewBackendAdminClient(baseURL, username, password string) *BackendAdminClient {
	return &BackendAdminClient{
		BaseURL:  strings.TrimRight(baseURL, "/"),
		Username: username,
		Password: password,
		HTTP:     &http.Client{Timeout: 30 * time.Second},
	}
}

// Do performs a request against an admin endpoint. The caller decodes
// the body. Returned http.Response is always non-nil on success.
func (c *BackendAdminClient) Do(ctx context.Context, method, path string, body any) ([]byte, *http.Response, error) {
	var reader io.Reader
	if body != nil {
		buf, err := json.Marshal(body)
		if err != nil {
			return nil, nil, err
		}
		reader = bytes.NewReader(buf)
	}
	req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reader)
	if err != nil {
		return nil, nil, err
	}
	req.SetBasicAuth(c.Username, c.Password)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	resp, err := c.HTTP.Do(req)
	if err != nil {
		return nil, nil, err
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, resp, err
	}
	return raw, resp, nil
}
