package mysql

import (
	"context"
	"database/sql"
	"strings"
	"time"

	"hyapp/pkg/activitymq"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
)

const (
	activityParticipantScopeSummary    = "summary"
	activityParticipantScopeDay        = "day"
	activityParticipantScopeDayRegion  = "day_region"
	activityParticipantScopeGift       = "gift"
	activityParticipantScopeGiftRegion = "gift_region"
	activityParticipantScopeTask       = "task"
	activityParticipantScopeTaskRegion = "task_region"
	activityParticipantSummaryDay      = "1970-01-01"
	activityDimensionLimit             = 100
)

const activityTemplateGiftDayUpsert = `
	INSERT INTO stat_activity_template_gift_day (
	  app_code, template_id, template_code, published_version, stat_day, region_id, gift_id,
	  participant_count, gift_event_count, gift_count, coin_amount, updated_at_ms
	) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
	ON DUPLICATE KEY UPDATE
	  template_code = VALUES(template_code), participant_count = participant_count + VALUES(participant_count),
	  gift_event_count = gift_event_count + 1, gift_count = gift_count + VALUES(gift_count),
	  coin_amount = coin_amount + VALUES(coin_amount), updated_at_ms = VALUES(updated_at_ms)`

// ActivityTemplateFactEvent aliases the owner MQ contract so replay jobs and
// online consumers cannot accidentally evolve two independent payload shapes.
type ActivityTemplateFactEvent = activitymq.ActivityTemplateFactMessage

type ActivityTemplateDataQuery struct {
	AppCode          string
	TemplateID       string
	PublishedVersion int64
	RegionID         int64
	StartMS          int64
	EndMS            int64
}

type ActivityTemplateOverview struct {
	TemplateID       string `json:"template_id"`
	TemplateCode     string `json:"template_code"`
	PublishedVersion int64  `json:"published_version"`
	Participants     int64  `json:"participants"`
	VisitCount       int64  `json:"visit_count"`
	GiftUsers        int64  `json:"gift_users"`
	TaskUsers        int64  `json:"task_users"`
	GiftEventCount   int64  `json:"gift_event_count"`
	GiftCount        int64  `json:"gift_count"`
	CoinAmount       int64  `json:"coin_amount"`
	TaskProgress     int64  `json:"task_progress"`
	TaskCompleted    int64  `json:"task_completed"`
	TaskClaimed      int64  `json:"task_claimed"`
}

type ActivityTemplateTrendPoint struct {
	StatDay        string `json:"stat_day"`
	Participants   int64  `json:"participants"`
	VisitCount     int64  `json:"visit_count"`
	GiftUsers      int64  `json:"gift_users"`
	TaskUsers      int64  `json:"task_users"`
	GiftEventCount int64  `json:"gift_event_count"`
	GiftCount      int64  `json:"gift_count"`
	CoinAmount     int64  `json:"coin_amount"`
	TaskProgress   int64  `json:"task_progress"`
	TaskCompleted  int64  `json:"task_completed"`
	TaskClaimed    int64  `json:"task_claimed"`
}

type ActivityTemplateGiftStat struct {
	GiftID         string `json:"gift_id"`
	GiftUsers      int64  `json:"gift_users"`
	GiftEventCount int64  `json:"gift_event_count"`
	GiftCount      int64  `json:"gift_count"`
	CoinAmount     int64  `json:"coin_amount"`
}

type ActivityTemplateTaskStat struct {
	TaskKey       string `json:"task_key"`
	TaskType      string `json:"task_type"`
	TaskUsers     int64  `json:"task_users"`
	TaskProgress  int64  `json:"task_progress"`
	TaskCompleted int64  `json:"task_completed"`
	TaskClaimed   int64  `json:"task_claimed"`
}

type ActivityTemplateData struct {
	Overview       ActivityTemplateOverview     `json:"overview"`
	Trend          []ActivityTemplateTrendPoint `json:"trend"`
	Gifts          []ActivityTemplateGiftStat   `json:"gifts"`
	Tasks          []ActivityTemplateTaskStat   `json:"tasks"`
	Granularity    string                       `json:"granularity"`
	Timezone       string                       `json:"timezone"`
	StartMS        int64                        `json:"start_ms"`
	EndMS          int64                        `json:"end_ms"`
	RegionID       int64                        `json:"region_id"`
	DimensionLimit int                          `json:"dimension_limit"`
	GiftSort       string                       `json:"gift_sort"`
	TaskSort       string                       `json:"task_sort"`
	ServerTimeMS   int64                        `json:"server_time_ms"`
}

