package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	activityv1 "hyapp.local/api/proto/activity/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	domain "hyapp/services/activity-service/internal/domain/inviteactivity"
	inviteactivityservice "hyapp/services/activity-service/internal/service/inviteactivity"
	mysqlstorage "hyapp/services/activity-service/internal/storage/mysql"
)

const (
	appCode         = "lalu"
	operatorAdminID = int64(900100100)
	tierStatus      = "active"
)

type walletEntrySummary struct {
	Count int64 `json:"count"`
	Coins int64 `json:"coins"`
}

func main() {
	var (
		activityAddr = flag.String("activity_addr", "127.0.0.1:14006", "activity-service gRPC address")
		walletAddr   = flag.String("wallet_addr", "127.0.0.1:13004", "wallet-service gRPC address")
		activityDSN  = flag.String("activity_dsn", "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=UTC", "activity MySQL DSN")
		walletDSN    = flag.String("wallet_dsn", "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=UTC", "wallet MySQL DSN")
	)
	flag.Parse()

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	ctx = appcode.WithContext(ctx, appCode)

	conn, err := grpc.DialContext(ctx, *activityAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	must("dial activity-service", err)
	defer conn.Close()
	adminClient := activityv1.NewAdminInviteActivityRewardServiceClient(conn)
	rewardClient := activityv1.NewInviteActivityRewardServiceClient(conn)
	walletConn, err := grpc.DialContext(ctx, *walletAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	must("dial wallet-service", err)
	defer walletConn.Close()
	walletClient := walletv1.NewWalletServiceClient(walletConn)

	activityRepo, err := mysqlstorage.Open(ctx, *activityDSN)
	must("open activity mysql", err)
	defer activityRepo.Close()
	must("migrate activity mysql", activityRepo.Migrate(ctx))
	walletDB, err := sql.Open("mysql", *walletDSN)
	must("open wallet mysql", err)
	defer walletDB.Close()
	must("ping wallet mysql", walletDB.PingContext(ctx))

	runID := fmt.Sprintf("iar_%d", time.Now().UnixMilli())
	inviterUserID := int64(880000000000) + time.Now().Unix()%10000000
	invitedUserID := inviterUserID + 9001
	now := time.Now().UTC()
	cycle := inviteactivityservice.CycleForTime(now)

	config := updateConfig(ctx, adminClient, runID)
	rechargeTier := findTier(config.GetTiers(), domain.RewardTypeRecharge, runID+"_recharge_160k")
	validTier := findTier(config.GetTiers(), domain.RewardTypeValidInvite, runID+"_valid_1")

	service := inviteactivityservice.New(activityRepo, walletClient, fakeInviteAttributionSource{inviterUserID: inviterUserID})
	consumeInvite(ctx, service, runID, inviterUserID, invitedUserID, now)
	afterInviteInviterStatus := getStatus(ctx, rewardClient, inviterUserID)
	afterInviteInviteeStatus := getStatus(ctx, rewardClient, invitedUserID)
	inviterInviteClaim := findClaim(afterInviteInviterStatus.GetClaims(), domain.RewardTypeInviteInviter)
	inviteeInviteClaim := findClaim(afterInviteInviteeStatus.GetClaims(), domain.RewardTypeInvitee)
	assert(inviterInviteClaim.GetRewardCoinAmount() == 700, "inviter per invite reward should be 700 coins")
	assert(inviteeInviteClaim.GetRewardCoinAmount() == 200, "invitee per invite reward should be 200 coins")
	consumeRecharge(ctx, service, runID, inviterUserID, 1, "google_play", 80000, now.Add(time.Minute))
	consumeRecharge(ctx, service, runID, inviterUserID, 2, "mifapay", 40000, now.Add(2*time.Minute))
	consumeRecharge(ctx, service, runID, inviterUserID, 3, "coin_seller_transfer", 40000, now.Add(3*time.Minute))
	consumeValidInvite(ctx, service, runID, inviterUserID, 1, now.Add(4*time.Minute))

	beforeClaim := getStatus(ctx, rewardClient, inviterUserID)
	assert(beforeClaim.GetProgress().GetTotalRechargeCoinAmount() == 160000, "invite recharge progress should be 160000 coins")
	assert(beforeClaim.GetProgress().GetValidInviteCount() == 1, "valid invite progress should be 1")
	leaderboard := listLeaderboard(ctx, rewardClient)
	assert(len(leaderboard.GetEntries()) > 0, "leaderboard should include at least one entry")
	leaderboardEntry := findLeaderboardEntry(leaderboard.GetEntries(), inviterUserID)
	assert(leaderboardEntry != nil, "leaderboard should include inviter")
	assert(leaderboardEntry.GetValidInviteCount() == 1, "leaderboard valid invite count should be 1")

	rechargeClaim := claim(ctx, rewardClient, inviterUserID, domain.RewardTypeRecharge, rechargeTier.GetTierId(), runID+"_claim_recharge")
	validClaim := claim(ctx, rewardClient, inviterUserID, domain.RewardTypeValidInvite, validTier.GetTierId(), runID+"_claim_valid")
	afterClaim := getStatus(ctx, rewardClient, inviterUserID)
	assert(afterClaim.GetProgress().GetClaimedRewardCoinAmount() == 3500, "claimed reward progress should be 3500 coins")

	walletInviteInviter := queryWalletEntry(ctx, walletDB, inviterInviteClaim.GetWalletTransactionId())
	walletInvitee := queryWalletEntry(ctx, walletDB, inviteeInviteClaim.GetWalletTransactionId())
	walletRecharge := queryWalletEntry(ctx, walletDB, rechargeClaim.GetWalletTransactionId())
	walletValid := queryWalletEntry(ctx, walletDB, validClaim.GetWalletTransactionId())
	assert(walletInviteInviter.Coins == 700, "wallet inviter per invite reward should credit 700 coins")
	assert(walletInvitee.Coins == 200, "wallet invitee per invite reward should credit 200 coins")
	assert(walletRecharge.Coins == 2500, "wallet recharge tier reward should credit 2500 coins")
	assert(walletValid.Coins == 300, "wallet valid invite reward should credit 300 coins")

	out := map[string]any{
		"run_id":          runID,
		"inviter_user_id": inviterUserID,
		"invited_user_id": invitedUserID,
		"cycle":           cycle.Key,
		"progress_before_claim": map[string]int64{
			"total_recharge_coin_amount": beforeClaim.GetProgress().GetTotalRechargeCoinAmount(),
			"valid_invite_count":         beforeClaim.GetProgress().GetValidInviteCount(),
		},
		"leaderboard_entry": map[string]any{
			"rank_no":                    leaderboardEntry.GetRankNo(),
			"user_id":                    leaderboardEntry.GetUserId(),
			"valid_invite_count":         leaderboardEntry.GetValidInviteCount(),
			"total_recharge_coin_amount": leaderboardEntry.GetTotalRechargeCoinAmount(),
		},
		"claims": []map[string]any{
			{"reward_type": inviterInviteClaim.GetRewardType(), "tier_code": inviterInviteClaim.GetTierCode(), "reward_coin_amount": inviterInviteClaim.GetRewardCoinAmount(), "wallet_transaction_id": inviterInviteClaim.GetWalletTransactionId()},
			{"reward_type": inviteeInviteClaim.GetRewardType(), "tier_code": inviteeInviteClaim.GetTierCode(), "reward_coin_amount": inviteeInviteClaim.GetRewardCoinAmount(), "wallet_transaction_id": inviteeInviteClaim.GetWalletTransactionId()},
			{"reward_type": rechargeClaim.GetRewardType(), "tier_code": rechargeClaim.GetTierCode(), "reward_coin_amount": rechargeClaim.GetRewardCoinAmount(), "wallet_transaction_id": rechargeClaim.GetWalletTransactionId()},
			{"reward_type": validClaim.GetRewardType(), "tier_code": validClaim.GetTierCode(), "reward_coin_amount": validClaim.GetRewardCoinAmount(), "wallet_transaction_id": validClaim.GetWalletTransactionId()},
		},
		"wallet_entries": map[string]walletEntrySummary{
			"invite_inviter": walletInviteInviter,
			"invite_invitee": walletInvitee,
			"recharge":       walletRecharge,
			"valid_invite":   walletValid,
		},
		"progress_after_claim": map[string]int64{
			"claimed_reward_coin_amount": afterClaim.GetProgress().GetClaimedRewardCoinAmount(),
		},
	}
	encoded, _ := json.MarshalIndent(out, "", "  ")
	fmt.Println(string(encoded))
}

func updateConfig(ctx context.Context, client activityv1.AdminInviteActivityRewardServiceClient, runID string) *activityv1.InviteActivityRewardConfig {
	resp, err := client.UpdateInviteActivityRewardConfig(ctx, &activityv1.UpdateInviteActivityRewardConfigRequest{
		Meta:                             meta(runID + "_update_config"),
		Enabled:                          true,
		PerInviteInviterRewardCoinAmount: 700,
		PerInviteInviteeRewardCoinAmount: 200,
		OperatorAdminId:                  operatorAdminID,
		Tiers: []*activityv1.InviteActivityRewardTier{
			{RewardType: domain.RewardTypeRecharge, TierCode: runID + "_recharge_80k", TierName: "Local recharge 80k", ThresholdCoinAmount: 80000, RewardCoinAmount: 1000, Status: tierStatus, SortOrder: 1},
			{RewardType: domain.RewardTypeRecharge, TierCode: runID + "_recharge_160k", TierName: "Local recharge 160k", ThresholdCoinAmount: 160000, RewardCoinAmount: 2500, Status: tierStatus, SortOrder: 2},
			{RewardType: domain.RewardTypeValidInvite, TierCode: runID + "_valid_1", TierName: "Local valid invite 1", ThresholdValidInviteCount: 1, RewardCoinAmount: 300, Status: tierStatus, SortOrder: 1},
			{RewardType: domain.RewardTypeValidInvite, TierCode: runID + "_valid_2", TierName: "Local valid invite 2", ThresholdValidInviteCount: 2, RewardCoinAmount: 900, Status: tierStatus, SortOrder: 2},
		},
	})
	must("update invite activity config", err)
	return resp.GetConfig()
}

func consumeInvite(ctx context.Context, service *inviteactivityservice.Service, runID string, inviterUserID int64, invitedUserID int64, occurredAt time.Time) {
	err := service.ConsumeInviteEvent(ctx, domain.InviteEvent{
		AppCode:       appCode,
		EventID:       runID + "_invite_bound_1",
		EventType:     domain.EventTypeUserInvited,
		InvitedUserID: invitedUserID,
		InviterUserID: inviterUserID,
		InviteCode:    runID,
		Source:        "invite_h5",
		OccurredAtMS:  occurredAt.UnixMilli(),
	})
	must("consume invite bound event", err)
}

func consumeRecharge(ctx context.Context, service *inviteactivityservice.Service, runID string, inviterUserID int64, index int64, rechargeType string, coins int64, occurredAt time.Time) {
	err := service.ConsumeWalletRechargeEvent(ctx, domain.RechargeEvent{
		AppCode:            appCode,
		EventID:            fmt.Sprintf("%s_recharge_%d", runID, index),
		EventType:          domain.EventTypeWalletRechargeRecorded,
		TransactionID:      fmt.Sprintf("%s_tx_%d", runID, index),
		CommandID:          fmt.Sprintf("%s_cmd_%d", runID, index),
		InvitedUserID:      inviterUserID + 100 + index,
		InviterUserID:      inviterUserID,
		RechargeCoinAmount: coins,
		RechargeType:       rechargeType,
		OccurredAtMS:       occurredAt.UnixMilli(),
	})
	must("consume invite recharge "+rechargeType, err)
}

func consumeValidInvite(ctx context.Context, service *inviteactivityservice.Service, runID string, inviterUserID int64, index int64, occurredAt time.Time) {
	err := service.ConsumeValidInviteEvent(ctx, domain.ValidInviteEvent{
		AppCode:       appCode,
		EventID:       fmt.Sprintf("%s_valid_%d", runID, index),
		EventType:     domain.EventTypeUserInviteBecameValid,
		InvitedUserID: inviterUserID + 200 + index,
		InviterUserID: inviterUserID,
		OccurredAtMS:  occurredAt.UnixMilli(),
	})
	must("consume valid invite", err)
}

func getStatus(ctx context.Context, client activityv1.InviteActivityRewardServiceClient, userID int64) *activityv1.InviteActivityRewardStatus {
	resp, err := client.GetInviteActivityRewardStatus(ctx, &activityv1.GetInviteActivityRewardStatusRequest{
		Meta:   meta("get_status"),
		UserId: userID,
	})
	must("get invite activity status", err)
	return resp.GetStatus()
}

func listLeaderboard(ctx context.Context, client activityv1.InviteActivityRewardServiceClient) *activityv1.ListInviteActivityLeaderboardResponse {
	resp, err := client.ListInviteActivityLeaderboard(ctx, &activityv1.ListInviteActivityLeaderboardRequest{
		Meta:     meta("list_leaderboard"),
		Page:     1,
		PageSize: 50,
	})
	must("list invite activity leaderboard", err)
	return resp
}

func claim(ctx context.Context, client activityv1.InviteActivityRewardServiceClient, userID int64, rewardType string, tierID int64, commandID string) *activityv1.InviteActivityRewardClaim {
	resp, err := client.ClaimInviteActivityReward(ctx, &activityv1.ClaimInviteActivityRewardRequest{
		Meta:       meta(commandID),
		UserId:     userID,
		RewardType: rewardType,
		TierId:     tierID,
		CommandId:  commandID,
	})
	must("claim invite activity reward "+rewardType, err)
	return resp.GetClaim()
}

func queryWalletEntry(ctx context.Context, db *sql.DB, transactionID string) walletEntrySummary {
	var summary walletEntrySummary
	err := db.QueryRowContext(ctx, `
		SELECT COUNT(*), COALESCE(SUM(available_delta), 0)
		FROM wallet_entries
		WHERE app_code = ? AND transaction_id = ? AND asset_type = 'COIN'`,
		appCode, transactionID,
	).Scan(&summary.Count, &summary.Coins)
	must("query wallet entry "+transactionID, err)
	return summary
}

func findTier(tiers []*activityv1.InviteActivityRewardTier, rewardType string, tierCode string) *activityv1.InviteActivityRewardTier {
	for _, tier := range tiers {
		if tier.GetRewardType() == rewardType && tier.GetTierCode() == tierCode {
			return tier
		}
	}
	log.Fatalf("tier not found: %s %s", rewardType, tierCode)
	return nil
}

func findClaim(claims []*activityv1.InviteActivityRewardClaim, rewardType string) *activityv1.InviteActivityRewardClaim {
	for _, claim := range claims {
		if claim.GetRewardType() == rewardType {
			return claim
		}
	}
	log.Fatalf("claim not found: %s", rewardType)
	return nil
}

func findLeaderboardEntry(entries []*activityv1.InviteActivityLeaderboardEntry, userID int64) *activityv1.InviteActivityLeaderboardEntry {
	for _, entry := range entries {
		if entry.GetUserId() == userID {
			return entry
		}
	}
	return nil
}

func meta(requestID string) *activityv1.RequestMeta {
	return &activityv1.RequestMeta{
		RequestId: requestID,
		Caller:    "invite-activity-local-flow",
		AppCode:   appCode,
		SentAtMs:  time.Now().UTC().UnixMilli(),
	}
}

func assert(ok bool, message string) {
	if !ok {
		log.Fatal(message)
	}
}

func must(step string, err error) {
	if err != nil {
		log.Fatalf("%s: %v", step, err)
	}
}

type fakeInviteAttributionSource struct {
	inviterUserID int64
}

func (s fakeInviteAttributionSource) GetInviteAttribution(_ context.Context, invitedUserID int64) (*userv1.InviteAttribution, error) {
	return &userv1.InviteAttribution{
		Found:         true,
		AppCode:       appCode,
		InvitedUserId: invitedUserID,
		InviterUserId: s.inviterUserID,
	}, nil
}
