package mysql

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

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

// Repository 是 activity-service 的 MySQL 存储入口。
type Repository struct {
	db                                 *sql.DB
	activityTemplateSettlementLateness time.Duration
}

const defaultActivityTemplateSettlementLateness = 10 * time.Minute

func (r *Repository) SetActivityTemplateSettlementLateness(value time.Duration) {
	if r == nil {
		return
	}
	if value <= 0 {
		value = defaultActivityTemplateSettlementLateness
	}
	r.activityTemplateSettlementLateness = value
}

func (r *Repository) activityTemplateSettlementLatenessMS() int64 {
	if r == nil || r.activityTemplateSettlementLateness <= 0 {
		return defaultActivityTemplateSettlementLateness.Milliseconds()
	}
	return r.activityTemplateSettlementLateness.Milliseconds()
}

// Open 创建 MySQL 连接池并做启动 ping。
func Open(ctx context.Context, dsn string) (*Repository, error) {
	if strings.TrimSpace(dsn) == "" {
		return nil, xerr.New(xerr.InvalidArgument, "mysql_dsn is required")
	}

	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return nil, err
	}

	// 标准库默认不限制连接数，高并发下会打爆 MySQL max_connections，必须显式设上限。
	db.SetMaxOpenConns(20)
	db.SetMaxIdleConns(10)
	db.SetConnMaxLifetime(30 * time.Minute)

	if err := db.PingContext(ctx); err != nil {
		_ = db.Close()
		return nil, err
	}

	return &Repository{db: db}, nil
}

// Migrate performs narrow, idempotent local schema fixes for activity-service.
func (r *Repository) Migrate(ctx context.Context) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if err := r.ensureActivityOutboxDeliveryColumns(ctx); err != nil {
		return err
	}
	if err := r.ensureActivityOutboxDeliveryIndexes(ctx); err != nil {
		return err
	}
	if err := r.ensureBroadcastOutboxIndexes(ctx); err != nil {
		return err
	}
	if err := r.ensureGameWinBroadcastConfigTable(ctx); err != nil {
		return err
	}
	if err := r.ensureWeeklyStarTables(ctx); err != nil {
		return err
	}
	if err := r.ensureAgencyOpeningTables(ctx); err != nil {
		return err
	}
	if err := r.ensureActivityTemplateTables(ctx); err != nil {
		return err
	}
	if err := r.ensureActivityTemplateRuntimeTables(ctx); err != nil {
		return err
	}
	if err := r.ensureWheelTables(ctx); err != nil {
		return err
	}
	if err := r.ensureRoomTurnoverRewardTables(ctx); err != nil {
		return err
	}
	if err := r.ensureCumulativeRechargeRewardTables(ctx); err != nil {
		return err
	}
	if err := r.ensureInviteActivityRewardTables(ctx); err != nil {
		return err
	}
	if err := r.ensureCPWeeklyRankTables(ctx); err != nil {
		return err
	}
	if err := r.ensureTaskDefinitionMetadataColumns(ctx); err != nil {
		return err
	}
	if err := r.ensureTemporaryGrowthLevelTables(ctx); err != nil {
		return err
	}
	return nil
}