func activityTemplateStatSchemaStatements() []string {
	return []string{
		`CREATE TABLE IF NOT EXISTS stat_activity_template_summary (
		  app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, template_code VARCHAR(64) NOT NULL,
		  published_version BIGINT NOT NULL, participant_count BIGINT NOT NULL DEFAULT 0, visit_event_count BIGINT NOT NULL DEFAULT 0,
		  gift_event_count BIGINT NOT NULL DEFAULT 0, gift_count BIGINT NOT NULL DEFAULT 0, coin_amount BIGINT NOT NULL DEFAULT 0,
		  progress_value BIGINT NOT NULL DEFAULT 0, completed_count BIGINT NOT NULL DEFAULT 0, claimed_count BIGINT NOT NULL DEFAULT 0,
		  first_occurred_at_ms BIGINT NOT NULL DEFAULT 0, last_occurred_at_ms BIGINT NOT NULL DEFAULT 0, updated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, template_id, published_version),
		  KEY idx_stat_activity_template_summary_code (app_code, template_code, published_version)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS stat_activity_template_day (
		  app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, template_code VARCHAR(64) NOT NULL,
		  published_version BIGINT NOT NULL, stat_day DATE NOT NULL, region_id BIGINT NOT NULL DEFAULT 0,
		  participant_count BIGINT NOT NULL DEFAULT 0, visit_event_count BIGINT NOT NULL DEFAULT 0, gift_event_count BIGINT NOT NULL DEFAULT 0,
		  gift_count BIGINT NOT NULL DEFAULT 0, coin_amount BIGINT NOT NULL DEFAULT 0,
		  progress_value BIGINT NOT NULL DEFAULT 0, completed_count BIGINT NOT NULL DEFAULT 0, claimed_count BIGINT NOT NULL DEFAULT 0,
		  updated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, template_id, published_version, stat_day, region_id),
		  KEY idx_stat_activity_template_day_code (app_code, template_code, published_version, stat_day),
		  KEY idx_stat_activity_template_day_region (app_code, template_id, published_version, region_id, stat_day)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS stat_activity_template_gift_day (
		  app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, template_code VARCHAR(64) NOT NULL,
		  published_version BIGINT NOT NULL, stat_day DATE NOT NULL, region_id BIGINT NOT NULL DEFAULT 0, gift_id VARCHAR(96) NOT NULL,
		  participant_count BIGINT NOT NULL DEFAULT 0, gift_event_count BIGINT NOT NULL DEFAULT 0,
		  gift_count BIGINT NOT NULL DEFAULT 0, coin_amount BIGINT NOT NULL DEFAULT 0, updated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, template_id, published_version, stat_day, region_id, gift_id),
		  KEY idx_stat_activity_template_gift_rank (app_code, template_id, published_version, stat_day, coin_amount, gift_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS stat_activity_template_task_day (
		  app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, template_code VARCHAR(64) NOT NULL,
		  published_version BIGINT NOT NULL, stat_day DATE NOT NULL, region_id BIGINT NOT NULL DEFAULT 0,
		  task_key VARCHAR(96) NOT NULL, task_type VARCHAR(64) NOT NULL,
		  participant_count BIGINT NOT NULL DEFAULT 0, progress_value BIGINT NOT NULL DEFAULT 0,
		  completed_count BIGINT NOT NULL DEFAULT 0, claimed_count BIGINT NOT NULL DEFAULT 0, updated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, template_id, published_version, stat_day, region_id, task_key),
		  KEY idx_stat_activity_template_task_rank (app_code, template_id, published_version, stat_day, completed_count, task_key)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
		`CREATE TABLE IF NOT EXISTS stat_activity_template_participants (
		  app_code VARCHAR(32) NOT NULL, template_id VARCHAR(96) NOT NULL, published_version BIGINT NOT NULL,
		  scope_type VARCHAR(20) NOT NULL, stat_day DATE NOT NULL, region_id BIGINT NOT NULL DEFAULT 0,
		  dimension_key VARCHAR(128) NOT NULL DEFAULT '', user_id BIGINT NOT NULL, first_participated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, template_id, published_version, scope_type, stat_day, region_id, dimension_key, user_id),
		  KEY idx_stat_activity_participant_dimension (app_code, template_id, published_version, scope_type, dimension_key, stat_day, user_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
	}
}

func (r *Repository) migrateActivityTemplateStats(ctx context.Context) error {
	for _, statement := range activityTemplateStatSchemaStatements() {
		if _, err := r.db.ExecContext(ctx, statement); err != nil {
			return err
		}
	}
	if err := r.ensureActivityTemplateColumn(ctx, "stat_activity_template_summary", "visit_event_count", "visit_event_count BIGINT NOT NULL DEFAULT 0 AFTER participant_count"); err != nil {
		return err
	}
	if err := r.ensureActivityTemplateColumn(ctx, "stat_activity_template_day", "visit_event_count", "visit_event_count BIGINT NOT NULL DEFAULT 0 AFTER participant_count"); err != nil {
		return err
	}
	return nil
}

func (r *Repository) ensureActivityTemplateColumn(ctx context.Context, tableName string, columnName string, columnDDL string) error {
	var count int
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*)
		   FROM information_schema.COLUMNS
		  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
		tableName, columnName,
	).Scan(&count); err != nil {
		return err
	}
	if count > 0 {
		return nil
	}
	// MySQL does not support ADD COLUMN IF NOT EXISTS across the versions used by
	// production and local Compose. These identifiers are fixed owner-schema constants;
	// the metadata check keeps repeated startup migrations idempotent without accepting SQL input.
	_, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` ADD COLUMN `+columnDDL)
	return err
}

