// A thin caller example. Actual item grants and gameplay storage belong to the
// game backend and are intentionally not simulated as successful grants here.
package main

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

type Client struct {
	URL, Token, GameID string
	HTTP               *http.Client
}

// Both deployment modes use this caller; game clients never receive the token.
func ClientFromEnvironment() (Client, error) {
	mode, address, id, token := os.Getenv("HUOHUA_SERVICE_MODE"), os.Getenv("HUOHUA_SERVICE_URL"), os.Getenv("HUOHUA_GAME_ID"), os.Getenv("HUOHUA_GAME_TOKEN")
	u, e := url.Parse(address)
	if e != nil || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || strings.Trim(u.Path, "/") != "" || !regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,63}$`).MatchString(id) || len(token) < 32 || strings.ContainsAny(token, " \t\r\n") {
		return Client{}, errors.New("invalid runtime environment")
	}
	if mode == "local" {
		ip := net.ParseIP(u.Hostname())
		if u.Scheme != "http" || ip == nil || !ip.IsLoopback() {
			return Client{}, errors.New("local mode requires a loopback HTTP URL")
		}
	} else if mode == "central" {
		if u.Scheme != "https" {
			return Client{}, errors.New("central mode requires an internal HTTPS URL")
		}
	} else {
		return Client{}, errors.New("HUOHUA_SERVICE_MODE must be local or central")
	}
	return Client{URL: strings.TrimRight(address, "/"), GameID: id, Token: token, HTTP: &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}}, nil
}

func (c Client) Call(ctx context.Context, method, path string, input, output any) error {
	var body []byte
	var err error
	if input != nil {
		body, err = json.Marshal(input)
		if err != nil {
			return err
		}
	}
	req, err := http.NewRequestWithContext(ctx, method, c.URL+"/v1/games/"+c.GameID+path, bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+c.Token)
	req.Header.Set("Content-Type", "application/json")
	res, err := c.HTTP.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode >= 300 {
		return fmt.Errorf("public service HTTP %d; query current state before retry", res.StatusCode)
	}
	return json.NewDecoder(io.LimitReader(res.Body, 2<<20)).Decode(output)
}

type Claim struct {
	ID     string `json:"claimId"`
	Status string `json:"status"`
}

// grant must atomically record claimID in the game's own grant table alongside
// adding the items. Repeating it must return success without adding items twice.
func ClaimGift(ctx context.Context, c Client, playerID, code, key string, grant func(context.Context, string) error) error {
	var claim Claim
	if err := c.Call(ctx, "POST", "/gifts/reservations", map[string]string{"playerId": playerID, "cdkey": code, "idempotencyKey": key}, &claim); err != nil {
		return err
	}
	if claim.Status == "confirmed" {
		return nil
	}
	if grant == nil {
		return errors.New("game item grant implementation required")
	}
	if err := grant(ctx, claim.ID); err != nil {
		return err
	}
	// If confirmation times out, query this claimID or repeat the same workflow;
	// grant must remain idempotent. Do not create a new key to grant again.
	return c.Call(ctx, "POST", "/gifts/reservations/"+claim.ID+"/confirm", nil, &claim)
}

// ConsumeStamina must be called only after authenticating the game player and
// assigning a stable business ID to the level/session action.
func (c Client) ConsumeStamina(ctx context.Context, playerID, eventID string, amount int64) (map[string]any, error) {
	var out map[string]any
	err := c.Call(ctx, "POST", "/stamina/consume", map[string]any{"playerId": playerID, "eventId": eventID, "amount": amount, "reason": "level_start"}, &out)
	return out, err
}
func (c Client) SubmitLeaderboard(ctx context.Context, playerID, field, eventID string, value float64, revision int64) error {
	var out map[string]any
	return c.Call(ctx, "POST", "/leaderboards/scores", map[string]any{"playerId": playerID, "field": field, "eventId": eventID, "value": value, "revision": revision}, &out)
}
func (c Client) CreateRewardedContext(ctx context.Context, playerID, placementID, key string) (map[string]any, error) {
	var out map[string]any
	err := c.Call(ctx, "POST", "/ads/contexts", map[string]string{"playerId": playerID, "placementId": placementID, "idempotencyKey": key}, &out)
	return out, err
}

// ForwardAdCallback is attached to the game's public HTTPS callback route.
// Preserve RawQuery (including encrypted bytes); do not verify/award again here.
func (c Client) ForwardAdCallback(w http.ResponseWriter, r *http.Request) {
	if r.Method != "GET" {
		http.Error(w, "method not allowed", 405)
		return
	}
	if len(r.URL.RawQuery) > 65536 {
		http.Error(w, "callback too large", 413)
		return
	}
	req, err := http.NewRequestWithContext(r.Context(), "GET", c.URL+"/v1/games/"+c.GameID+"/ads/wechat-callback?"+r.URL.RawQuery, nil)
	if err != nil {
		http.Error(w, "invalid callback", 400)
		return
	}
	req.Header.Set("Authorization", "Bearer "+c.Token)
	client := http.Client{Timeout: 10 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
	res, err := client.Do(req)
	if err != nil {
		http.Error(w, "callback service unavailable", 502)
		return
	}
	defer res.Body.Close()
	body, err := io.ReadAll(io.LimitReader(res.Body, 65537))
	if err != nil || len(body) > 65536 {
		http.Error(w, "invalid callback response", 502)
		return
	}
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(res.StatusCode)
	_, _ = w.Write(body)
}
func main() {
	client, e := ClientFromEnvironment()
	if e != nil {
		fmt.Fprintln(os.Stderr, e)
		os.Exit(2)
	}
	credential := AuthCredential{LoginMethod: os.Getenv("HUOHUA_LOGIN_METHOD"), Code: os.Getenv("HUOHUA_LOGIN_CODE"), LoginName: os.Getenv("HUOHUA_LOGIN_NAME"), Password: os.Getenv("HUOHUA_LOGIN_PASSWORD")}
	session, e := client.Authenticate(context.Background(), os.Getenv("HUOHUA_ACCOUNT_PLATFORM"), credential)
	if e != nil {
		fmt.Fprintln(os.Stderr, e)
		os.Exit(1)
	}
	fmt.Println("playerId:", session.Player.ID)
}

type PlayerSession struct {
	LoginMethod string `json:"loginMethod"`
	Player      struct {
		ID       string `json:"playerId"`
		GameID   string `json:"gameId"`
		Platform string `json:"platform"`
	} `json:"player"`
	Token     string    `json:"sessionToken"`
	ExpiresAt time.Time `json:"expiresAt"`
}

type AuthCredential struct {
	LoginMethod string `json:"loginMethod"`
	Code        string `json:"code,omitempty"`
	LoginName   string `json:"loginName,omitempty"`
	Password    string `json:"password,omitempty"`
}

func (c Client) Authenticate(ctx context.Context, platform string, credential AuthCredential) (PlayerSession, error) {
	var out PlayerSession
	if (platform != "app" && platform != "wechat" && platform != "taptap") || (credential.LoginMethod != "password" && credential.LoginMethod != "wechat" && credential.LoginMethod != "taptap") {
		return out, errors.New("unsupported platform or login method")
	}
	if err := c.Call(ctx, "POST", "/auth/"+platform+"/login", credential, &out); err != nil {
		return out, err
	}
	if out.Player.GameID != c.GameID || out.Player.Platform != platform || out.LoginMethod != credential.LoginMethod || out.Player.ID == "" || len(out.Token) != 64 {
		return PlayerSession{}, errors.New("invalid login response")
	}
	return out, nil
}
