package main

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

	activityv1 "hyapp.local/api/proto/activity/v1"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	adminBaseURL    = "http://127.0.0.1:13100"
	gatewayBaseURL  = "http://127.0.0.1:13000"
	activityGRPCURL = "127.0.0.1:13006"
	appCode         = "lalu"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// 真实链路从后台 HTTP 登录开始，拿到后台 token 后只通过 daily-task 接口创建配置，不直接写 task_definitions。
	adminToken := must(adminLogin(ctx))
	suffix := fmt.Sprintf("%d", time.Now().UnixNano())
	task := must(createTask(ctx, adminToken, suffix))

	// App 用户必须从 gateway 创建，确保 user-service 生成真实用户、session、access token 和腾讯 IM 账号导入链路。
	user := must(quickCreateUser(ctx, suffix))

	// IM 测试走 gateway 的真实 UserSig 接口；这里校验 SDKAppID、IM user_id 和 user_sig 都按当前 access token 生成。
	im := must(getIMUserSig(ctx, user.AccessToken))

	// 任务进度不允许 App 自报；smoke 用 activity-service 真实 gRPC 消费入口模拟 owner outbox worker 投递的服务端事实。
	event := must(consumeTaskEvent(ctx, task.TaskID, user.UserID, suffix))

	// App 查询和领取只走 gateway HTTP；领取后 activity-service 会调用真实 wallet-service CreditTaskReward。
	beforeClaim := must(getTaskItem(ctx, user.AccessToken, task.TaskID))
	claim := must(claimTask(ctx, user.AccessToken, beforeClaim, suffix))
	afterClaim := must(getTaskItem(ctx, user.AccessToken, task.TaskID))

	// 输出固定字段，方便人工和脚本快速确认本地真实接口链路是否完整通过。
	result := map[string]any{
		"admin_task_id":                 task.TaskID,
		"admin_task_icon_key":           task.IconKey,
		"admin_task_icon_url":           task.IconURL,
		"admin_task_action_type":        task.ActionType,
		"app_user_id":                   user.UserID,
		"app_display_user_id":           user.DisplayUserID,
		"im_sdk_app_id":                 im.SDKAppID,
		"im_user_id":                    im.UserID,
		"im_user_sig_non_empty":         im.UserSig != "",
		"event_status":                  event.Status,
		"event_matched_task_count":      event.MatchedTaskCount,
		"task_status_before_claim":      beforeClaim.Status,
		"task_progress_before_claim":    beforeClaim.ProgressValue,
		"task_claimable_before_claim":   beforeClaim.Claimable,
		"claim_status":                  claim.Status,
		"claim_wallet_transaction_id":   claim.WalletTransactionID,
		"claim_reward_coin_amount":      claim.RewardCoinAmount,
		"task_status_after_claim":       afterClaim.Status,
		"task_claimable_after_claim":    afterClaim.Claimable,
		"app_task_icon_key":             beforeClaim.IconKey,
		"app_task_icon_url":             beforeClaim.IconURL,
		"app_task_action_type":          beforeClaim.ActionType,
		"app_task_action_param":         beforeClaim.ActionParam,
		"app_task_action_payload_entry": beforeClaim.ActionPayload["entry"],
	}
	encoder := json.NewEncoder(os.Stdout)
	encoder.SetIndent("", "  ")
	if err := encoder.Encode(result); err != nil {
		panic(err)
	}
}

func adminLogin(ctx context.Context) (string, error) {
	var envelope struct {
		Data struct {
			AccessToken string `json:"accessToken"`
		} `json:"data"`
	}
	if err := postJSON(ctx, adminBaseURL+"/api/v1/auth/login", "", map[string]any{
		"username": "admin",
		"password": "admin123",
	}, &envelope); err != nil {
		return "", err
	}
	if envelope.Data.AccessToken == "" {
		return "", errors.New("admin login returned empty accessToken")
	}
	return envelope.Data.AccessToken, nil
}

type taskConfig struct {
	TaskID     string `json:"task_id"`
	IconKey    string `json:"icon_key"`
	IconURL    string `json:"icon_url"`
	ActionType string `json:"action_type"`
}