// ConsumeActivityTemplateFact projects one immutable fact and all participant
// uniqueness scopes in one transaction. Event consumption is written first, so
// RocketMQ redelivery cannot inflate either counters or unique participants.
func (r *Repository) ConsumeActivityTemplateFact(ctx context.Context, event ActivityTemplateFactEvent) error {
	app := appcode.Normalize(event.AppCode)
	event.AppCode = app
	event.EventID = strings.TrimSpace(event.EventID)
	event.EventType = strings.TrimSpace(event.EventType)
	event.TemplateID = strings.TrimSpace(event.TemplateID)
	event.TemplateCode = strings.TrimSpace(event.TemplateCode)
	event.GiftID = strings.TrimSpace(event.GiftID)
	event.TaskKey = strings.TrimSpace(event.TaskKey)
	event.TaskType = strings.TrimSpace(event.TaskType)
	if _, err := activitymq.EncodeActivityTemplateFactMessage(event); err != nil {
		return xerr.New(xerr.InvalidArgument, err.Error())
	}
	ctx = appcode.WithContext(ctx, app)
	return r.withEvent(ctx, SourceActivity, event.EventID, event.EventType, func(tx *sql.Tx, nowMS int64) error {
		return r.applyActivityTemplateFact(ctx, tx, event, nowMS)
	})
}

