package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	domain "hyapp/services/activity-service/internal/domain/sevendaycheckin"
)

type checkinRewardSnapshot struct {
	Rewards []checkinRewardSnapshotItem `json:"rewards"`
}

type checkinRewardSnapshotItem struct {
	DayIndex        int32 `json:"day_index"`
	ResourceGroupID int64 `json:"resource_group_id"`
}

// GetSevenDayCheckInConfig 读取七日签到当前配置；未配置是后台首屏的正常状态。
func (r *Repository) GetSevenDayCheckInConfig(ctx context.Context) (domain.Config, bool, error) {
	if r == nil || r.db == nil {
		return domain.Config{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	config, exists, err := r.getSevenDayCheckInConfig(ctx, r.db)
	if err != nil || !exists {
		return config, exists, err
	}
	rewards, err := r.listSevenDayCheckInCurrentRewards(ctx, r.db)
	if err != nil {
		return domain.Config{}, false, err
	}
	config.Rewards = rewards
	return config, true, nil
}

// GetSevenDayCheckInRewardsForVersion 从版本快照读取旧轮次奖励，保证用户一轮内配置稳定。
func (r *Repository) GetSevenDayCheckInRewardsForVersion(ctx context.Context, version int64) ([]domain.Reward, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	var snapshot string
	err := r.db.QueryRowContext(ctx, `
		SELECT COALESCE(CAST(snapshot_json AS CHAR), '')
		FROM seven_day_checkin_config_versions
		WHERE app_code = ? AND config_version = ?`,
		appcode.FromContext(ctx), version,
	).Scan(&snapshot)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, xerr.New(xerr.NotFound, "seven day checkin config version not found")
	}
	if err != nil {
		return nil, err
	}
	return rewardsFromSnapshot(snapshot)
}

// UpdateSevenDayCheckInConfig 原子替换当前 7 天奖励，并写入不可变版本快照。
func (r *Repository) UpdateSevenDayCheckInConfig(ctx context.Context, config domain.Config, nowMS int64) (domain.Config, error) {
	if r == nil || r.db == nil {
		return domain.Config{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.Config{}, err
	}
	defer func() { _ = tx.Rollback() }()

	current, exists, err := r.getSevenDayCheckInConfigForUpdate(ctx, tx)
	if err != nil {
		return domain.Config{}, err
	}
	nextVersion := int64(1)
	createdAt := nowMS
	if exists {
		nextVersion = current.ConfigVersion + 1
		createdAt = current.CreatedAtMS
	}
	snapshot, err := rewardsSnapshot(config.Rewards)
	if err != nil {
		return domain.Config{}, err
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO seven_day_checkin_configs (
			app_code, enabled, config_version, updated_by_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			enabled = VALUES(enabled),
			config_version = VALUES(config_version),
			updated_by_admin_id = VALUES(updated_by_admin_id),
			updated_at_ms = VALUES(updated_at_ms)`,
		appcode.FromContext(ctx), config.Enabled, nextVersion, config.UpdatedByAdminID, createdAt, nowMS,
	); err != nil {
		return domain.Config{}, err
	}
	if _, err := tx.ExecContext(ctx, `DELETE FROM seven_day_checkin_rewards WHERE app_code = ?`, appcode.FromContext(ctx)); err != nil {
		return domain.Config{}, err
	}
	for _, reward := range config.Rewards {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO seven_day_checkin_rewards (
				app_code, day_index, config_version, resource_group_id, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?)`,
			appcode.FromContext(ctx), reward.DayIndex, nextVersion, reward.ResourceGroupID, nowMS, nowMS,
		); err != nil {
			return domain.Config{}, err
		}
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO seven_day_checkin_config_versions (
			app_code, config_version, enabled, snapshot_json, created_by_admin_id, created_at_ms
		) VALUES (?, ?, ?, ?, ?, ?)`,
		appcode.FromContext(ctx), nextVersion, config.Enabled, snapshot, config.UpdatedByAdminID, nowMS,
	); err != nil {
		return domain.Config{}, err
	}
	if err := tx.Commit(); err != nil {
		return domain.Config{}, err
	}
	updated, exists, err := r.GetSevenDayCheckInConfig(ctx)
	if err != nil {
		return domain.Config{}, err
	}
	if !exists {
		return domain.Config{}, xerr.New(xerr.NotFound, "seven day checkin config not found")
	}
	return updated, nil
}

func (r *Repository) GetSevenDayCheckInAccount(ctx context.Context, userID int64) (domain.Account, bool, error) {
	if r == nil || r.db == nil {
		return domain.Account{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	row := r.db.QueryRowContext(ctx, checkinAccountSelectSQL()+`
		WHERE app_code = ? AND user_id = ?`,
		appcode.FromContext(ctx), userID,
	)
	account, err := scanCheckinAccount(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Account{}, false, nil
	}
	return account, err == nil, err
}

func (r *Repository) GetSevenDayCheckInClaimByDay(ctx context.Context, userID int64, checkinDay string) (domain.Claim, bool, error) {
	if r == nil || r.db == nil {
		return domain.Claim{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	row := r.db.QueryRowContext(ctx, checkinClaimSelectSQL()+`
		WHERE app_code = ? AND user_id = ? AND checkin_day = ?`,
		appcode.FromContext(ctx), userID, checkinDay,
	)
	claim, err := scanCheckinClaim(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Claim{}, false, nil
	}
	return claim, err == nil, err
}

func (r *Repository) ListSevenDayCheckInClaimsForUserCycle(ctx context.Context, userID int64, cycleNo int64) ([]domain.Claim, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	rows, err := r.db.QueryContext(ctx, checkinClaimSelectSQL()+`
		WHERE app_code = ? AND user_id = ? AND cycle_no = ?
		ORDER BY day_index ASC`,
		appcode.FromContext(ctx), userID, cycleNo,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	claims, _, err := scanCheckinClaims(rows, 0)
	return claims, err
}

// PrepareSevenDayCheckInClaim 锁定用户账户并创建/复用 pending claim；账户只在 grant 成功后推进。
func (r *Repository) PrepareSevenDayCheckInClaim(ctx context.Context, userID int64, commandID string, checkinDay string, yesterday string, nowMS int64) (domain.PrepareResult, error) {
	if r == nil || r.db == nil {
		return domain.PrepareResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.PrepareResult{}, err
	}
	defer func() { _ = tx.Rollback() }()

	if existing, exists, err := r.getCheckinClaimByCommandForUpdate(ctx, tx, commandID); err != nil || exists {
		if err != nil {
			return domain.PrepareResult{}, err
		}
		if existing.UserID != userID {
			return domain.PrepareResult{}, xerr.New(xerr.IdempotencyConflict, "checkin command payload conflicts")
		}
		if existing.Status == domain.StatusFailed {
			existing.Status = domain.StatusPending
			existing.FailureReason = ""
			existing.UpdatedAtMS = nowMS
			if err := r.updateCheckinClaimPending(ctx, tx, existing); err != nil {
				return domain.PrepareResult{}, err
			}
		}
		if err := tx.Commit(); err != nil {
			return domain.PrepareResult{}, err
		}
		return domain.PrepareResult{Claim: existing, Prepared: existing.Status != domain.StatusGranted}, nil
	}

	if existing, exists, err := r.getCheckinClaimByUserDayForUpdate(ctx, tx, userID, checkinDay); err != nil || exists {
		if err != nil {
			return domain.PrepareResult{}, err
		}
		if err := tx.Commit(); err != nil {
			return domain.PrepareResult{}, err
		}
		return domain.PrepareResult{Claim: existing, Reason: domain.ReasonAlreadySigned}, nil
	}

	config, exists, err := r.getSevenDayCheckInConfigForUpdate(ctx, tx)
	if err != nil {
		return domain.PrepareResult{}, err
	}
	if !exists {
		return domain.PrepareResult{Reason: domain.ReasonNotConfigured}, tx.Commit()
	}
	config.Rewards, err = r.listSevenDayCheckInCurrentRewards(ctx, tx)
	if err != nil {
		return domain.PrepareResult{}, err
	}
	if !config.Enabled {
		return domain.PrepareResult{Reason: domain.ReasonDisabled}, tx.Commit()
	}
	if !validCheckinRewards(config.Rewards) {
		return domain.PrepareResult{Reason: domain.ReasonInvalidConfig}, tx.Commit()
	}

	account, accountExists, err := r.getCheckinAccountForUpdate(ctx, tx, userID)
	if err != nil {
		return domain.PrepareResult{}, err
	}
	if accountExists && account.LastCheckinDay == checkinDay {
		return domain.PrepareResult{Reason: domain.ReasonAlreadySigned}, tx.Commit()
	}
	slot := nextCheckinSlot(account, accountExists, config.ConfigVersion, checkinDay, yesterday)
	rewards := config.Rewards
	if slot.ConfigVersion != config.ConfigVersion {
		rewards, err = r.getSevenDayCheckInRewardsForVersionTx(ctx, tx, slot.ConfigVersion)
		if err != nil {
			return domain.PrepareResult{}, err
		}
	}
	resourceGroupID := rewardGroupID(rewards, slot.DayIndex)
	if resourceGroupID <= 0 {
		return domain.PrepareResult{Reason: domain.ReasonInvalidConfig}, tx.Commit()
	}

	claim := domain.Claim{
		ClaimID:         idgen.New("checkin"),
		CommandID:       commandID,
		UserID:          userID,
		CycleNo:         slot.CycleNo,
		CheckinDay:      checkinDay,
		DayIndex:        slot.DayIndex,
		ConfigVersion:   slot.ConfigVersion,
		ResourceGroupID: resourceGroupID,
		Status:          domain.StatusPending,
		SignedAtMS:      nowMS,
		CreatedAtMS:     nowMS,
		UpdatedAtMS:     nowMS,
	}
	claim.WalletCommandID = walletCheckinCommandID(claim.ClaimID)
	if err := r.insertCheckinClaim(ctx, tx, claim); err != nil {
		if isMySQLDuplicate(err) {
			return domain.PrepareResult{}, xerr.New(xerr.Conflict, "checkin claim already exists")
		}
		return domain.PrepareResult{}, err
	}
	if err := tx.Commit(); err != nil {
		return domain.PrepareResult{}, err
	}
	return domain.PrepareResult{Claim: claim, Prepared: true}, nil
}

func (r *Repository) MarkSevenDayCheckInClaimGranted(ctx context.Context, claimID string, walletGrantID string, grantedAtMS int64) (domain.Claim, error) {
	if r == nil || r.db == nil {
		return domain.Claim{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.Claim{}, err
	}
	defer func() { _ = tx.Rollback() }()

	claim, err := r.getCheckinClaimByIDForUpdate(ctx, tx, claimID)
	if err != nil {
		return domain.Claim{}, err
	}
	if claim.Status == domain.StatusGranted {
		if err := tx.Commit(); err != nil {
			return domain.Claim{}, err
		}
		return claim, nil
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE seven_day_checkin_claims
		SET status = 'granted',
		    wallet_grant_id = ?,
		    failure_reason = '',
		    granted_at_ms = ?,
		    updated_at_ms = ?
		WHERE app_code = ? AND claim_id = ?`,
		walletGrantID, grantedAtMS, grantedAtMS, appcode.FromContext(ctx), claimID,
	); err != nil {
		return domain.Claim{}, err
	}
	if err := r.upsertCheckinAccountFromClaim(ctx, tx, claim, grantedAtMS); err != nil {
		return domain.Claim{}, err
	}
	if err := tx.Commit(); err != nil {
		return domain.Claim{}, err
	}
	claim.Status = domain.StatusGranted
	claim.WalletGrantID = walletGrantID
	claim.FailureReason = ""
	claim.GrantedAtMS = grantedAtMS
	claim.UpdatedAtMS = grantedAtMS
	return claim, nil
}

func (r *Repository) MarkSevenDayCheckInClaimFailed(ctx context.Context, claimID string, failureReason string, nowMS int64) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()
	claim, err := r.getCheckinClaimByIDForUpdate(ctx, tx, claimID)
	if err != nil {
		return err
	}
	if claim.Status == domain.StatusGranted {
		return tx.Commit()
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE seven_day_checkin_claims
		SET status = 'failed', failure_reason = ?, updated_at_ms = ?
		WHERE app_code = ? AND claim_id = ?`,
		truncateTaskFailure(failureReason), nowMS, appcode.FromContext(ctx), claimID,
	); err != nil {
		return err
	}
	return tx.Commit()
}

func (r *Repository) ListSevenDayCheckInClaims(ctx context.Context, query domain.ClaimQuery) ([]domain.Claim, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	where, args := checkinClaimWhere(ctx, query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM seven_day_checkin_claims `+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	pageSize := normalizePageSize(query.PageSize)
	page := query.Page
	if page <= 0 {
		page = 1
	}
	args = append(args, pageSize, (page-1)*pageSize)
	rows, err := r.db.QueryContext(ctx, checkinClaimSelectSQL()+`
		`+where+`
		ORDER BY created_at_ms DESC, claim_id DESC
		LIMIT ? OFFSET ?`, args...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	return scanCheckinClaims(rows, total)
}

func (r *Repository) getSevenDayCheckInConfig(ctx context.Context, q queryer) (domain.Config, bool, error) {
	row := q.QueryRowContext(ctx, checkinConfigSelectSQL()+` WHERE app_code = ?`, appcode.FromContext(ctx))
	config, err := scanCheckinConfig(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Config{}, false, nil
	}
	return config, err == nil, err
}

func (r *Repository) getSevenDayCheckInConfigForUpdate(ctx context.Context, tx *sql.Tx) (domain.Config, bool, error) {
	row := tx.QueryRowContext(ctx, checkinConfigSelectSQL()+` WHERE app_code = ? FOR UPDATE`, appcode.FromContext(ctx))
	config, err := scanCheckinConfig(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Config{}, false, nil
	}
	return config, err == nil, err
}

func (r *Repository) listSevenDayCheckInCurrentRewards(ctx context.Context, q queryer) ([]domain.Reward, error) {
	rows, err := q.QueryContext(ctx, `
		SELECT day_index, resource_group_id
		FROM seven_day_checkin_rewards
		WHERE app_code = ?
		ORDER BY day_index ASC`,
		appcode.FromContext(ctx),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanCheckinRewards(rows)
}

func (r *Repository) getSevenDayCheckInRewardsForVersionTx(ctx context.Context, tx *sql.Tx, version int64) ([]domain.Reward, error) {
	var snapshot string
	err := tx.QueryRowContext(ctx, `
		SELECT COALESCE(CAST(snapshot_json AS CHAR), '')
		FROM seven_day_checkin_config_versions
		WHERE app_code = ? AND config_version = ?`,
		appcode.FromContext(ctx), version,
	).Scan(&snapshot)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, xerr.New(xerr.NotFound, "seven day checkin config version not found")
	}
	if err != nil {
		return nil, err
	}
	return rewardsFromSnapshot(snapshot)
}

func (r *Repository) getCheckinAccountForUpdate(ctx context.Context, tx *sql.Tx, userID int64) (domain.Account, bool, error) {
	row := tx.QueryRowContext(ctx, checkinAccountSelectSQL()+`
		WHERE app_code = ? AND user_id = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), userID,
	)
	account, err := scanCheckinAccount(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Account{}, false, nil
	}
	return account, err == nil, err
}

func (r *Repository) getCheckinClaimByCommandForUpdate(ctx context.Context, tx *sql.Tx, commandID string) (domain.Claim, bool, error) {
	row := tx.QueryRowContext(ctx, checkinClaimSelectSQL()+`
		WHERE app_code = ? AND command_id = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), commandID,
	)
	claim, err := scanCheckinClaim(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Claim{}, false, nil
	}
	return claim, err == nil, err
}

func (r *Repository) getCheckinClaimByUserDayForUpdate(ctx context.Context, tx *sql.Tx, userID int64, checkinDay string) (domain.Claim, bool, error) {
	row := tx.QueryRowContext(ctx, checkinClaimSelectSQL()+`
		WHERE app_code = ? AND user_id = ? AND checkin_day = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), userID, checkinDay,
	)
	claim, err := scanCheckinClaim(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Claim{}, false, nil
	}
	return claim, err == nil, err
}

func (r *Repository) getCheckinClaimByIDForUpdate(ctx context.Context, tx *sql.Tx, claimID string) (domain.Claim, error) {
	row := tx.QueryRowContext(ctx, checkinClaimSelectSQL()+`
		WHERE app_code = ? AND claim_id = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), claimID,
	)
	claim, err := scanCheckinClaim(row)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Claim{}, xerr.New(xerr.NotFound, "seven day checkin claim not found")
	}
	return claim, err
}