func (r *Repository) ensureTaskDefinitionMetadataColumns(ctx context.Context) error {
	// 本地和测试环境会走自动迁移，逐列检查避免旧库重复启动时报 duplicate column；线上仍可用 deploy/mysql/migrations 的显式 SQL。
	additions := []struct {
		column string
		sql    string
	}{
		{"audience_type", `ALTER TABLE task_definitions ADD COLUMN audience_type VARCHAR(32) NOT NULL DEFAULT 'all' COMMENT '适用人群：all/newbie' AFTER description`},
		{"icon_key", `ALTER TABLE task_definitions ADD COLUMN icon_key VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'App 任务图标键' AFTER audience_type`},
		{"icon_url", `ALTER TABLE task_definitions ADD COLUMN icon_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'App 任务图标 URL' AFTER icon_key`},
		{"action_type", `ALTER TABLE task_definitions ADD COLUMN action_type VARCHAR(64) NOT NULL DEFAULT 'none' COMMENT 'App 跳转类型' AFTER icon_url`},
		{"action_param", `ALTER TABLE task_definitions ADD COLUMN action_param VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'App 跳转参数' AFTER action_type`},
		{"action_payload_json", `ALTER TABLE task_definitions ADD COLUMN action_payload_json JSON NULL COMMENT 'App 跳转扩展 JSON' AFTER action_param`},
		{"dimension_filter_json", `ALTER TABLE task_definitions ADD COLUMN dimension_filter_json JSON NULL COMMENT '任务事件维度过滤 JSON' AFTER action_payload_json`},
		{"reward_asset_type", `ALTER TABLE task_definitions ADD COLUMN reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型：COIN/POINT' AFTER reward_coin_amount`},
	}
	for _, addition := range additions {
		exists, err := r.columnExists(ctx, "task_definitions", addition.column)
		if err != nil {
			return err
		}
		if !exists {
			// 任务定义新增的是展示、跳转和事件过滤元数据，默认值保证旧任务无需人工补字段即可继续查询和领取。
			if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
				return err
			}
		}
	}
	progressAdditions := []struct {
		column string
		sql    string
	}{
		{"reward_asset_type", `ALTER TABLE user_task_progress ADD COLUMN reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型快照：COIN/POINT' AFTER reward_coin_amount`},
	}
	for _, addition := range progressAdditions {
		exists, err := r.columnExists(ctx, "user_task_progress", addition.column)
		if err != nil {
			return err
		}
		if !exists {
			if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
				return err
			}
		}
	}
	claimAdditions := []struct {
		column string
		sql    string
	}{
		{"reward_asset_type", `ALTER TABLE task_reward_claims ADD COLUMN reward_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '奖励资产类型快照：COIN/POINT' AFTER reward_coin_amount`},
	}
	for _, addition := range claimAdditions {
		exists, err := r.columnExists(ctx, "task_reward_claims", addition.column)
		if err != nil {
			return err
		}
		if !exists {
			if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
				return err
			}
		}
	}
	eventAdditions := []struct {
		column string
		sql    string
	}{
		{"metric_type", `ALTER TABLE task_event_consumption ADD COLUMN metric_type VARCHAR(64) NOT NULL DEFAULT '' COMMENT '任务指标类型' AFTER status`},
		{"value_delta", `ALTER TABLE task_event_consumption ADD COLUMN value_delta BIGINT NOT NULL DEFAULT 0 COMMENT '本次事件进度增量' AFTER metric_type`},
		{"dimensions_json", `ALTER TABLE task_event_consumption ADD COLUMN dimensions_json JSON NULL COMMENT '事件维度快照 JSON' AFTER value_delta`},
	}
	for _, addition := range eventAdditions {
		exists, err := r.columnExists(ctx, "task_event_consumption", addition.column)
		if err != nil {
			return err
		}
		if !exists {
			// 事件消费表补充 metric/value/dimensions 快照，主要用于排查“事件到了但任务没涨”的过滤原因。
			if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
				return err
			}
		}
	}
	return nil
}

// Close 释放 MySQL 连接池。
func (r *Repository) Close() error {
	if r == nil || r.db == nil {
		return nil
	}

	return r.db.Close()
}

// Ping 验证 MySQL 当前可用。
func (r *Repository) Ping(ctx context.Context) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}

	return r.db.PingContext(ctx)
}

func (r *Repository) ensureActivityOutboxDeliveryColumns(ctx context.Context) error {
	additions := []struct {
		name string
		sql  string
	}{
		{"locked_by", `ALTER TABLE activity_outbox ADD COLUMN locked_by VARCHAR(128) NOT NULL DEFAULT '' COMMENT '当前锁定 worker' AFTER next_retry_at_ms`},
		{"lock_until_ms", `ALTER TABLE activity_outbox ADD COLUMN lock_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT '锁过期时间，UTC epoch ms' AFTER locked_by`},
		{"last_error", `ALTER TABLE activity_outbox ADD COLUMN last_error VARCHAR(512) NOT NULL DEFAULT '' COMMENT '最后一次失败原因' AFTER lock_until_ms`},
	}
	for _, addition := range additions {
		exists, err := r.columnExists(ctx, "activity_outbox", addition.name)
		if err != nil {
			return err
		}
		if !exists {
			if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
				return err
			}
		}
	}
	return nil
}