func (r *Repository) applyActivityTemplateFact(ctx context.Context, tx *sql.Tx, event ActivityTemplateFactEvent, nowMS int64) error {
	giftEventCount := int64(0)
	if event.EventType == activitymq.EventTypeGiftScored {
		giftEventCount = 1
	}

	// summary uniqueness has no time or region dimension; it remains the exact
	// lifetime participant count even when one user appears in many daily rows.
	summaryParticipant, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeSummary, activityParticipantSummaryDay, 0, "")
	if err != nil {
		return err
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO stat_activity_template_summary (
		  app_code, template_id, template_code, published_version, participant_count,
		  visit_event_count, gift_event_count, gift_count, coin_amount, progress_value, completed_count, claimed_count,
		  first_occurred_at_ms, last_occurred_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
		  template_code = VALUES(template_code), participant_count = participant_count + VALUES(participant_count),
		  visit_event_count = visit_event_count + VALUES(visit_event_count),
		  gift_event_count = gift_event_count + VALUES(gift_event_count), gift_count = gift_count + VALUES(gift_count),
		  coin_amount = coin_amount + VALUES(coin_amount), progress_value = progress_value + VALUES(progress_value),
		  completed_count = completed_count + VALUES(completed_count), claimed_count = claimed_count + VALUES(claimed_count),
		  first_occurred_at_ms = IF(first_occurred_at_ms = 0, VALUES(first_occurred_at_ms), LEAST(first_occurred_at_ms, VALUES(first_occurred_at_ms))),
		  last_occurred_at_ms = GREATEST(last_occurred_at_ms, VALUES(last_occurred_at_ms)), updated_at_ms = VALUES(updated_at_ms)
	`, event.AppCode, event.TemplateID, event.TemplateCode, event.PublishedVersion, summaryParticipant,
		event.VisitCountDelta, giftEventCount, event.GiftCountDelta, event.CoinAmountDelta, event.ProgressDelta, event.CompletedDelta, event.ClaimedDelta,
		event.OccurredAtMS, event.OccurredAtMS, nowMS); err != nil {
		return err
	}

	// A global day scope supports exact cross-region query counts; the regional
	// scope independently increments the corresponding regional aggregate row.
	if _, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeDay, event.StatDay, 0, ""); err != nil {
		return err
	}
	dayParticipant, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeDayRegion, event.StatDay, event.RegionID, "")
	if err != nil {
		return err
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO stat_activity_template_day (
		  app_code, template_id, template_code, published_version, stat_day, region_id, participant_count,
		  visit_event_count, gift_event_count, gift_count, coin_amount, progress_value, completed_count, claimed_count, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
		  template_code = VALUES(template_code), participant_count = participant_count + VALUES(participant_count),
		  visit_event_count = visit_event_count + VALUES(visit_event_count),
		  gift_event_count = gift_event_count + VALUES(gift_event_count), gift_count = gift_count + VALUES(gift_count),
		  coin_amount = coin_amount + VALUES(coin_amount), progress_value = progress_value + VALUES(progress_value),
		  completed_count = completed_count + VALUES(completed_count), claimed_count = claimed_count + VALUES(claimed_count),
		  updated_at_ms = VALUES(updated_at_ms)
	`, event.AppCode, event.TemplateID, event.TemplateCode, event.PublishedVersion, event.StatDay, event.RegionID, dayParticipant,
		event.VisitCountDelta, giftEventCount, event.GiftCountDelta, event.CoinAmountDelta, event.ProgressDelta, event.CompletedDelta, event.ClaimedDelta, nowMS); err != nil {
		return err
	}

	if event.EventType == activitymq.EventTypeGiftScored && event.GiftID != "" {
		if _, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeGift, event.StatDay, 0, event.GiftID); err != nil {
			return err
		}
		giftParticipant, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeGiftRegion, event.StatDay, event.RegionID, event.GiftID)
		if err != nil {
			return err
		}
		if _, err := tx.ExecContext(ctx, activityTemplateGiftDayUpsert,
			event.AppCode, event.TemplateID, event.TemplateCode, event.PublishedVersion, event.StatDay, event.RegionID, event.GiftID,
			giftParticipant, event.GiftCountDelta, event.CoinAmountDelta, nowMS); err != nil {
			return err
		}
	}

	if (event.EventType == activitymq.EventTypeTaskProgressed || event.EventType == activitymq.EventTypeTaskClaimed) && event.TaskKey != "" {
		if _, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeTask, event.StatDay, 0, event.TaskKey); err != nil {
			return err
		}
		taskParticipant, err := r.insertActivityTemplateParticipant(ctx, tx, event, activityParticipantScopeTaskRegion, event.StatDay, event.RegionID, event.TaskKey)
		if err != nil {
			return err
		}
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO stat_activity_template_task_day (
			  app_code, template_id, template_code, published_version, stat_day, region_id, task_key, task_type,
			  participant_count, progress_value, completed_count, claimed_count, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
			ON DUPLICATE KEY UPDATE
			  template_code = VALUES(template_code), task_type = VALUES(task_type), participant_count = participant_count + VALUES(participant_count),
			  progress_value = progress_value + VALUES(progress_value), completed_count = completed_count + VALUES(completed_count),
			  claimed_count = claimed_count + VALUES(claimed_count), updated_at_ms = VALUES(updated_at_ms)
		`, event.AppCode, event.TemplateID, event.TemplateCode, event.PublishedVersion, event.StatDay, event.RegionID, event.TaskKey, event.TaskType,
			taskParticipant, event.ProgressDelta, event.CompletedDelta, event.ClaimedDelta, nowMS); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) insertActivityTemplateParticipant(
	ctx context.Context,
	tx *sql.Tx,
	event ActivityTemplateFactEvent,
	scopeType string,
	statDay string,
	regionID int64,
	dimensionKey string,
) (int64, error) {
	return insertUnique(ctx, tx, `
		INSERT IGNORE INTO stat_activity_template_participants (
		  app_code, template_id, published_version, scope_type, stat_day, region_id, dimension_key, user_id, first_participated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
	`, event.AppCode, event.TemplateID, event.PublishedVersion, scopeType, statDay, regionID, dimensionKey, event.UserID, event.OccurredAtMS)
}

// QueryActivityTemplateData reads only statistics-owned aggregate/uniqueness
// projections. start_ms/end_ms use the service-wide UTC [start,end) contract.
// Both boundaries must be UTC 00:00 because a day aggregate cannot truthfully
// answer a partial-day range; rejecting it prevents silently over-reporting data.
func (r *Repository) QueryActivityTemplateData(ctx context.Context, query ActivityTemplateDataQuery) (ActivityTemplateData, error) {
	query.AppCode = appcode.Normalize(query.AppCode)
	query.TemplateID = strings.TrimSpace(query.TemplateID)
	if r == nil || r.db == nil {
		return ActivityTemplateData{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if query.AppCode == "" || query.TemplateID == "" || query.PublishedVersion <= 0 || query.RegionID < 0 || query.StartMS <= 0 || query.EndMS <= query.StartMS {
		return ActivityTemplateData{}, xerr.New(xerr.InvalidArgument, "app_code, template_id, published_version and a valid time range are required")
	}
	if !isUTCActivityDayBoundary(query.StartMS) || !isUTCActivityDayBoundary(query.EndMS) {
		return ActivityTemplateData{}, xerr.New(xerr.InvalidArgument, "start_ms and end_ms must be UTC 00:00 day boundaries")
	}
	startDay := time.UnixMilli(query.StartMS).UTC().Format("2006-01-02")
	endDay := time.UnixMilli(query.EndMS - 1).UTC().Format("2006-01-02")
	result := ActivityTemplateData{
		Overview:       ActivityTemplateOverview{TemplateID: query.TemplateID, PublishedVersion: query.PublishedVersion},
		Trend:          make([]ActivityTemplateTrendPoint, 0),
		Gifts:          make([]ActivityTemplateGiftStat, 0),
		Tasks:          make([]ActivityTemplateTaskStat, 0),
		Granularity:    "day",
		Timezone:       "UTC",
		StartMS:        query.StartMS,
		EndMS:          query.EndMS,
		RegionID:       query.RegionID,
		DimensionLimit: activityDimensionLimit,
		GiftSort:       "coin_amount_desc,gift_count_desc,gift_id_asc",
		TaskSort:       "task_completed_desc,task_claimed_desc,task_key_asc",
		ServerTimeMS:   time.Now().UTC().UnixMilli(),
	}

	if err := r.db.QueryRowContext(ctx, `
		SELECT COALESCE(MAX(template_code), ''), COALESCE(SUM(visit_event_count), 0), COALESCE(SUM(gift_event_count), 0), COALESCE(SUM(gift_count), 0),
		       COALESCE(SUM(coin_amount), 0), COALESCE(SUM(progress_value), 0),
		       COALESCE(SUM(completed_count), 0), COALESCE(SUM(claimed_count), 0)
		FROM stat_activity_template_day
		WHERE app_code = ? AND template_id = ? AND published_version = ? AND stat_day BETWEEN ? AND ?
		  AND (? = 0 OR region_id = ?)
	`, query.AppCode, query.TemplateID, query.PublishedVersion, startDay, endDay, query.RegionID, query.RegionID).Scan(
		&result.Overview.TemplateCode, &result.Overview.VisitCount, &result.Overview.GiftEventCount, &result.Overview.GiftCount,
		&result.Overview.CoinAmount, &result.Overview.TaskProgress, &result.Overview.TaskCompleted, &result.Overview.TaskClaimed,
	); err != nil {
		return ActivityTemplateData{}, err
	}
	dayScope, giftScope, taskScope, participantRegionID := activityParticipantQueryScopes(query.RegionID)
	if err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(DISTINCT CASE WHEN scope_type = ? THEN user_id END),
		       COUNT(DISTINCT CASE WHEN scope_type = ? THEN user_id END),
		       COUNT(DISTINCT CASE WHEN scope_type = ? THEN user_id END)
		FROM stat_activity_template_participants
		WHERE app_code = ? AND template_id = ? AND published_version = ?
		  AND scope_type IN (?, ?, ?) AND region_id = ? AND stat_day BETWEEN ? AND ?
	`, dayScope, giftScope, taskScope,
		query.AppCode, query.TemplateID, query.PublishedVersion,
		dayScope, giftScope, taskScope, participantRegionID, startDay, endDay,
	).Scan(&result.Overview.Participants, &result.Overview.GiftUsers, &result.Overview.TaskUsers); err != nil {
		return ActivityTemplateData{}, err
	}

	days, err := r.queryActivityTemplateDays(ctx, query, startDay, endDay)
	if err != nil {
		return ActivityTemplateData{}, err
	}
	result.Trend = days
	gifts, err := r.queryActivityTemplateGifts(ctx, query, startDay, endDay)
	if err != nil {
		return ActivityTemplateData{}, err
	}
	result.Gifts = gifts
	tasks, err := r.queryActivityTemplateTasks(ctx, query, startDay, endDay)
	if err != nil {
		return ActivityTemplateData{}, err
	}
	result.Tasks = tasks
	return result, nil
}