func (r *Repository) insertCheckinClaim(ctx context.Context, tx *sql.Tx, claim domain.Claim) error {
	_, err := tx.ExecContext(ctx, `
		INSERT INTO seven_day_checkin_claims (
			app_code, claim_id, command_id, user_id, cycle_no, checkin_day, day_index,
			config_version, resource_group_id, wallet_command_id, wallet_grant_id,
			status, failure_reason, signed_at_ms, granted_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, '', ?, 0, ?, ?)`,
		appcode.FromContext(ctx), claim.ClaimID, claim.CommandID, claim.UserID, claim.CycleNo,
		claim.CheckinDay, claim.DayIndex, claim.ConfigVersion, claim.ResourceGroupID,
		claim.WalletCommandID, claim.Status, claim.SignedAtMS, claim.CreatedAtMS, claim.UpdatedAtMS,
	)
	return err
}

func (r *Repository) updateCheckinClaimPending(ctx context.Context, tx *sql.Tx, claim domain.Claim) error {
	_, err := tx.ExecContext(ctx, `
		UPDATE seven_day_checkin_claims
		SET status = 'pending',
		    wallet_grant_id = '',
		    failure_reason = '',
		    granted_at_ms = 0,
		    updated_at_ms = ?
		WHERE app_code = ? AND claim_id = ?`,
		claim.UpdatedAtMS, appcode.FromContext(ctx), claim.ClaimID,
	)
	return err
}