func createTask(ctx context.Context, adminToken string, suffix string) (taskConfig, error) {
	var envelope struct {
		Data taskConfig `json:"data"`
	}
	err := postJSON(ctx, adminBaseURL+"/api/v1/admin/activity/daily-tasks", adminToken, map[string]any{
		"task_type":             "daily",
		"category":              "smoke",
		"metric_type":           "recharge_coin",
		"title":                 "Codex真实链路Smoke-" + suffix,
		"description":           "本地真实接口任务链路验证",
		"audience_type":         "all",
		"icon_key":              "coin",
		"icon_url":              "https://media.haiyihy.com/admin/task-icon-coin.png",
		"action_type":           "wallet",
		"action_param":          "wallet",
		"action_payload_json":   `{"entry":"wallet"}`,
		"dimension_filter_json": `{}`,
		"target_value":          100,
		"target_unit":           "coin",
		"reward_coin_amount":    20,
		"status":                "active",
		"sort_order":            1,
		"effective_from_ms":     0,
		"effective_to_ms":       0,
	}, &envelope)
	if err != nil {
		return taskConfig{}, err
	}
	if envelope.Data.TaskID == "" || envelope.Data.IconKey != "coin" || envelope.Data.ActionType != "wallet" {
		return taskConfig{}, fmt.Errorf("admin task response missing metadata: %+v", envelope.Data)
	}
	return envelope.Data, nil
}

type appUser struct {
	UserID        string
	DisplayUserID string
	AccessToken   string
}

func quickCreateUser(ctx context.Context, suffix string) (appUser, error) {
	var envelope struct {
		Data struct {
			Token struct {
				UserID        string `json:"user_id"`
				DisplayUserID string `json:"display_user_id"`
				AccessToken   string `json:"access_token"`
			} `json:"token"`
		} `json:"data"`
	}
	if err := postJSON(ctx, gatewayBaseURL+"/api/v1/auth/account/quick-create", "", map[string]any{
		"password":  "TaskFlow@123456",
		"device_id": "codex-task-smoke-" + suffix,
		"platform":  "ios",
		"language":  "zh-CN",
		"timezone":  "Asia/Shanghai",
	}, &envelope); err != nil {
		return appUser{}, err
	}
	user := appUser{
		UserID:        envelope.Data.Token.UserID,
		DisplayUserID: envelope.Data.Token.DisplayUserID,
		AccessToken:   envelope.Data.Token.AccessToken,
	}
	if user.UserID == "" || user.AccessToken == "" {
		return appUser{}, fmt.Errorf("quick create returned incomplete token: %+v", user)
	}
	return user, nil
}

type imUserSig struct {
	SDKAppID int64  `json:"sdk_app_id"`
	UserID   string `json:"user_id"`
	UserSig  string `json:"user_sig"`
}

func getIMUserSig(ctx context.Context, accessToken string) (imUserSig, error) {
	var envelope struct {
		Data imUserSig `json:"data"`
	}
	if err := getJSON(ctx, gatewayBaseURL+"/api/v1/im/usersig", accessToken, &envelope); err != nil {
		return imUserSig{}, err
	}
	if envelope.Data.SDKAppID == 0 || envelope.Data.UserID == "" || envelope.Data.UserSig == "" {
		return imUserSig{}, fmt.Errorf("im usersig response incomplete: %+v", envelope.Data)
	}
	return envelope.Data, nil
}

type taskEventResult struct {
	Status           string
	MatchedTaskCount int32
}

func consumeTaskEvent(ctx context.Context, taskID string, userID string, suffix string) (taskEventResult, error) {
	conn, err := grpc.NewClient(activityGRPCURL, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return taskEventResult{}, err
	}
	defer conn.Close()
	resp, err := activityv1.NewTaskServiceClient(conn).ConsumeTaskEvent(ctx, &activityv1.ConsumeTaskEventRequest{
		Meta: &activityv1.RequestMeta{
			RequestId: "task-smoke-event-" + suffix,
			Caller:    "task-system-smoke",
			AppCode:   appCode,
			SentAtMs:  time.Now().UTC().UnixMilli(),
		},
		EventId:        "task-smoke-event-" + taskID + "-" + suffix,
		EventType:      "WalletRechargeRecorded",
		SourceService:  "wallet-service",
		UserId:         mustParseUserID(userID),
		MetricType:     "recharge_coin",
		Value:          100,
		OccurredAtMs:   time.Now().UTC().UnixMilli(),
		DimensionsJson: `{"source":"task-system-smoke"}`,
	})
	if err != nil {
		return taskEventResult{}, err
	}
	return taskEventResult{Status: resp.GetStatus(), MatchedTaskCount: resp.GetMatchedTaskCount()}, nil
}