func isUTCActivityDayBoundary(ms int64) bool {
	if ms <= 0 {
		return false
	}
	timestamp := time.UnixMilli(ms).UTC()
	return timestamp.Hour() == 0 && timestamp.Minute() == 0 && timestamp.Second() == 0 && timestamp.Nanosecond() == 0
}

func activityParticipantQueryScopes(regionID int64) (dayScope, giftScope, taskScope string, participantRegionID int64) {
	if regionID > 0 {
		return activityParticipantScopeDayRegion, activityParticipantScopeGiftRegion, activityParticipantScopeTaskRegion, regionID
	}
	return activityParticipantScopeDay, activityParticipantScopeGift, activityParticipantScopeTask, 0
}

func (r *Repository) queryActivityTemplateDays(ctx context.Context, query ActivityTemplateDataQuery, startDay, endDay string) ([]ActivityTemplateTrendPoint, error) {
	dayScope, giftScope, taskScope, participantRegionID := activityParticipantQueryScopes(query.RegionID)
	rows, err := r.db.QueryContext(ctx, `
		SELECT d.stat_day,
		       COALESCE((SELECT COUNT(*) FROM stat_activity_template_participants p
		         WHERE p.app_code = d.app_code AND p.template_id = d.template_id AND p.published_version = d.published_version
		           AND p.scope_type = ? AND p.region_id = ? AND p.stat_day = d.stat_day), 0),
		       COALESCE((SELECT COUNT(DISTINCT p.user_id) FROM stat_activity_template_participants p
		         WHERE p.app_code = d.app_code AND p.template_id = d.template_id AND p.published_version = d.published_version
		           AND p.scope_type = ? AND p.region_id = ? AND p.stat_day = d.stat_day), 0),
		       COALESCE((SELECT COUNT(DISTINCT p.user_id) FROM stat_activity_template_participants p
		         WHERE p.app_code = d.app_code AND p.template_id = d.template_id AND p.published_version = d.published_version
		           AND p.scope_type = ? AND p.region_id = ? AND p.stat_day = d.stat_day), 0),
		       SUM(d.visit_event_count), SUM(d.gift_event_count), SUM(d.gift_count), SUM(d.coin_amount), SUM(d.progress_value),
		       SUM(d.completed_count), SUM(d.claimed_count)
		FROM stat_activity_template_day d
		WHERE d.app_code = ? AND d.template_id = ? AND d.published_version = ? AND d.stat_day BETWEEN ? AND ?
		  AND (? = 0 OR d.region_id = ?)
		GROUP BY d.app_code, d.template_id, d.published_version, d.stat_day
		ORDER BY d.stat_day ASC
	`, dayScope, participantRegionID, giftScope, participantRegionID, taskScope, participantRegionID,
		query.AppCode, query.TemplateID, query.PublishedVersion, startDay, endDay, query.RegionID, query.RegionID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]ActivityTemplateTrendPoint, 0)
	for rows.Next() {
		var item ActivityTemplateTrendPoint
		var statDay time.Time
		if err := rows.Scan(&statDay, &item.Participants, &item.GiftUsers, &item.TaskUsers,
			&item.VisitCount, &item.GiftEventCount, &item.GiftCount, &item.CoinAmount,
			&item.TaskProgress, &item.TaskCompleted, &item.TaskClaimed); err != nil {
			return nil, err
		}
		item.StatDay = statDay.UTC().Format("2006-01-02")
		out = append(out, item)
	}
	return out, rows.Err()
}