func (r *Repository) upsertCheckinAccountFromClaim(ctx context.Context, tx *sql.Tx, claim domain.Claim, nowMS int64) error {
	account, exists, err := r.getCheckinAccountForUpdate(ctx, tx, claim.UserID)
	if err != nil {
		return err
	}
	cycleStartDay := claim.CheckinDay
	if exists && account.CycleNo == claim.CycleNo && account.CycleStartDay != "" && claim.DayIndex > 1 {
		cycleStartDay = account.CycleStartDay
	}
	_, err = tx.ExecContext(ctx, `
		INSERT INTO user_seven_day_checkin_accounts (
			app_code, user_id, cycle_no, cycle_start_day, last_checkin_day,
			last_day_index, config_version, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			cycle_no = VALUES(cycle_no),
			cycle_start_day = VALUES(cycle_start_day),
			last_checkin_day = VALUES(last_checkin_day),
			last_day_index = VALUES(last_day_index),
			config_version = VALUES(config_version),
			updated_at_ms = VALUES(updated_at_ms)`,
		appcode.FromContext(ctx), claim.UserID, claim.CycleNo, cycleStartDay, claim.CheckinDay,
		claim.DayIndex, claim.ConfigVersion, nowMS,
	)
	return err
}

func scanCheckinConfig(row rowScanner) (domain.Config, error) {
	var config domain.Config
	err := row.Scan(
		&config.AppCode,
		&config.Enabled,
		&config.ConfigVersion,
		&config.UpdatedByAdminID,
		&config.CreatedAtMS,
		&config.UpdatedAtMS,
	)
	return config, err
}

