package mysql

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

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

// ListVisibleDefinitions 返回 App 当前可见任务；查询路径不初始化用户进度，避免 daily 刷新产生批量写。
func (r *Repository) ListVisibleDefinitions(ctx context.Context, nowMS int64) ([]taskdomain.Definition, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, task_id, task_type, category, metric_type, title, description,
		       target_value, target_unit, reward_coin_amount, status, sort_order, version,
		       current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
		       updated_by_admin_id, created_at_ms, updated_at_ms
		FROM task_definitions
		WHERE app_code = ? AND status = 'active'
		  AND (effective_from_ms = 0 OR effective_from_ms <= ?)
		  AND (effective_to_ms = 0 OR effective_to_ms > ?)
		ORDER BY sort_order ASC, task_id ASC`,
		appcode.FromContext(ctx), nowMS, nowMS,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanDefinitions(rows)
}

// ListUserProgress 批量读取当前用户 daily/lifetime 进度，缺失进度由 service 按 0 渲染。
func (r *Repository) ListUserProgress(ctx context.Context, userID int64, cycleKeys []string) ([]taskdomain.Progress, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	cycleKeys = normalizeCycleKeys(cycleKeys)
	if len(cycleKeys) == 0 {
		return nil, nil
	}
	placeholders := strings.TrimRight(strings.Repeat("?,", len(cycleKeys)), ",")
	args := []any{appcode.FromContext(ctx), userID}
	for _, cycleKey := range cycleKeys {
		args = append(args, cycleKey)
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, user_id, task_id, task_version_id, cycle_key, progress_value,
		       target_value, reward_coin_amount, status, completed_at_ms, claimed_at_ms, updated_at_ms
		FROM user_task_progress
		WHERE app_code = ? AND user_id = ? AND cycle_key IN (`+placeholders+`)`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	progresses := make([]taskdomain.Progress, 0)
	for rows.Next() {
		progress, err := scanProgress(rows)
		if err != nil {
			return nil, err
		}
		progresses = append(progresses, progress)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return progresses, nil
}

// ConsumeTaskEvent 在单事务内完成事件幂等、任务匹配和用户进度累加。
func (r *Repository) ConsumeTaskEvent(ctx context.Context, event taskdomain.Event, cycleKey string, nowMS int64) (taskdomain.EventResult, error) {
	if r == nil || r.db == nil {
		return taskdomain.EventResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return taskdomain.EventResult{}, err
	}
	defer func() { _ = tx.Rollback() }()

	inserted, existingStatus, err := r.insertTaskEventConsumption(ctx, tx, event, cycleKey, nowMS)
	if err != nil {
		return taskdomain.EventResult{}, err
	}
	if !inserted {
		return taskdomain.EventResult{EventID: event.EventID, Status: existingStatus}, nil
	}

	definitions, err := r.listDefinitionsForEvent(ctx, tx, event.MetricType, event.OccurredAtMS)
	if err != nil {
		return taskdomain.EventResult{}, err
	}
	matched := int32(0)
	for _, definition := range definitions {
		progressCycle := cycleKey
		if definition.TaskType == taskdomain.TypeExclusive {
			progressCycle = taskdomain.CycleLifetime
		}
		changed, err := r.applyTaskProgressDelta(ctx, tx, event.UserID, definition, progressCycle, event.Value, nowMS)
		if err != nil {
			return taskdomain.EventResult{}, err
		}
		if changed {
			matched++
		}
	}
	status := taskdomain.EventStatusConsumed
	skipReason := ""
	if matched == 0 {
		status = taskdomain.EventStatusSkipped
		skipReason = "no_matching_task"
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE task_event_consumption
		SET status = ?, skip_reason = ?, consumed_at_ms = ?
		WHERE app_code = ? AND event_id = ?`,
		status, skipReason, nowMS, appcode.FromContext(ctx), event.EventID,
	); err != nil {
		return taskdomain.EventResult{}, err
	}
	if err := tx.Commit(); err != nil {
		return taskdomain.EventResult{}, err
	}
	return taskdomain.EventResult{EventID: event.EventID, Status: status, MatchedTaskCount: matched}, nil
}

// ListTaskDefinitions 是后台任务配置分页查询；keyword 只匹配任务 ID、标题和指标，避免扫描大 JSON。
func (r *Repository) ListTaskDefinitions(ctx context.Context, query taskdomain.DefinitionQuery) ([]taskdomain.Definition, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	where, args := taskDefinitionWhere(ctx, query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM task_definitions `+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, `
		SELECT app_code, task_id, task_type, category, metric_type, title, description,
		       target_value, target_unit, reward_coin_amount, status, sort_order, version,
		       current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
		       updated_by_admin_id, created_at_ms, updated_at_ms
		FROM task_definitions `+where+`
		ORDER BY sort_order ASC, updated_at_ms DESC, task_id ASC
		LIMIT ? OFFSET ?`, args...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	definitions, err := scanDefinitions(rows)
	if err != nil {
		return nil, 0, err
	}
	return definitions, total, nil
}

// UpsertTaskDefinition 创建或更新任务定义；领奖关键字段变更时写新版本快照。
func (r *Repository) UpsertTaskDefinition(ctx context.Context, command taskdomain.DefinitionCommand, nowMS int64) (taskdomain.Definition, bool, error) {
	if r == nil || r.db == nil {
		return taskdomain.Definition{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return taskdomain.Definition{}, false, err
	}
	defer func() { _ = tx.Rollback() }()

	if strings.TrimSpace(command.TaskID) == "" {
		definition, err := r.createTaskDefinition(ctx, tx, command, nowMS)
		if err != nil {
			return taskdomain.Definition{}, false, err
		}
		if err := tx.Commit(); err != nil {
			return taskdomain.Definition{}, false, err
		}
		return definition, true, nil
	}

	current, err := r.getTaskDefinitionForUpdate(ctx, tx, command.TaskID)
	if err != nil {
		return taskdomain.Definition{}, false, err
	}
	next := current
	next.TaskType = command.TaskType
	next.Category = command.Category
	next.MetricType = command.MetricType
	next.Title = command.Title
	next.Description = command.Description
	next.TargetValue = command.TargetValue
	next.TargetUnit = command.TargetUnit
	next.RewardCoinAmount = command.RewardCoinAmount
	next.Status = command.Status
	next.SortOrder = command.SortOrder
	next.EffectiveFromMS = command.EffectiveFromMS
	next.EffectiveToMS = command.EffectiveToMS
	next.UpdatedByAdminID = command.OperatorAdminID
	next.UpdatedAtMS = nowMS
	if taskDefinitionNeedsVersion(current, next) {
		next.Version = current.Version + 1
		versionID, err := r.insertTaskDefinitionVersion(ctx, tx, next, nowMS)
		if err != nil {
			return taskdomain.Definition{}, false, err
		}
		next.CurrentVersionID = versionID
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE task_definitions
		SET task_type = ?, category = ?, metric_type = ?, title = ?, description = ?,
		    target_value = ?, target_unit = ?, reward_coin_amount = ?, status = ?, sort_order = ?,
		    version = ?, current_task_version_id = ?, effective_from_ms = ?, effective_to_ms = ?,
		    updated_by_admin_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND task_id = ?`,
		next.TaskType, next.Category, next.MetricType, next.Title, next.Description,
		next.TargetValue, next.TargetUnit, next.RewardCoinAmount, next.Status, next.SortOrder,
		next.Version, next.CurrentVersionID, next.EffectiveFromMS, next.EffectiveToMS,
		next.UpdatedByAdminID, next.UpdatedAtMS, appcode.FromContext(ctx), next.TaskID,
	); err != nil {
		return taskdomain.Definition{}, false, err
	}
	if err := tx.Commit(); err != nil {
		return taskdomain.Definition{}, false, err
	}
	return next, false, nil
}

// SetTaskDefinitionStatus 更新后台状态；archived 也只是不再展示，不删除历史事实。
func (r *Repository) SetTaskDefinitionStatus(ctx context.Context, taskID string, status string, operatorAdminID int64, nowMS int64) (taskdomain.Definition, error) {
	if r == nil || r.db == nil {
		return taskdomain.Definition{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	result, err := r.db.ExecContext(ctx, `
		UPDATE task_definitions
		SET status = ?, updated_by_admin_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND task_id = ?`,
		status, operatorAdminID, nowMS, appcode.FromContext(ctx), taskID,
	)
	if err != nil {
		return taskdomain.Definition{}, err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return taskdomain.Definition{}, err
	}
	if affected == 0 {
		return taskdomain.Definition{}, xerr.New(xerr.NotFound, "task definition not found")
	}
	return r.getTaskDefinition(ctx, taskID)
}

// PrepareTaskClaim 锁定完成进度并创建 pending claim；钱包调用在事务外执行。
func (r *Repository) PrepareTaskClaim(ctx context.Context, command taskdomain.ClaimCommand, nowMS int64) (taskdomain.Claim, error) {
	if r == nil || r.db == nil {
		return taskdomain.Claim{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return taskdomain.Claim{}, err
	}
	defer func() { _ = tx.Rollback() }()

	if existing, exists, err := r.getClaimByCommandForUpdate(ctx, tx, command.CommandID); err != nil || exists {
		if err != nil {
			return taskdomain.Claim{}, err
		}
		if existing.UserID != command.UserID || existing.TaskID != command.TaskID || existing.TaskType != command.TaskType || existing.CycleKey != command.CycleKey {
			return taskdomain.Claim{}, xerr.New(xerr.RequestConflict, "claim command payload conflicts")
		}
		if err := tx.Commit(); err != nil {
			return taskdomain.Claim{}, err
		}
		return existing, nil
	}

	if existing, exists, err := r.getClaimByTaskCycleForUpdate(ctx, tx, command.UserID, command.TaskID, command.CycleKey); err != nil || exists {
		if err != nil {
			return taskdomain.Claim{}, err
		}
		if existing.Status == taskdomain.ClaimStatusGranted {
			if err := tx.Commit(); err != nil {
				return taskdomain.Claim{}, err
			}
			return existing, nil
		}
		return taskdomain.Claim{}, xerr.New(xerr.RewardPending, "task reward claim is pending")
	}

	definition, err := r.getTaskDefinitionForUpdate(ctx, tx, command.TaskID)
	if err != nil {
		return taskdomain.Claim{}, err
	}
	if definition.TaskType != command.TaskType {
		return taskdomain.Claim{}, xerr.New(xerr.InvalidArgument, "task_type does not match task definition")
	}
	progress, err := r.getProgressForUpdate(ctx, tx, command.UserID, command.TaskID, command.CycleKey)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return taskdomain.Claim{}, xerr.New(xerr.Conflict, "task is not completed")
		}
		return taskdomain.Claim{}, err
	}
	if progress.Status == taskdomain.StatusClaimed {
		return taskdomain.Claim{}, xerr.New(xerr.Conflict, "task reward already claimed")
	}
	if progress.Status != taskdomain.StatusCompleted {
		return taskdomain.Claim{}, xerr.New(xerr.Conflict, "task is not completed")
	}

	claim := taskdomain.Claim{
		ClaimID:          idgen.New("tclaim"),
		CommandID:        command.CommandID,
		UserID:           command.UserID,
		TaskID:           command.TaskID,
		TaskType:         command.TaskType,
		CycleKey:         command.CycleKey,
		RewardCoinAmount: progress.RewardCoinAmount,
		Status:           taskdomain.ClaimStatusPending,
		CreatedAtMS:      nowMS,
		UpdatedAtMS:      nowMS,
	}
	claim.WalletCommandID = walletTaskCommandID(claim.ClaimID)
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO task_reward_claims (
			app_code, claim_id, command_id, user_id, task_id, task_type, cycle_key,
			reward_coin_amount, wallet_command_id, status, failure_reason, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?)`,
		appcode.FromContext(ctx), claim.ClaimID, claim.CommandID, claim.UserID, claim.TaskID, claim.TaskType,
		claim.CycleKey, claim.RewardCoinAmount, claim.WalletCommandID, claim.Status, claim.CreatedAtMS, claim.UpdatedAtMS,
	); err != nil {
		return taskdomain.Claim{}, err
	}
	if err := tx.Commit(); err != nil {
		return taskdomain.Claim{}, err
	}
	return claim, nil
}

// MarkTaskClaimGranted 把钱包成功回执和用户任务 claimed 状态放在同一 activity 事务里。
func (r *Repository) MarkTaskClaimGranted(ctx context.Context, claimID string, walletTransactionID string, grantedAtMS int64) (taskdomain.Claim, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return taskdomain.Claim{}, err
	}
	defer func() { _ = tx.Rollback() }()

	claim, err := r.getClaimByIDForUpdate(ctx, tx, claimID)
	if err != nil {
		return taskdomain.Claim{}, err
	}
	if claim.Status == taskdomain.ClaimStatusGranted {
		if err := tx.Commit(); err != nil {
			return taskdomain.Claim{}, err
		}
		return claim, nil
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE task_reward_claims
		SET status = 'granted', wallet_transaction_id = ?, failure_reason = '', updated_at_ms = ?
		WHERE app_code = ? AND claim_id = ?`,
		walletTransactionID, grantedAtMS, appcode.FromContext(ctx), claimID,
	); err != nil {
		return taskdomain.Claim{}, err
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE user_task_progress
		SET status = 'claimed', claimed_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ? AND task_id = ? AND cycle_key = ?`,
		grantedAtMS, grantedAtMS, appcode.FromContext(ctx), claim.UserID, claim.TaskID, claim.CycleKey,
	); err != nil {
		return taskdomain.Claim{}, err
	}
	if err := tx.Commit(); err != nil {
		return taskdomain.Claim{}, err
	}
	claim.Status = taskdomain.ClaimStatusGranted
	claim.WalletTransactionID = walletTransactionID
	claim.UpdatedAtMS = grantedAtMS
	return claim, nil
}

// MarkTaskClaimFailed 记录钱包失败原因但不修改 progress，用户后续可用同 command_id 重试。
func (r *Repository) MarkTaskClaimFailed(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")
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE task_reward_claims
		SET status = 'failed', failure_reason = ?, updated_at_ms = ?
		WHERE app_code = ? AND claim_id = ? AND status <> 'granted'`,
		truncateTaskFailure(failureReason), nowMS, appcode.FromContext(ctx), claimID,
	)
	return err
}

func (r *Repository) insertTaskEventConsumption(ctx context.Context, tx *sql.Tx, event taskdomain.Event, cycleKey string, nowMS int64) (bool, string, error) {
	_, err := tx.ExecContext(ctx, `
		INSERT INTO task_event_consumption (
			app_code, event_id, event_type, source_service, user_id, task_day, status,
			skip_reason, created_at_ms, consumed_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, 'consumed', '', ?, 0)`,
		appcode.FromContext(ctx), event.EventID, event.EventType, event.SourceService, event.UserID, cycleKey, nowMS,
	)
	if err == nil {
		return true, "", nil
	}
	if !isMySQLDuplicate(err) {
		return false, "", err
	}
	var status string
	if err := tx.QueryRowContext(ctx, `
		SELECT status FROM task_event_consumption
		WHERE app_code = ? AND event_id = ?`,
		appcode.FromContext(ctx), event.EventID,
	).Scan(&status); err != nil {
		return false, "", err
	}
	return false, status, nil
}

func (r *Repository) listDefinitionsForEvent(ctx context.Context, tx *sql.Tx, metricType string, occurredAtMS int64) ([]taskdomain.Definition, error) {
	rows, err := tx.QueryContext(ctx, `
		SELECT app_code, task_id, task_type, category, metric_type, title, description,
		       target_value, target_unit, reward_coin_amount, status, sort_order, version,
		       current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
		       updated_by_admin_id, created_at_ms, updated_at_ms
		FROM task_definitions
		WHERE app_code = ? AND status = 'active' AND metric_type = ?
		  AND (effective_from_ms = 0 OR effective_from_ms <= ?)
		  AND (effective_to_ms = 0 OR effective_to_ms > ?)
		ORDER BY sort_order ASC, task_id ASC
		FOR UPDATE`,
		appcode.FromContext(ctx), metricType, occurredAtMS, occurredAtMS,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanDefinitions(rows)
}

func (r *Repository) applyTaskProgressDelta(ctx context.Context, tx *sql.Tx, userID int64, definition taskdomain.Definition, cycleKey string, delta int64, nowMS int64) (bool, error) {
	progress, err := r.getProgressForUpdate(ctx, tx, userID, definition.TaskID, cycleKey)
	if err != nil {
		if !errors.Is(err, sql.ErrNoRows) {
			return false, err
		}
		status := taskdomain.StatusInProgress
		completedAtMS := int64(0)
		if delta >= definition.TargetValue {
			status = taskdomain.StatusCompleted
			completedAtMS = nowMS
		}
		_, err := tx.ExecContext(ctx, `
			INSERT INTO user_task_progress (
				app_code, user_id, task_id, task_version_id, cycle_key, progress_value,
				target_value, reward_coin_amount, status, completed_at_ms, claimed_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
			appcode.FromContext(ctx), userID, definition.TaskID, definition.CurrentVersionID, cycleKey, delta,
			definition.TargetValue, definition.RewardCoinAmount, status, completedAtMS, nowMS,
		)
		return err == nil, err
	}
	if definition.TaskType == taskdomain.TypeExclusive && (progress.Status == taskdomain.StatusCompleted || progress.Status == taskdomain.StatusClaimed) {
		return false, nil
	}
	nextValue := progress.ProgressValue + delta
	nextStatus := progress.Status
	completedAtMS := progress.CompletedAtMS
	if nextStatus == taskdomain.StatusInProgress && nextValue >= progress.TargetValue {
		nextStatus = taskdomain.StatusCompleted
		completedAtMS = nowMS
	}
	_, err = tx.ExecContext(ctx, `
		UPDATE user_task_progress
		SET progress_value = ?, status = ?, completed_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ? AND task_id = ? AND cycle_key = ?`,
		nextValue, nextStatus, completedAtMS, nowMS, appcode.FromContext(ctx), userID, definition.TaskID, cycleKey,
	)
	return err == nil, err
}

func (r *Repository) createTaskDefinition(ctx context.Context, tx *sql.Tx, command taskdomain.DefinitionCommand, nowMS int64) (taskdomain.Definition, error) {
	definition := taskdomain.Definition{
		AppCode:          appcode.FromContext(ctx),
		TaskID:           idgen.New("task"),
		TaskType:         command.TaskType,
		Category:         command.Category,
		MetricType:       command.MetricType,
		Title:            command.Title,
		Description:      command.Description,
		TargetValue:      command.TargetValue,
		TargetUnit:       command.TargetUnit,
		RewardCoinAmount: command.RewardCoinAmount,
		Status:           command.Status,
		SortOrder:        command.SortOrder,
		Version:          1,
		EffectiveFromMS:  command.EffectiveFromMS,
		EffectiveToMS:    command.EffectiveToMS,
		CreatedByAdminID: command.OperatorAdminID,
		UpdatedByAdminID: command.OperatorAdminID,
		CreatedAtMS:      nowMS,
		UpdatedAtMS:      nowMS,
	}
	versionID, err := r.insertTaskDefinitionVersion(ctx, tx, definition, nowMS)
	if err != nil {
		return taskdomain.Definition{}, err
	}
	definition.CurrentVersionID = versionID
	_, err = tx.ExecContext(ctx, `
		INSERT INTO task_definitions (
			app_code, task_id, task_type, category, metric_type, title, description,
			target_value, target_unit, reward_coin_amount, status, sort_order, version,
			current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
			updated_by_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		definition.AppCode, definition.TaskID, definition.TaskType, definition.Category, definition.MetricType,
		definition.Title, definition.Description, definition.TargetValue, definition.TargetUnit,
		definition.RewardCoinAmount, definition.Status, definition.SortOrder, definition.Version,
		definition.CurrentVersionID, definition.EffectiveFromMS, definition.EffectiveToMS,
		definition.CreatedByAdminID, definition.UpdatedByAdminID, definition.CreatedAtMS, definition.UpdatedAtMS,
	)
	return definition, err
}

func (r *Repository) insertTaskDefinitionVersion(ctx context.Context, tx *sql.Tx, definition taskdomain.Definition, nowMS int64) (int64, error) {
	payload, err := json.Marshal(definition)
	if err != nil {
		return 0, err
	}
	effectiveCycle := taskdomain.CycleLifetime
	if definition.TaskType == taskdomain.TypeDaily {
		effectiveCycle = "current"
	}
	result, err := tx.ExecContext(ctx, `
		INSERT INTO task_definition_versions (
			app_code, task_id, version, snapshot_json, effective_from_cycle, created_at_ms
		) VALUES (?, ?, ?, ?, ?, ?)`,
		appcode.FromContext(ctx), definition.TaskID, definition.Version, string(payload), effectiveCycle, nowMS,
	)
	if err != nil {
		return 0, err
	}
	return result.LastInsertId()
}

func (r *Repository) getTaskDefinition(ctx context.Context, taskID string) (taskdomain.Definition, error) {
	row := r.db.QueryRowContext(ctx, `
		SELECT app_code, task_id, task_type, category, metric_type, title, description,
		       target_value, target_unit, reward_coin_amount, status, sort_order, version,
		       current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
		       updated_by_admin_id, created_at_ms, updated_at_ms
		FROM task_definitions
		WHERE app_code = ? AND task_id = ?`,
		appcode.FromContext(ctx), taskID,
	)
	return scanDefinition(row)
}

func (r *Repository) getTaskDefinitionForUpdate(ctx context.Context, tx *sql.Tx, taskID string) (taskdomain.Definition, error) {
	row := tx.QueryRowContext(ctx, `
		SELECT app_code, task_id, task_type, category, metric_type, title, description,
		       target_value, target_unit, reward_coin_amount, status, sort_order, version,
		       current_task_version_id, effective_from_ms, effective_to_ms, created_by_admin_id,
		       updated_by_admin_id, created_at_ms, updated_at_ms
		FROM task_definitions
		WHERE app_code = ? AND task_id = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), taskID,
	)
	definition, err := scanDefinition(row)
	if errors.Is(err, sql.ErrNoRows) {
		return taskdomain.Definition{}, xerr.New(xerr.NotFound, "task definition not found")
	}
	return definition, err
}