func (r *Repository) queryActivityTemplateGifts(ctx context.Context, query ActivityTemplateDataQuery, startDay, endDay string) ([]ActivityTemplateGiftStat, error) {
	_, giftScope, _, participantRegionID := activityParticipantQueryScopes(query.RegionID)
	rows, err := r.db.QueryContext(ctx, `
		SELECT g.gift_id,
		       COALESCE((SELECT COUNT(DISTINCT p.user_id) FROM stat_activity_template_participants p
		         WHERE p.app_code = g.app_code AND p.template_id = g.template_id AND p.published_version = g.published_version
		           AND p.scope_type = ? AND p.region_id = ? AND p.dimension_key = g.gift_id AND p.stat_day BETWEEN ? AND ?), 0),
		       SUM(g.gift_event_count), SUM(g.gift_count), SUM(g.coin_amount)
		FROM stat_activity_template_gift_day g
		WHERE g.app_code = ? AND g.template_id = ? AND g.published_version = ? AND g.stat_day BETWEEN ? AND ?
		  AND (? = 0 OR g.region_id = ?)
		GROUP BY g.app_code, g.template_id, g.published_version, g.gift_id
		ORDER BY SUM(g.coin_amount) DESC, SUM(g.gift_count) DESC, g.gift_id ASC
		LIMIT ?
	`, giftScope, participantRegionID, startDay, endDay, query.AppCode, query.TemplateID, query.PublishedVersion,
		startDay, endDay, query.RegionID, query.RegionID, activityDimensionLimit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]ActivityTemplateGiftStat, 0)
	for rows.Next() {
		var item ActivityTemplateGiftStat
		if err := rows.Scan(&item.GiftID, &item.GiftUsers, &item.GiftEventCount, &item.GiftCount, &item.CoinAmount); err != nil {
			return nil, err
		}
		out = append(out, item)
	}
	return out, rows.Err()
}