func (r *Repository) ensureActivityOutboxDeliveryIndexes(ctx context.Context) error {
	// activity_outbox 仍服务 activity-service 自己的异步事件；索引按 event_type 收敛，避免不同活动补偿 worker 互相扫全表。
	additions := []struct {
		name string
		sql  string
	}{
		{"idx_activity_outbox_event_retry", `CREATE INDEX idx_activity_outbox_event_retry ON activity_outbox (app_code, event_type, status, next_retry_at_ms, created_at_ms, outbox_id)`},
		{"idx_activity_outbox_event_lock", `CREATE INDEX idx_activity_outbox_event_lock ON activity_outbox (app_code, event_type, status, lock_until_ms, created_at_ms, outbox_id)`},
		{"idx_activity_outbox_retention", `CREATE INDEX idx_activity_outbox_retention ON activity_outbox (app_code, status, updated_at_ms, outbox_id)`},
	}
	for _, addition := range additions {
		exists, err := r.indexExists(ctx, "activity_outbox", addition.name)
		if err != nil {
			return err
		}
		if !exists {
			if _, err := r.db.ExecContext(ctx, addition.sql); err != nil {
				return err
			}
		}
	}
	return nil
}

func (r *Repository) ensureBroadcastOutboxIndexes(ctx context.Context) error {
	additions := []struct {
		name      string
		statement string
	}{
		{"idx_im_broadcast_outbox_retention", `CREATE INDEX idx_im_broadcast_outbox_retention ON im_broadcast_outbox (app_code, status, updated_at_ms, event_id)`},
	}
	for _, addition := range additions {
		exists, err := r.indexExists(ctx, "im_broadcast_outbox", addition.name)
		if err != nil {
			return err
		}
		if !exists {
			if _, err := r.db.ExecContext(ctx, addition.statement); err != nil {
				return err
			}
		}
	}
	return nil
}

func (r *Repository) columnExists(ctx context.Context, table string, column string) (bool, error) {
	database, err := r.currentDatabase(ctx)
	if err != nil {
		return false, err
	}
	var count int
	if err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM information_schema.COLUMNS
		WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
		database, table, column,
	).Scan(&count); err != nil {
		return false, err
	}
	return count > 0, nil
}

func (r *Repository) indexExists(ctx context.Context, table string, index string) (bool, error) {
	database, err := r.currentDatabase(ctx)
	if err != nil {
		return false, err
	}
	var count int
	if err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM information_schema.STATISTICS
		WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ?`,
		database, table, index,
	).Scan(&count); err != nil {
		return false, err
	}
	return count > 0, nil
}

func (r *Repository) indexColumnsMatch(ctx context.Context, table string, index string, expected []string) (bool, error) {
	database, err := r.currentDatabase(ctx)
	if err != nil {
		return false, err
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT COLUMN_NAME
		FROM information_schema.STATISTICS
		WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ?
		ORDER BY SEQ_IN_INDEX`, database, table, index)
	if err != nil {
		return false, err
	}
	defer rows.Close()
	actual := make([]string, 0, len(expected))
	for rows.Next() {
		var column string
		if err := rows.Scan(&column); err != nil {
			return false, err
		}
		actual = append(actual, column)
	}
	if err := rows.Err(); err != nil {
		return false, err
	}
	if len(actual) != len(expected) {
		return false, nil
	}
	for index := range expected {
		if actual[index] != expected[index] {
			return false, nil
		}
	}
	return true, nil
}

func (r *Repository) currentDatabase(ctx context.Context) (string, error) {
	var database sql.NullString
	if err := r.db.QueryRowContext(ctx, `SELECT DATABASE()`).Scan(&database); err != nil {
		return "", err
	}
	if !database.Valid || strings.TrimSpace(database.String) == "" {
		return "", fmt.Errorf("mysql database is not selected")
	}
	return database.String, nil
}

// GetActivity 从 MySQL 活动配置表读取同步查询投影。
func (r *Repository) GetActivity(ctx context.Context, activityID string) (activitydomain.Activity, error) {
	if r == nil || r.db == nil {
		return activitydomain.Activity{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}

	row := r.db.QueryRowContext(ctx,
		`SELECT app_code, activity_id, status, updated_at_ms
		 FROM activities
		 WHERE app_code = ? AND activity_id = ?`,
		appcode.FromContext(ctx),
		activityID,
	)

	var activity activitydomain.Activity
	var status string
	if err := row.Scan(&activity.AppCode, &activity.ActivityID, &status, &activity.UpdatedAtMs); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			// 未配置活动时返回 NotFound，上层可决定是否降级为 inactive。
			return activitydomain.Activity{}, xerr.New(xerr.NotFound, "activity not found")
		}

		return activitydomain.Activity{}, err
	}
	activity.Status = activitydomain.Status(status)

	return activity, nil
}
