// Standard-library caller for either App runtime mode. Keep the service token
// in the App backend. Default authentication uses Register/Login, not Resolve.
package main

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

type Client struct {
	Base, AppID, Token string
	HTTP               *http.Client
}

func FromEnvironment() (Client, error) {
	base, id, token, mode := os.Getenv("HUOHUA_APP_SERVICE_URL"), os.Getenv("HUOHUA_APP_ID"), os.Getenv("HUOHUA_APP_TOKEN"), os.Getenv("HUOHUA_APP_SERVICE_MODE")
	u, e := url.Parse(base)
	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 App 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 loopback HTTP")
		}
	} else if mode == "central" {
		if u.Scheme != "https" {
			return Client{}, errors.New("central mode requires internal HTTPS")
		}
	} else {
		return Client{}, errors.New("HUOHUA_APP_SERVICE_MODE must be local or central")
	}
	return Client{Base: strings.TrimRight(base, "/"), AppID: 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 b []byte
	var e error
	if input != nil {
		b, e = json.Marshal(input)
		if e != nil {
			return e
		}
	}
	r, e := http.NewRequestWithContext(ctx, method, c.Base+"/v1/apps/"+c.AppID+path, bytes.NewReader(b))
	if e != nil {
		return e
	}
	r.Header.Set("Authorization", "Bearer "+c.Token)
	r.Header.Set("Content-Type", "application/json")
	res, e := c.HTTP.Do(r)
	if e != nil {
		return e
	}
	defer res.Body.Close()
	if res.StatusCode < 200 || res.StatusCode >= 300 {
		return fmt.Errorf("App service HTTP %d; query existing business result before retry", res.StatusCode)
	}
	return json.NewDecoder(io.LimitReader(res.Body, 2<<20)).Decode(output)
}

type Session struct {
	LoginMethod string `json:"loginMethod"`
	User        struct {
		Platform  string `json:"platform"`
		AppUserID string `json:"appUserId"`
		AppID     string `json:"appId"`
		LoginName string `json:"loginName"`
	} `json:"user"`
	SessionToken string    `json:"sessionToken"`
	ExpiresAt    time.Time `json:"expiresAt"`
}

func (c Client) Register(ctx context.Context, loginName, password string) (Session, error) {
	var out Session
	err := c.Call(ctx, "POST", "/auth/register", map[string]string{"loginMethod": "password", "loginName": loginName, "password": password}, &out)
	return out, err
}
func (c Client) Login(ctx context.Context, loginName, password string) (Session, error) {
	var out Session
	err := c.Call(ctx, "POST", "/auth/login", map[string]string{"loginMethod": "password", "loginName": loginName, "password": password}, &out)
	return out, err
}

// Success invalidates every old session; obtain a new one through Login.
func (c Client) ChangePassword(ctx context.Context, token, oldPassword, newPassword string) error {
	var out map[string]any
	return c.Call(ctx, "POST", "/auth/change-password", map[string]string{"sessionToken": token, "oldPassword": oldPassword, "newPassword": newPassword}, &out)
}

// grant must record claimId and change the real App entitlement in one App DB
// transaction. An uncertain confirmation is retried with the same claimId.
func (c Client) Claim(ctx context.Context, user, code, key string, grant func(context.Context, string) error) error {
	var v struct {
		ID     string `json:"claimId"`
		Status string `json:"status"`
	}
	if e := c.Call(ctx, "POST", "/promotion-claims", map[string]string{"appUserId": user, "code": code, "idempotencyKey": key}, &v); e != nil {
		return e
	}
	if v.Status == "confirmed" {
		return nil
	}
	if grant == nil {
		return errors.New("explicit game-independent App entitlement grant required")
	}
	if e := grant(ctx, v.ID); e != nil {
		return e
	}
	return c.Call(ctx, "POST", "/promotion-claims/"+url.PathEscape(v.ID)+"/confirm", nil, &v)
}
func main() {
	c, e := FromEnvironment()
	if e != nil {
		fmt.Fprintln(os.Stderr, e)
		os.Exit(1)
	}
	var out map[string]any
	if e = c.Call(context.Background(), "GET", "/capabilities", nil, &out); e != nil {
		fmt.Fprintln(os.Stderr, e)
		os.Exit(1)
	}
	_ = json.NewEncoder(os.Stdout).Encode(out)
}

// Management credentials stay behind the App's own operator authorization.
func ManagementFromEnvironment() (Client, error) {
	c, e := FromEnvironment()
	if e != nil {
		return c, e
	}
	token := os.Getenv("HUOHUA_APP_MANAGEMENT_TOKEN")
	if len(token) < 32 || token == c.Token || strings.ContainsAny(token, " \t\r\n") {
		return Client{}, errors.New("independent management credential required")
	}
	c.Token = token
	return c, nil
}
func (c Client) RevokeAll(ctx context.Context, token, operationID string) error {
	var out map[string]any
	return c.Call(ctx, "POST", "/auth/revoke-all-sessions", map[string]string{"sessionToken": token, "operationId": operationID}, &out)
}
func (c Client) Deactivate(ctx context.Context, token, password, operationID string) error {
	var out map[string]any
	return c.Call(ctx, "POST", "/auth/deactivate", map[string]string{"sessionToken": token, "password": password, "operationId": operationID}, &out)
}
func (c Client) ReserveCredits(ctx context.Context, user, field string, amount int64, operationID string) (string, error) {
	var out struct {
		ID string `json:"operationId"`
	}
	e := c.Call(ctx, "POST", "/entitlements/reservations", map[string]any{"appUserId": user, "field": field, "amount": amount, "operationId": operationID}, &out)
	return out.ID, e
}
func (c Client) SettleCredits(ctx context.Context, operationID string, confirm bool) error {
	action := "cancel"
	if confirm {
		action = "confirm"
	}
	var out map[string]any
	return c.Call(ctx, "POST", "/entitlements/operations/"+url.PathEscape(operationID)+"/"+action, map[string]any{}, &out)
}

// The App backend must derive user from its authenticated session, not a posted user ID.
func (c Client) CompleteAd(ctx context.Context, contextID, user, transactionID string) error {
	var out map[string]any
	return c.Call(ctx, "POST", "/ads/contexts/"+url.PathEscape(contextID)+"/complete", map[string]string{"appUserId": user, "transactionId": transactionID}, &out)
}