func (r *Repository) queryActivityTemplateTasks(ctx context.Context, query ActivityTemplateDataQuery, startDay, endDay string) ([]ActivityTemplateTaskStat, error) {
	_, _, taskScope, participantRegionID := activityParticipantQueryScopes(query.RegionID)
	rows, err := r.db.QueryContext(ctx, `
		SELECT t.task_key, MAX(t.task_type),
		       COALESCE((SELECT COUNT(DISTINCT p.user_id) FROM stat_activity_template_participants p
		         WHERE p.app_code = t.app_code AND p.template_id = t.template_id AND p.published_version = t.published_version
		           AND p.scope_type = ? AND p.region_id = ? AND p.dimension_key = t.task_key AND p.stat_day BETWEEN ? AND ?), 0),
		       SUM(t.progress_value), SUM(t.completed_count), SUM(t.claimed_count)
		FROM stat_activity_template_task_day t
		WHERE t.app_code = ? AND t.template_id = ? AND t.published_version = ? AND t.stat_day BETWEEN ? AND ?
		  AND (? = 0 OR t.region_id = ?)
		GROUP BY t.app_code, t.template_id, t.published_version, t.task_key
		ORDER BY SUM(t.completed_count) DESC, SUM(t.claimed_count) DESC, t.task_key ASC
		LIMIT ?
	`, taskScope, participantRegionID, startDay, endDay, query.AppCode, query.TemplateID, query.PublishedVersion,
		startDay, endDay, query.RegionID, query.RegionID, activityDimensionLimit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]ActivityTemplateTaskStat, 0)
	for rows.Next() {
		var item ActivityTemplateTaskStat
		if err := rows.Scan(&item.TaskKey, &item.TaskType, &item.TaskUsers, &item.TaskProgress, &item.TaskCompleted, &item.TaskClaimed); err != nil {
			return nil, err
		}
		out = append(out, item)
	}
	return out, rows.Err()
}
