package mysql

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

	mysqlDriver "github.com/go-sql-driver/mysql"
	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	domain "hyapp/services/activity-service/internal/domain/activitytemplate"
)

const activityTemplateSelect = `
	SELECT app_code, template_id, template_code, name, activity_type, status, start_ms, end_ms, all_regions,
	       daily_leaderboard_size, total_leaderboard_size, CAST(display_config_json AS CHAR), revision, published_version,
	       created_by_admin_id, updated_by_admin_id, published_by_admin_id, created_at_ms, updated_at_ms,
	       published_at_ms, archived_at_ms
	FROM activity_templates`

type activityTemplateQuerier interface {
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

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

// ListActivityTemplates 查询轻量列表投影，只额外附加区域 ID；大段规则、任务和素材只在详情接口读取。
func (r *Repository) ListActivityTemplates(ctx context.Context, query domain.ListQuery) ([]domain.Summary, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	where, args := activityTemplateListWhere(ctx, query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM activity_templates t `+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	pageSize := normalizePageSize(query.PageSize)
	page := query.Page
	if page <= 0 {
		page = 1
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT t.app_code, t.template_id, t.template_code, t.name, t.activity_type, t.status, t.start_ms, t.end_ms,
		       t.all_regions, t.revision, t.published_version, t.created_by_admin_id, t.updated_by_admin_id,
		       t.created_at_ms, t.updated_at_ms
		FROM activity_templates t `+where+`
		ORDER BY t.updated_at_ms DESC, t.template_id DESC
		LIMIT ? OFFSET ?`, append(args, int(pageSize), int((page-1)*pageSize))...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	items := make([]domain.Summary, 0)
	for rows.Next() {
		var item domain.Summary
		if err := rows.Scan(
			&item.AppCode, &item.TemplateID, &item.TemplateCode, &item.Name, &item.ActivityType, &item.Status,
			&item.StartMS, &item.EndMS, &item.AllRegions, &item.Revision, &item.PublishedVersion,
			&item.CreatedByAdminID, &item.UpdatedByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS,
		); err != nil {
			return nil, 0, err
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return nil, 0, err
	}
	if err := attachActivityTemplateSummaryRegions(ctx, r.db, items); err != nil {
		return nil, 0, err
	}
	return items, total, nil
}

func (r *Repository) GetActivityTemplate(ctx context.Context, templateID string) (domain.Template, error) {
	if r == nil || r.db == nil {
		return domain.Template{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	return getActivityTemplateAggregate(ctx, r.db, appcode.FromContext(ctx), strings.TrimSpace(templateID), false)
}

func (r *Repository) CreateActivityTemplate(ctx context.Context, item domain.Template, operatorAdminID int64, nowMS int64) (domain.Template, error) {
	if r == nil || r.db == nil {
		return domain.Template{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.Template{}, err
	}
	defer tx.Rollback()
	item.AppCode = appcode.FromContext(ctx)
	item.TemplateID = idgen.New("acttpl")
	item.Status = domain.StatusDraft
	item.Revision = 1
	item.PublishedVersion = 0
	item.CreatedByAdminID = operatorAdminID
	item.UpdatedByAdminID = operatorAdminID
	item.PublishedByAdminID = 0
	item.CreatedAtMS = nowMS
	item.UpdatedAtMS = nowMS
	item.PublishedAtMS = 0
	item.ArchivedAtMS = 0
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO activity_templates (
		  app_code, template_id, template_code, name, activity_type, status, start_ms, end_ms, all_regions,
		  daily_leaderboard_size, total_leaderboard_size, display_config_json, revision, published_version,
		  created_by_admin_id, updated_by_admin_id, published_by_admin_id, created_at_ms, updated_at_ms, published_at_ms, archived_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		item.AppCode, item.TemplateID, item.TemplateCode, item.Name, item.ActivityType, item.Status, item.StartMS, item.EndMS, item.AllRegions,
		item.DailyLeaderboardSize, item.TotalLeaderboardSize, item.DisplayConfigJSON, item.Revision, item.PublishedVersion,
		item.CreatedByAdminID, item.UpdatedByAdminID, item.PublishedByAdminID, item.CreatedAtMS, item.UpdatedAtMS, item.PublishedAtMS, item.ArchivedAtMS,
	); err != nil {
		return domain.Template{}, activityTemplateWriteError(err)
	}
	if err := replaceActivityTemplateChildren(ctx, tx, item, nowMS); err != nil {
		return domain.Template{}, activityTemplateWriteError(err)
	}
	if err := tx.Commit(); err != nil {
		return domain.Template{}, activityTemplateWriteError(err)
	}
	return r.GetActivityTemplate(ctx, item.TemplateID)
}

func (r *Repository) UpdateActivityTemplate(ctx context.Context, command domain.UpdateCommand, nowMS int64) (domain.Template, error) {
	if r == nil || r.db == nil {
		return domain.Template{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.Template{}, err
	}
	defer tx.Rollback()
	appCode := appcode.FromContext(ctx)
	current, err := getActivityTemplateAggregate(ctx, tx, appCode, command.Template.TemplateID, true)
	if err != nil {
		return domain.Template{}, err
	}
	if current.Revision != command.ExpectedRevision {
		return domain.Template{}, xerr.New(xerr.Conflict, "activity template revision changed")
	}
	// Repository 再做一次身份防线，避免未来新增调用方绕过 service 直接改写 deep-link/runtime code。
	if command.Template.TemplateCode != current.TemplateCode {
		return domain.Template{}, xerr.New(xerr.Conflict, "activity template_code is immutable")
	}
	if current.Status == domain.StatusArchived {
		return domain.Template{}, xerr.New(xerr.Conflict, "archived activity template is read-only")
	}
	item := command.Template
	item.AppCode = appCode
	item.TemplateID = current.TemplateID
	item.Status = current.Status
	item.Revision = current.Revision + 1
	item.PublishedVersion = current.PublishedVersion
	item.CreatedByAdminID = current.CreatedByAdminID
	item.CreatedAtMS = current.CreatedAtMS
	item.UpdatedByAdminID = command.OperatorAdminID
	item.UpdatedAtMS = nowMS
	item.PublishedByAdminID = current.PublishedByAdminID
	item.PublishedAtMS = current.PublishedAtMS
	item.ArchivedAtMS = current.ArchivedAtMS
	result, err := tx.ExecContext(ctx, `
		UPDATE activity_templates
		SET name = ?, activity_type = ?, start_ms = ?, end_ms = ?, all_regions = ?,
		    daily_leaderboard_size = ?, total_leaderboard_size = ?, display_config_json = CAST(? AS JSON),
		    revision = ?, updated_by_admin_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND template_id = ? AND revision = ?`,
		item.Name, item.ActivityType, item.StartMS, item.EndMS, item.AllRegions,
		item.DailyLeaderboardSize, item.TotalLeaderboardSize, item.DisplayConfigJSON,
		item.Revision, item.UpdatedByAdminID, item.UpdatedAtMS,
		appCode, item.TemplateID, command.ExpectedRevision)
	if err != nil {
		return domain.Template{}, activityTemplateWriteError(err)
	}
	affected, _ := result.RowsAffected()
	if affected != 1 {
		return domain.Template{}, xerr.New(xerr.Conflict, "activity template revision changed")
	}
	if err := replaceActivityTemplateChildren(ctx, tx, item, nowMS); err != nil {
		return domain.Template{}, activityTemplateWriteError(err)
	}
	if err := tx.Commit(); err != nil {
		return domain.Template{}, activityTemplateWriteError(err)
	}
	return r.GetActivityTemplate(ctx, item.TemplateID)
}

func (r *Repository) SetActivityTemplateStatus(ctx context.Context, command domain.StatusCommand, nowMS int64) (domain.Template, error) {
	if r == nil || r.db == nil {
		return domain.Template{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return domain.Template{}, err
	}
	defer tx.Rollback()
	appCode := appcode.FromContext(ctx)
	if command.Status == domain.StatusPublished {
		// 同一 App 的发布事务先争用一条稳定互斥行，再读取当前草稿与已发布区间。
		// 否则两个互不相同的草稿可以在各自事务快照中都看不到对方，造成区域排期写偏差。
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO activity_template_publish_locks (app_code, updated_at_ms)
			VALUES (?, ?)
			ON DUPLICATE KEY UPDATE updated_at_ms = ?`, appCode, nowMS, nowMS); err != nil {
			return domain.Template{}, err
		}
	}
	current, err := getActivityTemplateAggregate(ctx, tx, appCode, command.TemplateID, true)
	if err != nil {
		return domain.Template{}, err
	}
	if current.Revision != command.ExpectedRevision {
		return domain.Template{}, xerr.New(xerr.Conflict, "activity template revision changed")
	}
	if current.Status == domain.StatusArchived {
		return domain.Template{}, xerr.New(xerr.Conflict, "archived activity template is read-only")
	}
	if command.Status == domain.StatusPublished {
		// 发布冲突检查与状态写入必须在同一个事务内。all_regions 与任何区域活动冲突；
		// 定向活动只在区域集合有交集且 UTC [start_ms,end_ms) 相交时冲突。
		if err := ensureNoPublishedActivityTemplateOverlap(ctx, tx, current); err != nil {
			return domain.Template{}, err
		}
	}
	nextRevision := current.Revision + 1
	nextPublishedVersion := current.PublishedVersion
	nextPublishedBy := current.PublishedByAdminID
	nextPublishedAt := current.PublishedAtMS
	nextArchivedAt := current.ArchivedAtMS
	if command.Status == domain.StatusPublished {
		nextPublishedVersion++
		nextPublishedBy = command.OperatorAdminID
		nextPublishedAt = nowMS
	}
	if command.Status == domain.StatusArchived {
		nextArchivedAt = nowMS
	}
	result, err := tx.ExecContext(ctx, `
		UPDATE activity_templates
		SET status = ?, revision = ?, published_version = ?, updated_by_admin_id = ?, updated_at_ms = ?,
		    published_by_admin_id = ?, published_at_ms = ?, archived_at_ms = ?
		WHERE app_code = ? AND template_id = ? AND revision = ?`,
		command.Status, nextRevision, nextPublishedVersion, command.OperatorAdminID, nowMS,
		nextPublishedBy, nextPublishedAt, nextArchivedAt,
		appCode, current.TemplateID, command.ExpectedRevision)
	if err != nil {
		return domain.Template{}, activityTemplateWriteError(err)
	}
	affected, _ := result.RowsAffected()
	if affected != 1 {
		return domain.Template{}, xerr.New(xerr.Conflict, "activity template revision changed")
	}
	if command.Status == domain.StatusPublished {
		published, err := getActivityTemplateAggregate(ctx, tx, appCode, current.TemplateID, false)
		if err != nil {
			return domain.Template{}, err
		}
		// wallet-owner snapshot IDs are produced immediately before this transaction and keyed by the next
		// published version. Only these trusted refs are embedded; mutable edit tables continue storing group IDs.
		for index := range published.Tasks {
			if published.Tasks[index].RewardResourceGroupID <= 0 {
				continue
			}
			snapshot, ok := command.RewardSnapshots[published.Tasks[index].RewardResourceGroupID]
			if !ok || snapshot.SnapshotID == "" {
				return domain.Template{}, xerr.New(xerr.Conflict, "activity task reward snapshot is missing")
			}
			published.Tasks[index].RewardSnapshot = snapshot
		}
		for index := range published.RankRewards {
			snapshot, ok := command.RewardSnapshots[published.RankRewards[index].ResourceGroupID]
			if !ok || snapshot.SnapshotID == "" {
				return domain.Template{}, xerr.New(xerr.Conflict, "activity rank reward snapshot is missing")
			}
			published.RankRewards[index].RewardSnapshot = snapshot
		}
		published.LifecycleStatus = ""
		snapshotJSON, err := json.Marshal(published)
		if err != nil {
			return domain.Template{}, err
		}
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO activity_template_versions (
			  app_code, template_id, version_no, snapshot_json, published_by_admin_id, published_at_ms
			) VALUES (?, ?, ?, CAST(? AS JSON), ?, ?)`,
			appCode, current.TemplateID, nextPublishedVersion, string(snapshotJSON), command.OperatorAdminID, nowMS); err != nil {
			return domain.Template{}, activityTemplateWriteError(err)
		}
		// 运行侧只绑定刚写入的不可变快照；这里和状态/版本在同一事务建启用窗口及 UTC 结算周期，
		// 避免发布已成功但 MQ 消费或 cron 永远找不到运行版本的半发布状态。
		if err := insertActivityTemplateRuntimeVersion(ctx, tx, published, nextPublishedVersion, nowMS, r.activityTemplateSettlementLatenessMS()); err != nil {
			return domain.Template{}, activityTemplateWriteError(err)
		}
	} else if command.Status == domain.StatusDisabled {
		// 停用只关闭当前发布版本的真实事件窗口，并把未来周期取消、进行中的周期截断为立即可结算。
		// 已冻结快照和历史发奖任务不删除，后续重试仍按原 published_version 审计。
		if err := closeActivityTemplateRuntimeVersion(ctx, tx, current.TemplateID, current.PublishedVersion, nowMS, r.activityTemplateSettlementLatenessMS()); err != nil {
			return domain.Template{}, activityTemplateWriteError(err)
		}
	}
	if err := tx.Commit(); err != nil {
		return domain.Template{}, activityTemplateWriteError(err)
	}
	return r.GetActivityTemplate(ctx, current.TemplateID)
}

func (r *Repository) ListActivityTemplateVersions(ctx context.Context, templateID string, page int32, pageSize int32) ([]domain.Version, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	appCode := appcode.FromContext(ctx)
	if _, err := getActivityTemplateBase(ctx, r.db, appCode, templateID, false); err != nil {
		return nil, 0, err
	}
	var total int64
	if err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*) FROM activity_template_versions WHERE app_code = ? AND template_id = ?`,
		appCode, templateID).Scan(&total); err != nil {
		return nil, 0, err
	}
	pageSize = normalizePageSize(pageSize)
	if page <= 0 {
		page = 1
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT v.version_no, CAST(v.snapshot_json AS CHAR), v.published_by_admin_id, v.published_at_ms,
		       COALESCE(rv.runtime_from_ms, 0), COALESCE(rv.runtime_to_ms, 0)
		FROM activity_template_versions v
		LEFT JOIN activity_template_runtime_versions rv
		  ON rv.app_code = v.app_code AND rv.template_id = v.template_id AND rv.version_no = v.version_no
		WHERE v.app_code = ? AND v.template_id = ?
		ORDER BY v.version_no DESC
		LIMIT ? OFFSET ?`, appCode, templateID, int(pageSize), int((page-1)*pageSize))
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	items := make([]domain.Version, 0)
	for rows.Next() {
		var item domain.Version
		var snapshotJSON string
		item.TemplateID = templateID
		if err := rows.Scan(
			&item.VersionNo, &snapshotJSON, &item.PublishedByAdminID, &item.PublishedAtMS,
			&item.RuntimeFromMS, &item.RuntimeToMS,
		); err != nil {
			return nil, 0, err
		}
		if err := json.Unmarshal([]byte(snapshotJSON), &item.Snapshot); err != nil {
			return nil, 0, fmt.Errorf("decode activity template version %d: %w", item.VersionNo, err)
		}
		items = append(items, item)
	}
	return items, total, rows.Err()
}

func (r *Repository) GetActivityTemplateVersion(ctx context.Context, templateID string, versionNo int64) (domain.Version, error) {
	if r == nil || r.db == nil {
		return domain.Version{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	var item domain.Version
	var snapshotJSON string
	item.TemplateID = strings.TrimSpace(templateID)
	err := r.db.QueryRowContext(ctx, `
		SELECT v.version_no, CAST(v.snapshot_json AS CHAR), v.published_by_admin_id, v.published_at_ms,
		       COALESCE(rv.runtime_from_ms, 0), COALESCE(rv.runtime_to_ms, 0)
		FROM activity_template_versions v
		LEFT JOIN activity_template_runtime_versions rv
		  ON rv.app_code = v.app_code AND rv.template_id = v.template_id AND rv.version_no = v.version_no
		WHERE v.app_code = ? AND v.template_id = ? AND v.version_no = ?`,
		appcode.FromContext(ctx), item.TemplateID, versionNo).Scan(
		&item.VersionNo, &snapshotJSON, &item.PublishedByAdminID, &item.PublishedAtMS,
		&item.RuntimeFromMS, &item.RuntimeToMS)
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Version{}, xerr.New(xerr.NotFound, "activity template version not found")
	}
	if err != nil {
		return domain.Version{}, err
	}
	if err := json.Unmarshal([]byte(snapshotJSON), &item.Snapshot); err != nil {
		return domain.Version{}, fmt.Errorf("decode activity template version %d: %w", item.VersionNo, err)
	}
	return item, nil
}

func activityTemplateListWhere(ctx context.Context, query domain.ListQuery) (string, []any) {
	parts := []string{"t.app_code = ?"}
	args := []any{appcode.FromContext(ctx)}
	if query.Status == "" {
		parts = append(parts, "t.status <> ?")
		args = append(args, domain.StatusArchived)
	} else {
		switch query.Status {
		case domain.LifecycleScheduled:
			parts = append(parts, "t.status = ? AND t.start_ms > ?")
			args = append(args, domain.StatusPublished, query.NowMS)
		case domain.LifecycleOngoing, "active":
			parts = append(parts, "t.status = ? AND t.start_ms <= ? AND t.end_ms > ?")
			args = append(args, domain.StatusPublished, query.NowMS, query.NowMS)
		case domain.LifecycleEnded:
			parts = append(parts, "t.status = ? AND t.end_ms <= ?")
			args = append(args, domain.StatusPublished, query.NowMS)
		default:
			parts = append(parts, "t.status = ?")
			args = append(args, query.Status)
		}
	}
	if query.Keyword != "" {
		parts = append(parts, "(t.template_id LIKE ? OR t.template_code LIKE ? OR t.name LIKE ?)")
		keyword := "%" + query.Keyword + "%"
		args = append(args, keyword, keyword, keyword)
	}
	if query.StartMS > 0 || query.EndMS > 0 {
		// 未排期草稿的 start_ms/end_ms 均为 0，不属于任何时间区间；尤其只有 end_ms 时不能把它们误算为相交。
		parts = append(parts, "t.start_ms > 0 AND t.end_ms > t.start_ms")
	}
	if query.StartMS > 0 {
		// 区间筛选使用相交语义，不要求活动开始时间落在查询区间内。
		parts = append(parts, "t.end_ms > ?")
		args = append(args, query.StartMS)
	}
	if query.EndMS > 0 {
		parts = append(parts, "t.start_ms < ?")
		args = append(args, query.EndMS)
	}
	if query.AllRegions != nil {
		parts = append(parts, "t.all_regions = ?")
		args = append(args, *query.AllRegions)
	}
	if query.RegionID != nil {
		if query.AllRegions != nil && !*query.AllRegions {
			parts = append(parts, "EXISTS (SELECT 1 FROM activity_template_regions tr WHERE tr.app_code = t.app_code AND tr.template_id = t.template_id AND tr.region_id = ?)")
		} else {
			// 未显式限定 all_regions 时，区域筛选返回所有对该区域生效的活动，包括全区活动。
			parts = append(parts, "(t.all_regions = TRUE OR EXISTS (SELECT 1 FROM activity_template_regions tr WHERE tr.app_code = t.app_code AND tr.template_id = t.template_id AND tr.region_id = ?))")
		}
		args = append(args, *query.RegionID)
	}
	return "WHERE " + strings.Join(parts, " AND "), args
}

func getActivityTemplateAggregate(ctx context.Context, q activityTemplateQuerier, appCode string, templateID string, forUpdate bool) (domain.Template, error) {
	item, err := getActivityTemplateBase(ctx, q, appCode, templateID, forUpdate)
	if err != nil {
		return domain.Template{}, err
	}
	if err := attachActivityTemplateChildren(ctx, q, &item); err != nil {
		return domain.Template{}, err
	}
	return item, nil
}

func getActivityTemplateBase(ctx context.Context, q activityTemplateQuerier, appCode string, templateID string, forUpdate bool) (domain.Template, error) {
	query := activityTemplateSelect + ` WHERE app_code = ? AND template_id = ?`
	if forUpdate {
		query += ` FOR UPDATE`
	}
	item, err := scanActivityTemplate(q.QueryRowContext(ctx, query, appCode, strings.TrimSpace(templateID)))
	if errors.Is(err, sql.ErrNoRows) {
		return domain.Template{}, xerr.New(xerr.NotFound, "activity template not found")
	}
	return item, err
}

func scanActivityTemplate(scanner activityTemplateScanner) (domain.Template, error) {
	var item domain.Template
	err := scanner.Scan(
		&item.AppCode, &item.TemplateID, &item.TemplateCode, &item.Name, &item.ActivityType, &item.Status,
		&item.StartMS, &item.EndMS, &item.AllRegions, &item.DailyLeaderboardSize, &item.TotalLeaderboardSize,
		&item.DisplayConfigJSON, &item.Revision, &item.PublishedVersion,
		&item.CreatedByAdminID, &item.UpdatedByAdminID, &item.PublishedByAdminID,
		&item.CreatedAtMS, &item.UpdatedAtMS, &item.PublishedAtMS, &item.ArchivedAtMS,
	)
	return item, err
}

func attachActivityTemplateChildren(ctx context.Context, q activityTemplateQuerier, item *domain.Template) error {
	item.RegionIDs = []int64{}
	item.Locales = []domain.Locale{}
	item.Gifts = []domain.Gift{}
	item.Tasks = []domain.Task{}
	item.RankRewards = []domain.RankReward{}
	item.Assets = []domain.Asset{}
	rows, err := q.QueryContext(ctx, `SELECT region_id FROM activity_template_regions WHERE app_code = ? AND template_id = ? ORDER BY region_id`, item.AppCode, item.TemplateID)
	if err != nil {
		return err
	}
	for rows.Next() {
		var regionID int64
		if err := rows.Scan(&regionID); err != nil {
			rows.Close()
			return err
		}
		item.RegionIDs = append(item.RegionIDs, regionID)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return err
	}
	if err := rows.Close(); err != nil {
		return err
	}
	rows, err = q.QueryContext(ctx, `SELECT locale, title, rules FROM activity_template_locales WHERE app_code = ? AND template_id = ? ORDER BY locale`, item.AppCode, item.TemplateID)
	if err != nil {
		return err
	}
	for rows.Next() {
		var locale domain.Locale
		if err := rows.Scan(&locale.Locale, &locale.Title, &locale.Rules); err != nil {
			rows.Close()
			return err
		}
		item.Locales = append(item.Locales, locale)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return err
	}
	if err := rows.Close(); err != nil {
		return err
	}
	rows, err = q.QueryContext(ctx, `
		SELECT gift_id, sort_order, name, icon_url, coin_price, price_version
		FROM activity_template_gifts WHERE app_code = ? AND template_id = ? ORDER BY sort_order DESC, gift_id`, item.AppCode, item.TemplateID)
	if err != nil {
		return err
	}
	for rows.Next() {
		var gift domain.Gift
		if err := rows.Scan(&gift.GiftID, &gift.SortOrder, &gift.Name, &gift.IconURL, &gift.CoinPrice, &gift.PriceVersion); err != nil {
			rows.Close()
			return err
		}
		item.Gifts = append(item.Gifts, gift)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return err
	}
	if err := rows.Close(); err != nil {
		return err
	}
	rows, err = q.QueryContext(ctx, `
		SELECT task_key, task_type, target_value, reward_resource_group_id, sort_order
		FROM activity_template_tasks WHERE app_code = ? AND template_id = ? ORDER BY sort_order DESC, task_key`, item.AppCode, item.TemplateID)
	if err != nil {
		return err
	}
	for rows.Next() {
		var task domain.Task
		if err := rows.Scan(&task.TaskKey, &task.TaskType, &task.TargetValue, &task.RewardResourceGroupID, &task.SortOrder); err != nil {
			rows.Close()
			return err
		}
		item.Tasks = append(item.Tasks, task)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return err
	}
	if err := rows.Close(); err != nil {
		return err
	}
	rows, err = q.QueryContext(ctx, `
		SELECT board_type, rank_from, rank_to, resource_group_id, sort_order
		FROM activity_template_rank_rewards WHERE app_code = ? AND template_id = ? ORDER BY board_type, rank_from, rank_to`, item.AppCode, item.TemplateID)
	if err != nil {
		return err
	}
	for rows.Next() {
		var reward domain.RankReward
		if err := rows.Scan(&reward.BoardType, &reward.RankFrom, &reward.RankTo, &reward.ResourceGroupID, &reward.SortOrder); err != nil {
			rows.Close()
			return err
		}
		item.RankRewards = append(item.RankRewards, reward)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return err
	}
	if err := rows.Close(); err != nil {
		return err
	}
	rows, err = q.QueryContext(ctx, `
		SELECT asset_key, locale, url, media_type, sort_order
		FROM activity_template_assets WHERE app_code = ? AND template_id = ? ORDER BY asset_key, locale, sort_order DESC`, item.AppCode, item.TemplateID)
	if err != nil {
		return err
	}
	for rows.Next() {
		var asset domain.Asset
		if err := rows.Scan(&asset.AssetKey, &asset.Locale, &asset.URL, &asset.MediaType, &asset.SortOrder); err != nil {
			rows.Close()
			return err
		}
		item.Assets = append(item.Assets, asset)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return err
	}
	if err := rows.Close(); err != nil {
		return err
	}
	return nil
}

func replaceActivityTemplateChildren(ctx context.Context, tx *sql.Tx, item domain.Template, nowMS int64) error {
	for _, table := range []string{
		"activity_template_regions",
		"activity_template_locales",
		"activity_template_gifts",
		"activity_template_tasks",
		"activity_template_rank_rewards",
		"activity_template_assets",
	} {
		if _, err := tx.ExecContext(ctx, `DELETE FROM `+table+` WHERE app_code = ? AND template_id = ?`, item.AppCode, item.TemplateID); err != nil {
			return err
		}
	}
	for _, regionID := range item.RegionIDs {
		if _, err := tx.ExecContext(ctx, `INSERT INTO activity_template_regions (app_code, template_id, region_id, created_at_ms) VALUES (?, ?, ?, ?)`, item.AppCode, item.TemplateID, regionID, nowMS); err != nil {
			return err
		}
	}
	for _, locale := range item.Locales {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO activity_template_locales (app_code, template_id, locale, title, rules, created_at_ms, updated_at_ms)
			VALUES (?, ?, ?, ?, ?, ?, ?)`, item.AppCode, item.TemplateID, locale.Locale, locale.Title, locale.Rules, nowMS, nowMS); err != nil {
			return err
		}
	}
	for _, gift := range item.Gifts {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO activity_template_gifts (app_code, template_id, gift_id, sort_order, name, icon_url, coin_price, price_version, created_at_ms, updated_at_ms)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			item.AppCode, item.TemplateID, gift.GiftID, gift.SortOrder, gift.Name, gift.IconURL, gift.CoinPrice, gift.PriceVersion, nowMS, nowMS); err != nil {
			return err
		}
	}
	for _, task := range item.Tasks {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO activity_template_tasks (app_code, template_id, task_key, task_type, target_value, reward_resource_group_id, sort_order, created_at_ms, updated_at_ms)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			item.AppCode, item.TemplateID, task.TaskKey, task.TaskType, task.TargetValue, task.RewardResourceGroupID, task.SortOrder, nowMS, nowMS); err != nil {
			return err
		}
	}
	for _, reward := range item.RankRewards {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO activity_template_rank_rewards (app_code, template_id, board_type, rank_from, rank_to, resource_group_id, sort_order, created_at_ms, updated_at_ms)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			item.AppCode, item.TemplateID, reward.BoardType, reward.RankFrom, reward.RankTo, reward.ResourceGroupID, reward.SortOrder, nowMS, nowMS); err != nil {
			return err
		}
	}
	for _, asset := range item.Assets {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO activity_template_assets (app_code, template_id, asset_key, locale, url, media_type, sort_order, created_at_ms, updated_at_ms)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			item.AppCode, item.TemplateID, asset.AssetKey, asset.Locale, asset.URL, asset.MediaType, asset.SortOrder, nowMS, nowMS); err != nil {
			return err
		}
	}
	return nil
}

func ensureNoPublishedActivityTemplateOverlap(ctx context.Context, tx *sql.Tx, item domain.Template) error {
	rows, err := tx.QueryContext(ctx, activityTemplateSelect+`
		WHERE app_code = ? AND template_id <> ? AND status = ? AND start_ms < ? AND end_ms > ?
		ORDER BY start_ms, template_id
		FOR UPDATE`, item.AppCode, item.TemplateID, domain.StatusPublished, item.EndMS, item.StartMS)
	if err != nil {
		return err
	}
	candidates := make([]domain.Template, 0)
	for rows.Next() {
		candidate, err := scanActivityTemplate(rows)
		if err != nil {
			rows.Close()
			return err
		}
		candidates = append(candidates, candidate)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return err
	}
	if err := rows.Close(); err != nil {
		return err
	}
	for index := range candidates {
		if err := attachActivityTemplateRegions(ctx, tx, &candidates[index]); err != nil {
			return err
		}
		if activityTemplateScopesOverlap(item, candidates[index]) {
			return xerr.New(xerr.Conflict, "published activity template overlaps another activity in the same region scope")
		}
	}
	return nil
}

func activityTemplateScopesOverlap(left domain.Template, right domain.Template) bool {
	if left.AllRegions || right.AllRegions {
		return true
	}
	seen := make(map[int64]struct{}, len(left.RegionIDs))
	for _, regionID := range left.RegionIDs {
		seen[regionID] = struct{}{}
	}
	for _, regionID := range right.RegionIDs {
		if _, exists := seen[regionID]; exists {
			return true
		}
	}
	return false
}

func attachActivityTemplateRegions(ctx context.Context, q activityTemplateQuerier, item *domain.Template) error {
	rows, err := q.QueryContext(ctx, `SELECT region_id FROM activity_template_regions WHERE app_code = ? AND template_id = ? ORDER BY region_id`, item.AppCode, item.TemplateID)
	if err != nil {
		return err
	}
	defer rows.Close()
	item.RegionIDs = []int64{}
	for rows.Next() {
		var regionID int64
		if err := rows.Scan(&regionID); err != nil {
			return err
		}
		item.RegionIDs = append(item.RegionIDs, regionID)
	}
	return rows.Err()
}

func attachActivityTemplateSummaryRegions(ctx context.Context, q activityTemplateQuerier, items []domain.Summary) error {
	if len(items) == 0 {
		return nil
	}
	placeholders := make([]string, 0, len(items))
	args := make([]any, 0, len(items)+1)
	args = append(args, items[0].AppCode)
	indexes := make(map[string]int, len(items))
	for index := range items {
		items[index].RegionIDs = []int64{}
		placeholders = append(placeholders, "?")
		args = append(args, items[index].TemplateID)
		indexes[items[index].TemplateID] = index
	}
	rows, err := q.QueryContext(ctx, `
		SELECT template_id, region_id
		FROM activity_template_regions
		WHERE app_code = ? AND template_id IN (`+strings.Join(placeholders, ",")+`)
		ORDER BY template_id, region_id`, args...)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var templateID string
		var regionID int64
		if err := rows.Scan(&templateID, &regionID); err != nil {
			return err
		}
		if index, exists := indexes[templateID]; exists {
			items[index].RegionIDs = append(items[index].RegionIDs, regionID)
		}
	}
	return rows.Err()
}

func activityTemplateWriteError(err error) error {
	if err == nil {
		return nil
	}
	var mysqlErr *mysqlDriver.MySQLError
	if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
		return xerr.New(xerr.Conflict, "activity template code or child key already exists")
	}
	return err
}