type taskItem struct {
	TaskID        string         `json:"task_id"`
	TaskType      string         `json:"task_type"`
	TaskDay       string         `json:"task_day"`
	Status        string         `json:"status"`
	ProgressValue int64          `json:"progress_value"`
	Claimable     bool           `json:"claimable"`
	IconKey       string         `json:"icon_key"`
	IconURL       string         `json:"icon_url"`
	ActionType    string         `json:"action_type"`
	ActionParam   string         `json:"action_param"`
	ActionPayload map[string]any `json:"action_payload"`
}

func getTaskItem(ctx context.Context, accessToken string, taskID string) (taskItem, error) {
	var envelope struct {
		Data struct {
			Sections []struct {
				Items []taskItem `json:"items"`
			} `json:"sections"`
		} `json:"data"`
	}
	if err := getJSON(ctx, gatewayBaseURL+"/api/v1/tasks/tabs", accessToken, &envelope); err != nil {
		return taskItem{}, err
	}
	for _, section := range envelope.Data.Sections {
		for _, item := range section.Items {
			if item.TaskID == taskID {
				if item.ActionPayload == nil {
					item.ActionPayload = map[string]any{}
				}
				return item, nil
			}
		}
	}
	return taskItem{}, fmt.Errorf("task %s not found in tabs response", taskID)
}

type claimResult struct {
	Status              string `json:"status"`
	WalletTransactionID string `json:"wallet_transaction_id"`
	RewardCoinAmount    int64  `json:"reward_coin_amount"`
}

func claimTask(ctx context.Context, accessToken string, item taskItem, suffix string) (claimResult, error) {
	var envelope struct {
		Data claimResult `json:"data"`
	}
	if err := postJSON(ctx, gatewayBaseURL+"/api/v1/tasks/claim", accessToken, map[string]any{
		"task_id":    item.TaskID,
		"task_type":  item.TaskType,
		"task_day":   item.TaskDay,
		"command_id": "cmd-task-smoke-claim-" + suffix,
	}, &envelope); err != nil {
		return claimResult{}, err
	}
	if envelope.Data.WalletTransactionID == "" {
		return claimResult{}, fmt.Errorf("claim returned empty wallet transaction: %+v", envelope.Data)
	}
	return envelope.Data, nil
}

func getJSON(ctx context.Context, url string, bearer string, out any) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return err
	}
	return doJSON(req, bearer, out)
}

func postJSON(ctx context.Context, url string, bearer string, payload any, out any) error {
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	return doJSON(req, bearer, out)
}

func doJSON(req *http.Request, bearer string, out any) error {
	req.Header.Set("X-App-Code", appCode)
	req.Header.Set("X-Request-ID", "task-smoke-"+fmt.Sprintf("%d", time.Now().UnixNano()))
	if bearer != "" {
		req.Header.Set("Authorization", "Bearer "+bearer)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("%s %s failed: status=%d body=%s", req.Method, req.URL.Path, resp.StatusCode, string(data))
	}
	if err := json.Unmarshal(data, out); err != nil {
		return fmt.Errorf("decode %s %s failed: %w body=%s", req.Method, req.URL.Path, err, string(data))
	}
	return nil
}

func mustParseUserID(value string) int64 {
	var userID int64
	if _, err := fmt.Sscanf(strings.TrimSpace(value), "%d", &userID); err != nil || userID <= 0 {
		panic(fmt.Errorf("invalid user_id %q", value))
	}
	return userID
}

func must[T any](value T, err error) T {
	if err != nil {
		panic(err)
	}
	return value
}