func scanCheckinRewards(rows *sql.Rows) ([]domain.Reward, error) {
	rewards := make([]domain.Reward, 0)
	for rows.Next() {
		var reward domain.Reward
		if err := rows.Scan(&reward.DayIndex, &reward.ResourceGroupID); err != nil {
			return nil, err
		}
		rewards = append(rewards, reward)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return rewards, nil
}

func scanCheckinAccount(row rowScanner) (domain.Account, error) {
	var account domain.Account
	err := row.Scan(
		&account.AppCode,
		&account.UserID,
		&account.CycleNo,
		&account.CycleStartDay,
		&account.LastCheckinDay,
		&account.LastDayIndex,
		&account.ConfigVersion,
		&account.UpdatedAtMS,
	)
	return account, err
}

func scanCheckinClaims(rows *sql.Rows, total int64) ([]domain.Claim, int64, error) {
	claims := make([]domain.Claim, 0)
	for rows.Next() {
		claim, err := scanCheckinClaim(rows)
		if err != nil {
			return nil, 0, err
		}
		claims = append(claims, claim)
	}
	if err := rows.Err(); err != nil {
		return nil, 0, err
	}
	return claims, total, nil
}

func scanCheckinClaim(row rowScanner) (domain.Claim, error) {
	var claim domain.Claim
	err := row.Scan(
		&claim.ClaimID,
		&claim.CommandID,
		&claim.UserID,
		&claim.CycleNo,
		&claim.CheckinDay,
		&claim.DayIndex,
		&claim.ConfigVersion,
		&claim.ResourceGroupID,
		&claim.WalletCommandID,
		&claim.WalletGrantID,
		&claim.Status,
		&claim.FailureReason,
		&claim.SignedAtMS,
		&claim.GrantedAtMS,
		&claim.CreatedAtMS,
		&claim.UpdatedAtMS,
	)
	return claim, err
}

func checkinConfigSelectSQL() string {
	return `SELECT app_code, enabled, config_version, updated_by_admin_id, created_at_ms, updated_at_ms
	        FROM seven_day_checkin_configs`
}

func checkinAccountSelectSQL() string {
	return `SELECT app_code, user_id, cycle_no, cycle_start_day, last_checkin_day,
	               last_day_index, config_version, updated_at_ms
	        FROM user_seven_day_checkin_accounts`
}

func checkinClaimSelectSQL() string {
	return `SELECT claim_id, command_id, user_id, cycle_no, checkin_day, day_index,
	               config_version, resource_group_id, wallet_command_id, wallet_grant_id,
	               status, failure_reason, signed_at_ms, granted_at_ms, created_at_ms, updated_at_ms
	        FROM seven_day_checkin_claims`
}

func checkinClaimWhere(ctx context.Context, query domain.ClaimQuery) (string, []any) {
	conditions := []string{"app_code = ?"}
	args := []any{appcode.FromContext(ctx)}
	if query.Status != "" {
		conditions = append(conditions, "status = ?")
		args = append(args, query.Status)
	}
	if query.UserID > 0 {
		conditions = append(conditions, "user_id = ?")
		args = append(args, query.UserID)
	}
	if query.CheckinDay != "" {
		conditions = append(conditions, "checkin_day = ?")
		args = append(args, query.CheckinDay)
	}
	if query.DayIndex > 0 {
		conditions = append(conditions, "day_index = ?")
		args = append(args, query.DayIndex)
	}
	return "WHERE " + strings.Join(conditions, " AND "), args
}

func rewardsSnapshot(rewards []domain.Reward) (string, error) {
	snapshot := checkinRewardSnapshot{Rewards: make([]checkinRewardSnapshotItem, 0, len(rewards))}
	for _, reward := range rewards {
		snapshot.Rewards = append(snapshot.Rewards, checkinRewardSnapshotItem{
			DayIndex:        reward.DayIndex,
			ResourceGroupID: reward.ResourceGroupID,
		})
	}
	payload, err := json.Marshal(snapshot)
	if err != nil {
		return "", err
	}
	return string(payload), nil
}

func rewardsFromSnapshot(snapshot string) ([]domain.Reward, error) {
	var decoded checkinRewardSnapshot
	if err := json.Unmarshal([]byte(snapshot), &decoded); err != nil {
		return nil, err
	}
	rewards := make([]domain.Reward, 0, len(decoded.Rewards))
	for _, reward := range decoded.Rewards {
		rewards = append(rewards, domain.Reward{DayIndex: reward.DayIndex, ResourceGroupID: reward.ResourceGroupID})
	}
	return rewards, nil
}

type checkinSlot struct {
	CycleNo       int64
	DayIndex      int32
	ConfigVersion int64
}

func nextCheckinSlot(account domain.Account, exists bool, currentVersion int64, today string, yesterday string) checkinSlot {
	if !exists || account.UserID <= 0 {
		return checkinSlot{CycleNo: 1, DayIndex: 1, ConfigVersion: currentVersion}
	}
	if account.LastCheckinDay == yesterday && account.LastDayIndex > 0 && account.LastDayIndex < domain.DayCount {
		return checkinSlot{CycleNo: account.CycleNo, DayIndex: account.LastDayIndex + 1, ConfigVersion: account.ConfigVersion}
	}
	nextCycle := account.CycleNo + 1
	if nextCycle <= 0 {
		nextCycle = 1
	}
	return checkinSlot{CycleNo: nextCycle, DayIndex: 1, ConfigVersion: currentVersion}
}

func rewardGroupID(rewards []domain.Reward, dayIndex int32) int64 {
	for _, reward := range rewards {
		if reward.DayIndex == dayIndex {
			return reward.ResourceGroupID
		}
	}
	return 0
}

func validCheckinRewards(rewards []domain.Reward) bool {
	if len(rewards) != domain.DayCount {
		return false
	}
	seen := make(map[int32]struct{}, domain.DayCount)
	for _, reward := range rewards {
		if reward.DayIndex < 1 || reward.DayIndex > domain.DayCount || reward.ResourceGroupID <= 0 {
			return false
		}
		if _, ok := seen[reward.DayIndex]; ok {
			return false
		}
		seen[reward.DayIndex] = struct{}{}
	}
	return len(seen) == domain.DayCount
}

func walletCheckinCommandID(claimID string) string {
	return fmt.Sprintf("wcheckin_%s", claimID)
}