func (r *Repository) getProgressForUpdate(ctx context.Context, tx *sql.Tx, userID int64, taskID string, cycleKey string) (taskdomain.Progress, error) {
	row := tx.QueryRowContext(ctx, `
		SELECT app_code, user_id, task_id, task_version_id, cycle_key, progress_value,
		       target_value, reward_coin_amount, status, completed_at_ms, claimed_at_ms, updated_at_ms
		FROM user_task_progress
		WHERE app_code = ? AND user_id = ? AND task_id = ? AND cycle_key = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), userID, taskID, cycleKey,
	)
	return scanProgress(row)
}

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

func (r *Repository) getClaimByTaskCycleForUpdate(ctx context.Context, tx *sql.Tx, userID int64, taskID string, cycleKey string) (taskdomain.Claim, bool, error) {
	row := tx.QueryRowContext(ctx, taskClaimSelectSQL()+`
		WHERE app_code = ? AND user_id = ? AND task_id = ? AND cycle_key = ?
		FOR UPDATE`, appcode.FromContext(ctx), userID, taskID, cycleKey)
	claim, err := scanClaim(row)
	if errors.Is(err, sql.ErrNoRows) {
		return taskdomain.Claim{}, false, nil
	}
	return claim, err == nil, err
}

func (r *Repository) getClaimByIDForUpdate(ctx context.Context, tx *sql.Tx, claimID string) (taskdomain.Claim, error) {
	row := tx.QueryRowContext(ctx, taskClaimSelectSQL()+`
		WHERE app_code = ? AND claim_id = ?
		FOR UPDATE`, appcode.FromContext(ctx), claimID)
	claim, err := scanClaim(row)
	if errors.Is(err, sql.ErrNoRows) {
		return taskdomain.Claim{}, xerr.New(xerr.NotFound, "task reward claim not found")
	}
	return claim, err
}

func scanDefinitions(rows *sql.Rows) ([]taskdomain.Definition, error) {
	definitions := make([]taskdomain.Definition, 0)
	for rows.Next() {
		definition, err := scanDefinition(rows)
		if err != nil {
			return nil, err
		}
		definitions = append(definitions, definition)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return definitions, nil
}

type rowScanner interface {
	Scan(dest ...any) error
}

func scanDefinition(row rowScanner) (taskdomain.Definition, error) {
	var definition taskdomain.Definition
	err := row.Scan(
		&definition.AppCode,
		&definition.TaskID,
		&definition.TaskType,
		&definition.Category,
		&definition.MetricType,
		&definition.Title,
		&definition.Description,
		&definition.TargetValue,
		&definition.TargetUnit,
		&definition.RewardCoinAmount,
		&definition.Status,
		&definition.SortOrder,
		&definition.Version,
		&definition.CurrentVersionID,
		&definition.EffectiveFromMS,
		&definition.EffectiveToMS,
		&definition.CreatedByAdminID,
		&definition.UpdatedByAdminID,
		&definition.CreatedAtMS,
		&definition.UpdatedAtMS,
	)
	return definition, err
}

func scanProgress(row rowScanner) (taskdomain.Progress, error) {
	var progress taskdomain.Progress
	err := row.Scan(
		&progress.AppCode,
		&progress.UserID,
		&progress.TaskID,
		&progress.TaskVersionID,
		&progress.CycleKey,
		&progress.ProgressValue,
		&progress.TargetValue,
		&progress.RewardCoinAmount,
		&progress.Status,
		&progress.CompletedAtMS,
		&progress.ClaimedAtMS,
		&progress.UpdatedAtMS,
	)
	return progress, err
}

func scanClaim(row rowScanner) (taskdomain.Claim, error) {
	var claim taskdomain.Claim
	err := row.Scan(
		&claim.ClaimID,
		&claim.CommandID,
		&claim.UserID,
		&claim.TaskID,
		&claim.TaskType,
		&claim.CycleKey,
		&claim.RewardCoinAmount,
		&claim.WalletCommandID,
		&claim.WalletTransactionID,
		&claim.Status,
		&claim.FailureReason,
		&claim.CreatedAtMS,
		&claim.UpdatedAtMS,
	)
	return claim, err
}

func taskClaimSelectSQL() string {
	return `SELECT claim_id, command_id, user_id, task_id, task_type, cycle_key, reward_coin_amount,
	               wallet_command_id, wallet_transaction_id, status, failure_reason, created_at_ms, updated_at_ms
	        FROM task_reward_claims `
}

func taskDefinitionWhere(ctx context.Context, query taskdomain.DefinitionQuery) (string, []any) {
	conditions := []string{"app_code = ?"}
	args := []any{appcode.FromContext(ctx)}
	if query.TaskType != "" {
		conditions = append(conditions, "task_type = ?")
		args = append(args, query.TaskType)
	}
	if query.Category != "" {
		conditions = append(conditions, "category = ?")
		args = append(args, query.Category)
	}
	if query.Status != "" {
		conditions = append(conditions, "status = ?")
		args = append(args, query.Status)
	}
	if query.Keyword != "" {
		conditions = append(conditions, "(task_id LIKE ? OR title LIKE ? OR metric_type LIKE ?)")
		like := "%" + query.Keyword + "%"
		args = append(args, like, like, like)
	}
	return "WHERE " + strings.Join(conditions, " AND "), args
}

func taskDefinitionNeedsVersion(current taskdomain.Definition, next taskdomain.Definition) bool {
	return current.TaskType != next.TaskType ||
		current.Category != next.Category ||
		current.MetricType != next.MetricType ||
		current.TargetValue != next.TargetValue ||
		current.TargetUnit != next.TargetUnit ||
		current.RewardCoinAmount != next.RewardCoinAmount
}

func normalizePageSize(value int32) int32 {
	if value <= 0 {
		return 20
	}
	if value > 100 {
		return 100
	}
	return value
}

func normalizeCycleKeys(values []string) []string {
	seen := make(map[string]bool, len(values))
	result := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" || seen[value] {
			continue
		}
		seen[value] = true
		result = append(result, value)
	}
	return result
}

func walletTaskCommandID(claimID string) string {
	return fmt.Sprintf("wtask_%s", claimID)
}

func truncateTaskFailure(value string) string {
	value = strings.TrimSpace(value)
	if len(value) <= 255 {
		return value
	}
	return value[:255]
}
