package mysql

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

	mysqlDriver "github.com/go-sql-driver/mysql"
)

func ensureResourceShopPurchaseOrderIndexes(ctx context.Context, db *sql.DB) error {

	if _, err := db.ExecContext(ctx, `
		ALTER TABLE resource_shop_purchase_orders
		ADD INDEX idx_resource_shop_purchase_time (app_code, created_at_ms, order_id)`); err != nil && !isDuplicateKeyNameError(err) {
		return err
	}

	if _, err := db.ExecContext(ctx, `
		ALTER TABLE resource_shop_purchase_orders
		ADD INDEX idx_resource_shop_purchase_resource_time (app_code, resource_id, created_at_ms, order_id)`); err != nil && !isDuplicateKeyNameError(err) {
		return err
	}
	return nil
}

func ensureResourceGrantRevokeSchema(ctx context.Context, db *sql.DB) error {
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE resource_grants
		ADD COLUMN revoked_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '撤销时间，UTC epoch ms' AFTER operator_user_id`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE resource_grants
		ADD COLUMN revoked_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '撤销操作人用户 ID' AFTER revoked_at_ms`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE resource_grants
		ADD COLUMN revoke_reason VARCHAR(256) NOT NULL DEFAULT '' COMMENT '撤销原因' AFTER revoked_by_user_id`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	return nil
}

func isDuplicateKeyNameError(err error) bool {
	var mysqlErr *mysqlDriver.MySQLError
	return errors.As(err, &mysqlErr) && mysqlErr.Number == 1061
}

func ensureHostSalarySettlementSchema(ctx context.Context, db *sql.DB) error {
	// 工资政策表是小型配置表；启动兼容只补列、回填配置和建立点查索引，不扫描工资账户或钱包流水。
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN settlement_trigger_mode VARCHAR(24) NOT NULL DEFAULT 'automatic' COMMENT '结算触发方式：automatic/manual' AFTER settlement_mode`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN cycle_key VARCHAR(7) NOT NULL DEFAULT '' COMMENT '政策归属工资周期，UTC yyyy-MM' AFTER policy_id`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN policy_version BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '不可变政策版本；首次等于后台政策 ID，修订后递增' AFTER cycle_key`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policy_levels
		ADD COLUMN policy_version BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属不可变政策版本' AFTER policy_id`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN policy_type VARCHAR(32) NOT NULL DEFAULT 'SALARY_DIAMOND' COMMENT '政策类型：SALARY_DIAMOND/POINT_DIAMOND' AFTER name, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN point_diamonds_per_usd BIGINT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 每美元对应钻石积分' AFTER gift_coin_to_diamond_ratio, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN coins_per_usd BIGINT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 每美元对应金币' AFTER point_diamonds_per_usd, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN minimum_withdraw_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 最低提现美元美分' AFTER coins_per_usd, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	// 渠道门槛不做启动时 UPDATE 回填：旧快照以 0 标记未拆分，资金事务读取时回退历史单一门槛。
	// 这样兼容升级只在线追加小型配置表字段，不扫描任何钱包或提现流水。
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN coin_seller_minimum_withdraw_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 用户找币商最低提现美元美分；0 回退旧字段' AFTER minimum_withdraw_usd_minor, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN platform_minimum_withdraw_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 用户找平台最低提现美元美分；0 回退旧字段' AFTER coin_seller_minimum_withdraw_usd_minor, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN diamond_exchange_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开放 POINT_DIAMOND 兑换金币' AFTER platform_minimum_withdraw_usd_minor, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN withdraw_fee_bps INT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 提现费率基点' AFTER diamond_exchange_enabled, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN agency_point_share_bps INT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 代理分成占主播基础积分的基点' AFTER withdraw_fee_bps, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	// POINT_DIAMOND 的 Host/Agency 金额可能共用一条钱包分录；独立投影从送礼事务写入，
	// 让统计按用户和 UTC 时间索引读取，且完全排除其他 POINT_DIAMOND 资产变动。
	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS point_diamond_gift_income_entries (
		  entry_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
		  app_code VARCHAR(32) NOT NULL,
		  transaction_id VARCHAR(96) NOT NULL,
		  host_user_id BIGINT NOT NULL,
		  agency_user_id BIGINT NOT NULL DEFAULT 0,
		  sender_user_id BIGINT NOT NULL DEFAULT 0,
		  host_base_amount BIGINT NOT NULL,
		  agency_amount BIGINT NOT NULL DEFAULT 0,
		  created_at_ms BIGINT NOT NULL,
		  UNIQUE KEY uk_point_diamond_gift_income_tx (app_code, transaction_id),
		  KEY idx_point_diamond_gift_host_time (app_code, host_user_id, created_at_ms, entry_id),
		  KEY idx_point_diamond_gift_agency_time (app_code, agency_user_id, created_at_ms, entry_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='POINT_DIAMOND 送礼收益角色拆分投影'`); err != nil {
		return err
	}
	// 提现限制随工资政策版本发布；启动兼容只补小型配置表列，绝不回查钱包或提现流水推导计数。
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN coin_seller_withdrawal_limit_period VARCHAR(16) NOT NULL DEFAULT 'month' COMMENT '用户找币商提现限制周期：day/week/month' AFTER residual_diamond_to_usd_rate`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN coin_seller_withdrawal_limit_count BIGINT NOT NULL DEFAULT 0 COMMENT '用户找币商提现次数，0 表示不限' AFTER coin_seller_withdrawal_limit_period`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN platform_withdrawal_limit_period VARCHAR(16) NOT NULL DEFAULT 'month' COMMENT '用户找平台提现限制周期：day/week/month' AFTER coin_seller_withdrawal_limit_count`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN platform_withdrawal_limit_count BIGINT NOT NULL DEFAULT 0 COMMENT '用户找平台提现次数，0 表示不限' AFTER platform_withdrawal_limit_period`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD COLUMN platform_withdrawal_allowed_days VARCHAR(96) NOT NULL DEFAULT '' COMMENT '用户找平台提现允许的 UTC 月内日，逗号分隔，空表示每天' AFTER platform_withdrawal_limit_count`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS point_withdrawal_policy_counters (
		  app_code VARCHAR(32) NOT NULL,
		  source_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT',
		  user_id BIGINT NOT NULL,
		  channel VARCHAR(24) NOT NULL,
		  period VARCHAR(16) NOT NULL,
		  period_key VARCHAR(10) NOT NULL,
		  withdrawal_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
		  created_at_ms BIGINT NOT NULL,
		  updated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, source_asset_type, user_id, channel, period, period_key)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户自然周期提现次数计数表'`); err != nil {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS point_withdrawal_limit_reservations (
		  app_code VARCHAR(32) NOT NULL,
		  source_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT',
		  withdrawal_ref VARCHAR(128) NOT NULL,
		  freeze_command_id VARCHAR(128) NOT NULL,
		  user_id BIGINT NOT NULL,
		  channel VARCHAR(24) NOT NULL,
		  policy_id BIGINT UNSIGNED NOT NULL,
		  policy_version BIGINT UNSIGNED NOT NULL,
		  period VARCHAR(16) NOT NULL,
		  period_key VARCHAR(10) NOT NULL,
		  counted BOOLEAN NOT NULL DEFAULT FALSE,
		  status VARCHAR(24) NOT NULL DEFAULT 'active',
		  created_at_ms BIGINT NOT NULL,
		  released_at_ms BIGINT NOT NULL DEFAULT 0,
		  PRIMARY KEY (app_code, source_asset_type, withdrawal_ref),
		  UNIQUE KEY uk_point_withdrawal_limit_reservation_command (app_code, source_asset_type, freeze_command_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台积分提现次数预留表'`); err != nil {
		return err
	}
	// 两张表只保存周期聚合和在途 reservation；补列与改键不会扫描大型钱包流水。
	if _, err := db.ExecContext(ctx, `ALTER TABLE point_withdrawal_policy_counters ADD COLUMN source_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' AFTER app_code, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if columns, err := tablePrimaryKeyColumns(ctx, db, "point_withdrawal_policy_counters"); err != nil {
		return err
	} else if strings.Join(columns, ",") != "app_code,source_asset_type,user_id,channel,period,period_key" {
		if _, err := db.ExecContext(ctx, `ALTER TABLE point_withdrawal_policy_counters DROP PRIMARY KEY, ADD PRIMARY KEY (app_code, source_asset_type, user_id, channel, period, period_key), ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
			return err
		}
	}
	if _, err := db.ExecContext(ctx, `ALTER TABLE point_withdrawal_limit_reservations ADD COLUMN source_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' AFTER app_code, ALGORITHM=INPLACE, LOCK=NONE`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if columns, err := tablePrimaryKeyColumns(ctx, db, "point_withdrawal_limit_reservations"); err != nil {
		return err
	} else if strings.Join(columns, ",") != "app_code,source_asset_type,withdrawal_ref" {
		if _, err := db.ExecContext(ctx, `ALTER TABLE point_withdrawal_limit_reservations DROP PRIMARY KEY, ADD PRIMARY KEY (app_code, source_asset_type, withdrawal_ref), ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
			return err
		}
	}
	reservationIndexColumns, err := tableIndexColumns(ctx, db, "point_withdrawal_limit_reservations", "uk_point_withdrawal_limit_reservation_command")
	if err != nil {
		return err
	}
	if strings.Join(reservationIndexColumns, ",") != "app_code,source_asset_type,freeze_command_id" {
		if len(reservationIndexColumns) > 0 {
			// 删除旧唯一键和建立资产隔离的新唯一键必须同一条 DDL 完成，不能给在线写入留下无约束窗口。
			if _, err := db.ExecContext(ctx, `ALTER TABLE point_withdrawal_limit_reservations DROP INDEX uk_point_withdrawal_limit_reservation_command, ADD UNIQUE KEY uk_point_withdrawal_limit_reservation_command (app_code, source_asset_type, freeze_command_id), ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
				return err
			}
		} else {
			if _, err := db.ExecContext(ctx, `ALTER TABLE point_withdrawal_limit_reservations ADD UNIQUE KEY uk_point_withdrawal_limit_reservation_command (app_code, source_asset_type, freeze_command_id), ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
				return err
			}
		}
	}
	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS host_salary_policy_cycle_bindings (
		  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
		  region_id BIGINT NOT NULL,
		  cycle_key VARCHAR(7) NOT NULL,
		  policy_id BIGINT UNSIGNED NOT NULL,
		  policy_version BIGINT UNSIGNED NOT NULL,
		  source_cycle_key VARCHAR(7) NOT NULL DEFAULT '',
		  created_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, region_id, cycle_key)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主播工资周期不可变政策绑定表'`); err != nil {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_salary_policy_cycle_bindings
		ADD COLUMN source_cycle_key VARCHAR(7) NOT NULL DEFAULT '' COMMENT '来源月份；等于 cycle_key 表示显式发布，否则表示继承' AFTER policy_version`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	// 迁移前绑定视为显式版本；只有新代码的运行时继承会写入与目标月不同的来源月。
	if _, err := db.ExecContext(ctx, `
		UPDATE host_salary_policy_cycle_bindings
		SET source_cycle_key = cycle_key
		WHERE source_cycle_key = ''`); err != nil {
		return err
	}
	var legacyBindingIndexCount int
	// 旧唯一键会阻止同一政策覆盖多个区域或被复制到新周期；信息 schema 是常数级元数据查询，避免盲目 DDL 报错。
	if err := db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM information_schema.STATISTICS
		WHERE TABLE_SCHEMA = DATABASE()
		  AND TABLE_NAME = 'host_salary_policy_cycle_bindings'
		  AND INDEX_NAME = 'uk_host_salary_policy_cycle_version'`).Scan(&legacyBindingIndexCount); err != nil {
		return err
	}
	if legacyBindingIndexCount > 0 {
		if _, err := db.ExecContext(ctx, `
			ALTER TABLE host_salary_policy_cycle_bindings
			DROP INDEX uk_host_salary_policy_cycle_version, ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
			return err
		}
	}
	if _, err := db.ExecContext(ctx, `
		UPDATE host_agency_salary_policies
		SET cycle_key = DATE_FORMAT(
		      DATE_ADD('1970-01-01 00:00:00', INTERVAL (COALESCE(NULLIF(effective_from_ms, 0), created_at_ms) DIV 1000) SECOND),
		      '%Y-%m'
		    ),
		    policy_version = policy_id
		WHERE cycle_key = '' OR policy_version = 0`); err != nil {
		return err
	}
	// 旧结构每个 policy_id 只有一份快照；小型等级表按复合主键点关联回填，不触碰钱包流水和工资记录。
	if _, err := db.ExecContext(ctx, `
		UPDATE host_agency_salary_policy_levels level
		JOIN host_agency_salary_policies policy
		  ON policy.app_code = level.app_code
		 AND policy.policy_id = level.policy_id
		SET level.policy_version = policy.policy_version
		WHERE level.policy_version = 0`); err != nil {
		return err
	}
	if columns, err := tablePrimaryKeyColumns(ctx, db, "host_agency_salary_policies"); err != nil {
		return err
	} else if strings.Join(columns, ",") != "app_code,policy_id,policy_version" {
		if _, err := db.ExecContext(ctx, `ALTER TABLE host_agency_salary_policies DROP PRIMARY KEY, ADD PRIMARY KEY (app_code, policy_id, policy_version), ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
			return err
		}
	}
	if columns, err := tablePrimaryKeyColumns(ctx, db, "host_agency_salary_policy_levels"); err != nil {
		return err
	} else if strings.Join(columns, ",") != "app_code,policy_id,policy_version,level_no" {
		if _, err := db.ExecContext(ctx, `ALTER TABLE host_agency_salary_policy_levels DROP PRIMARY KEY, ADD PRIMARY KEY (app_code, policy_id, policy_version, level_no), ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
			return err
		}
	}
	levelIndexColumns, err := tableIndexColumns(ctx, db, "host_agency_salary_policy_levels", "idx_host_agency_salary_policy_level_required")
	if err != nil {
		return err
	}
	if strings.Join(levelIndexColumns, ",") != "app_code,policy_id,policy_version,status,required_diamonds" {
		if len(levelIndexColumns) > 0 {
			if _, err := db.ExecContext(ctx, `ALTER TABLE host_agency_salary_policy_levels DROP INDEX idx_host_agency_salary_policy_level_required, ADD INDEX idx_host_agency_salary_policy_level_required (app_code, policy_id, policy_version, status, required_diamonds), ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
				return err
			}
		} else if _, err := db.ExecContext(ctx, `ALTER TABLE host_agency_salary_policy_levels ADD INDEX idx_host_agency_salary_policy_level_required (app_code, policy_id, policy_version, status, required_diamonds), ALGORITHM=INPLACE, LOCK=NONE`); err != nil {
			return err
		}
	}
	if _, err := db.ExecContext(ctx, `
		INSERT IGNORE INTO host_salary_policy_cycle_bindings (
		  app_code, region_id, cycle_key, policy_id, policy_version, source_cycle_key, created_at_ms
		)
		SELECT policy.app_code, policy.region_id, policy.cycle_key, policy.policy_id, policy.policy_version, policy.cycle_key, policy.created_at_ms
		FROM host_agency_salary_policies policy
		WHERE policy.cycle_key <> ''
		  AND policy.status = 'active'
		  AND NOT EXISTS (
		    SELECT 1
		    FROM host_agency_salary_policies newer
		    WHERE newer.app_code = policy.app_code
		      AND newer.region_id = policy.region_id
		      AND newer.cycle_key = policy.cycle_key
		      AND newer.status = 'active'
		      AND (
		        newer.effective_from_ms > policy.effective_from_ms
		        OR (newer.effective_from_ms = policy.effective_from_ms AND newer.policy_id > policy.policy_id)
		      )
		  )`); err != nil {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_agency_salary_policies
		ADD INDEX idx_host_agency_salary_policy_cycle (app_code, region_id, cycle_key, status, settlement_mode, settlement_trigger_mode)`); err != nil && !isDuplicateKeyNameError(err) {
		return err
	}

	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_salary_settlement_records
		ADD COLUMN trigger_mode VARCHAR(24) NOT NULL DEFAULT 'automatic' COMMENT '触发方式：automatic/manual' AFTER settlement_type`); err != nil && !isDuplicateColumnError(err) {
		return err
	}

	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_salary_settlement_records
		ADD COLUMN settlement_role VARCHAR(24) NOT NULL DEFAULT 'all' COMMENT '结算身份：all/host/agency' AFTER trigger_mode`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_salary_settlement_progress
		ADD COLUMN last_policy_version BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后一次使用的不可变政策版本' AFTER last_policy_id`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE host_salary_settlement_records
		ADD COLUMN policy_version BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用的不可变政策版本' AFTER policy_id`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	return nil
}

func ensureExternalRechargeSchema(ctx context.Context, db *sql.DB) error {
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE wallet_recharge_products
		ADD COLUMN audience_type VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '充值档位适用用户类型：normal/coin_seller' AFTER app_code`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE wallet_recharge_products
		ADD COLUMN app_visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否在 App 普通充值列表展示；不影响 Google Play 验单' AFTER status`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS third_party_payment_channels (
		  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
		  provider_code VARCHAR(32) NOT NULL COMMENT '三方支付渠道编码',
		  provider_name VARCHAR(64) NOT NULL COMMENT '三方支付渠道名称',
		  status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '状态：active/disabled',
		  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
		  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
		  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
		  PRIMARY KEY (app_code, provider_code),
		  KEY idx_third_party_payment_channels_status (app_code, status, sort_order)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='三方支付渠道配置表'`); err != nil {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS third_party_payment_methods (
		  method_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '支付方式 ID',
		  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
		  provider_code VARCHAR(32) NOT NULL COMMENT '三方支付渠道编码',
		  country_code VARCHAR(16) NOT NULL COMMENT 'MiFaPay countryCode，GLOBAL 表示全球方式',
		  country_name VARCHAR(64) NOT NULL DEFAULT '' COMMENT '国家/地区名称',
		  currency_code VARCHAR(8) NOT NULL COMMENT '下单币种',
		  pay_way VARCHAR(64) NOT NULL COMMENT 'MiFaPay payWay',
		  pay_type VARCHAR(64) NOT NULL COMMENT 'MiFaPay payType',
		  method_name VARCHAR(128) NOT NULL COMMENT '支付方式展示名',
		  logo_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '支付方式 logo',
		  status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '状态：active/disabled',
		  usd_to_currency_rate DECIMAL(24,8) NOT NULL DEFAULT 0 COMMENT '1 USD 可兑换本币金额；0 表示未配置，不在 H5 展示',
		  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
		  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
		  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
		  PRIMARY KEY (method_id),
		  UNIQUE KEY uk_third_party_payment_method (app_code, provider_code, country_code, pay_way, pay_type),
		  KEY idx_third_party_payment_methods_country (app_code, provider_code, country_code, status, sort_order)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='三方支付国家和方式配置表'`); err != nil {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS external_recharge_orders (
		  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
		  order_id VARCHAR(96) NOT NULL COMMENT '平台外部充值订单 ID',
		  command_id VARCHAR(128) NOT NULL COMMENT '客户端命令幂等 ID',
		  target_user_id BIGINT NOT NULL COMMENT '充值目标用户 ID',
		  target_region_id BIGINT NOT NULL COMMENT '充值目标区域 ID',
		  target_country_id BIGINT NOT NULL DEFAULT 0 COMMENT '充值目标国家 ID',
		  target_country_code VARCHAR(16) NOT NULL DEFAULT '' COMMENT '充值目标国家码',
		  audience_type VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '商品适用用户类型',
		  product_id BIGINT NOT NULL COMMENT '充值商品 ID',
		  product_code VARCHAR(128) NOT NULL DEFAULT '' COMMENT '充值商品编码快照',
		  product_name VARCHAR(128) NOT NULL DEFAULT '' COMMENT '充值商品名称快照',
		  coin_amount BIGINT NOT NULL COMMENT '到账金币数量',
		  usd_minor_amount BIGINT NOT NULL COMMENT '美元最小单位金额',
		  provider_code VARCHAR(32) NOT NULL COMMENT '支付渠道：mifapay/usdt_trc20',
		  payment_method_id BIGINT NOT NULL DEFAULT 0 COMMENT '三方支付方式 ID',
		  country_code VARCHAR(16) NOT NULL DEFAULT '' COMMENT '支付国家码',
		  currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '支付币种',
		  provider_amount_minor BIGINT NOT NULL DEFAULT 0 COMMENT '三方下单金额最小单位',
		  pay_way VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'MiFaPay payWay',
		  pay_type VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'MiFaPay payType',
		  pay_url TEXT NULL COMMENT '三方收银台地址',
		  provider_order_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '三方订单号',
		  tx_hash VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'USDT-TRC20 交易哈希',
		  receive_address VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'USDT 收款地址',
		  status VARCHAR(32) NOT NULL DEFAULT 'pending' COMMENT 'pending/redirected/credited/failed',
		  failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
		  wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '入账钱包交易 ID',
		  provider_payload JSON NULL COMMENT '三方原始响应或链上交易快照',
		  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
		  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
		  PRIMARY KEY (app_code, order_id),
		  UNIQUE KEY uk_external_recharge_command (app_code, command_id),
		  KEY idx_external_recharge_user_time (app_code, target_user_id, created_at_ms),
		  KEY idx_external_recharge_provider_order (app_code, provider_code, provider_order_id),
		  KEY idx_external_recharge_wallet_tx (app_code, wallet_transaction_id),
		  KEY idx_external_recharge_reconcile (app_code, status, provider_code, updated_at_ms, created_at_ms),
		  KEY idx_external_recharge_tx (app_code, provider_code, tx_hash)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='H5 外部充值订单表'`); err != nil {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE external_recharge_orders
		ADD COLUMN target_country_id BIGINT NOT NULL DEFAULT 0 COMMENT '充值目标国家 ID' AFTER target_region_id`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	return seedThirdPartyPaymentDefaults(ctx, db)
}

// ensureGooglePaymentPaidDetailsSchema 给 payment_orders 补 Google Orders API 实付明细列；
// 财务列表按 wallet_transaction_id 反查 Google 支付订单，需要独立索引避免回表扫描。
func ensureGooglePaymentPaidDetailsSchema(ctx context.Context, db *sql.DB) error {
	alters := []string{
		`ALTER TABLE payment_orders ADD COLUMN paid_currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '用户实付币种，来自 Google Orders API' AFTER amount_micro`,
		`ALTER TABLE payment_orders ADD COLUMN paid_amount_micro BIGINT NOT NULL DEFAULT 0 COMMENT '用户实付总额微单位（实付币种）' AFTER paid_currency_code`,
		`ALTER TABLE payment_orders ADD COLUMN paid_tax_micro BIGINT NOT NULL DEFAULT 0 COMMENT '订单税额微单位（实付币种）' AFTER paid_amount_micro`,
		`ALTER TABLE payment_orders ADD COLUMN paid_net_micro BIGINT NOT NULL DEFAULT 0 COMMENT 'Google 扣费后开发者净收入微单位（实付币种）' AFTER paid_tax_micro`,
		`ALTER TABLE payment_orders ADD COLUMN paid_synced_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '实付明细同步时间，UTC epoch ms；0 表示未同步' AFTER paid_net_micro`,
		`ALTER TABLE payment_orders ADD COLUMN paid_sync_attempt_count INT NOT NULL DEFAULT 0 COMMENT 'Google 实付同步连续失败次数' AFTER paid_synced_at_ms`,
		`ALTER TABLE payment_orders ADD COLUMN paid_sync_last_error VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'Google 实付同步最近错误' AFTER paid_sync_attempt_count`,
		`ALTER TABLE payment_orders ADD COLUMN paid_sync_next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'Google 实付同步下次重试时间，UTC epoch ms' AFTER paid_sync_last_error`,
	}
	for _, stmt := range alters {
		if _, err := db.ExecContext(ctx, stmt); err != nil && !isDuplicateColumnError(err) {
			return err
		}
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE payment_orders
		ADD INDEX idx_payment_orders_wallet_tx (app_code, wallet_transaction_id)`); err != nil && !isDuplicateKeyNameError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE payment_orders
		ADD INDEX idx_payment_orders_paid_sync (provider, paid_synced_at_ms, paid_sync_next_retry_at_ms, created_at_ms)`); err != nil && !isDuplicateKeyNameError(err) {
		return err
	}
	return nil
}

// ensureWalletPolicyInstanceSchema 保留现有 POINT 提现、兑换和旧分成统计所需的运行表。
// 新送礼不读取这里的 Host/Agency POINT 字段，周期钻石只由 Host 工资政策路径累计。
func ensureWalletPolicyInstanceSchema(ctx context.Context, db *sql.DB) error {
	_, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS wallet_policy_instances (
		  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
		  instance_code VARCHAR(96) NOT NULL COMMENT '后台策略实例编码',
		  template_code VARCHAR(96) NOT NULL COMMENT '模板编码快照',
		  template_version VARCHAR(64) NOT NULL COMMENT '模板版本快照',
		  region_id BIGINT NOT NULL DEFAULT 0 COMMENT '适用地区，0 表示 App 兜底',
		  status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
		  effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'UTC epoch ms',
		  effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
		  host_point_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '兼容既有表结构；新礼物不读取',
		  host_point_ratio_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '兼容既有表结构；新礼物不读取',
		  agency_point_ratio_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '兼容既有表结构；新礼物不读取',
		  points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 展示换算比例',
		  withdraw_fee_bps INT NOT NULL DEFAULT 500 COMMENT '提现手续费 bps',
		  rule_json JSON NOT NULL COMMENT '完整政策快照',
		  published_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
		  published_at_ms BIGINT NOT NULL DEFAULT 0,
		  created_at_ms BIGINT NOT NULL,
		  updated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, instance_code, region_id),
		  KEY idx_wallet_policy_active (app_code, region_id, status, effective_from_ms, effective_to_ms)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包运行侧政策实例表'`)
	if err != nil {
		return err
	}
	if _, err := db.ExecContext(ctx, `ALTER TABLE wallet_policy_instances ADD COLUMN agency_point_ratio_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '兼容既有表结构；新礼物不读取' AFTER host_point_ratio_ppm`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	_, err = db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS agency_point_share_entries (
		  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
		  transaction_id VARCHAR(96) NOT NULL,
		  host_user_id BIGINT NOT NULL,
		  agency_owner_user_id BIGINT NOT NULL,
		  sender_user_id BIGINT NOT NULL,
		  room_id VARCHAR(96) NOT NULL DEFAULT '',
		  point_delta BIGINT NOT NULL,
		  ratio_ppm BIGINT NOT NULL,
		  share_base_type VARCHAR(32) NOT NULL DEFAULT 'unknown',
		  share_base_amount BIGINT NOT NULL DEFAULT 0,
		  policy_instance_code VARCHAR(96) NOT NULL DEFAULT '',
		  created_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, transaction_id, agency_owner_user_id),
		  KEY idx_agency_point_share_owner_time (app_code, agency_owner_user_id, created_at_ms),
		  KEY idx_agency_point_share_host_time (app_code, host_user_id, created_at_ms)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='旧 Agency POINT 流水结构；新送礼不读写'`)
	if err != nil {
		return err
	}
	if _, err := db.ExecContext(ctx, `ALTER TABLE agency_point_share_entries ADD COLUMN share_base_type VARCHAR(32) NOT NULL DEFAULT 'unknown' COMMENT '分成基数：unknown/charge_amount/host_income' AFTER ratio_ppm`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	if _, err := db.ExecContext(ctx, `ALTER TABLE agency_point_share_entries ADD COLUMN share_base_amount BIGINT NOT NULL DEFAULT 0 COMMENT '本次分成计算基数；unknown 时不可解释' AFTER share_base_type`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	return nil
}

// ensurePointWithdrawalCoinSellerSchema 创建跨 Fami/Huwaa/Lalu 共用、按 app_code 隔离的提现币商配置。
// 表放在 wallet 数据库，保证兑换时读取比例和写账本处于同一可信服务边界，H5 不能自报比例。
func ensurePointWithdrawalCoinSellerSchema(ctx context.Context, db *sql.DB) error {
	_, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS point_withdrawal_coin_seller_configs (
		  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
		  seller_user_id BIGINT NOT NULL COMMENT '币商用户 ID',
		  sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序，越大越靠前',
		  point_amount BIGINT NOT NULL COMMENT '兑换比例中的 POINT 分母',
		  seller_coin_amount BIGINT NOT NULL COMMENT '兑换比例中的币商金币分子',
		  service_country_codes JSON NOT NULL COMMENT '服务国家码数组；空数组表示不限国家',
		  service_region_ids JSON NULL COMMENT '服务区域 ID 数组；与国家数组互斥，两个数组都为空表示全量',
		  status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
		  created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
		  updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
		  created_at_ms BIGINT NOT NULL,
		  updated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, seller_user_id),
		  KEY idx_point_withdraw_seller_list (app_code, status, sort_order, seller_user_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='POINT 提现币商白名单与兑换比例'`)
	if err != nil {
		return err
	}
	// 配置表按 App/币商唯一，数据量很小；INSTANT 补 nullable JSON 列不复制表，NULL 与空数组都解释为未限制区域。
	if _, err := db.ExecContext(ctx, `
		ALTER TABLE point_withdrawal_coin_seller_configs
		ADD COLUMN service_region_ids JSON NULL COMMENT '服务区域 ID 数组；与国家数组互斥，两个数组都为空表示全量'
		AFTER service_country_codes, ALGORITHM=INSTANT`); err != nil && !isDuplicateColumnError(err) {
		return err
	}
	return nil
}

func ensureGiftDiamondRatioSchema(ctx context.Context, db *sql.DB) error {
	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs (
		  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
		  region_id BIGINT NOT NULL DEFAULT 0 COMMENT '房间可见区域，0 表示全局默认',
		  gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码',
		  status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
		  ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '房间贡献热度和主播周期钻石入账比例，百分比',
		  coin_return_ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '收礼人金币返还比例，百分比',
		  created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID',
		  updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新后台用户 ID',
		  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
		  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
		  PRIMARY KEY (app_code, region_id, gift_type_code),
		  KEY idx_gift_diamond_ratio_region (app_code, region_id, status)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物钻石和房间贡献比例配置表'`); err != nil {
		return err
	}
	if _, err := ensureGiftReturnCoinRatioColumn(ctx, db); err != nil {
		return err
	}
	// 礼物倍率是运营显式发布的运行政策。启动只维护表结构，绝不能按 App 名自动生成
	// 100/10/1；已存在的正式记录原样保留，无配置时送礼链路会在扣费前失败关闭。
	return nil
}

func ensureGiftReturnCoinRatioColumn(ctx context.Context, db *sql.DB) (bool, error) {

	if _, err := db.ExecContext(ctx, `
		ALTER TABLE gift_diamond_ratio_configs
		ADD COLUMN coin_return_ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '收礼人金币返还比例，百分比' AFTER ratio_percent`); err != nil {
		if isDuplicateColumnError(err) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

func ensureCoinSellerSalaryExchangeRateSchema(ctx context.Context, db *sql.DB) error {
	_, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS coin_seller_salary_exchange_rate_tiers (
		  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
		  region_id BIGINT NOT NULL COMMENT '币商所属区域 ID',
		  tier_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '区间 ID',
		  min_usd_minor BIGINT NOT NULL COMMENT '起始美元金额，单位美分，包含',
		  max_usd_minor BIGINT NOT NULL COMMENT '结束美元金额，单位美分，包含',
		  coin_per_usd BIGINT NOT NULL COMMENT '每 1 USD 工资可兑换的币商专用金币数量',
		  status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT '状态：active/disabled',
		  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
		  created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
		  updated_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新人用户 ID',
		  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
		  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
		  PRIMARY KEY (tier_id),
		  KEY idx_coin_seller_salary_rates_region (app_code, region_id, status, min_usd_minor, max_usd_minor),
		  KEY idx_coin_seller_salary_rates_sort (app_code, region_id, status, sort_order)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='币商工资兑换比例区间表'`)
	return err
}

func ensureUserResourceEquipmentSchema(ctx context.Context, db *sql.DB) error {

	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS user_resource_equipment (
		  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
		  user_id BIGINT NOT NULL COMMENT '用户 ID',
		  resource_type VARCHAR(32) NOT NULL COMMENT '资源类型',
		  entitlement_id VARCHAR(96) NOT NULL COMMENT '权益 ID',
		  resource_id BIGINT NOT NULL COMMENT '资源 ID',
		  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
		  PRIMARY KEY (app_code, user_id, resource_type, entitlement_id),
		  KEY idx_user_resource_equipment_user (app_code, user_id, updated_at_ms)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源装备表'`); err != nil {
		return err
	}

	primaryColumns, err := tablePrimaryKeyColumns(ctx, db, "user_resource_equipment")
	if err != nil {
		return err
	}
	if strings.Join(primaryColumns, ",") == "app_code,user_id,resource_type" {
		if _, err = db.ExecContext(ctx, `
			ALTER TABLE user_resource_equipment
			DROP PRIMARY KEY,
			ADD PRIMARY KEY (app_code, user_id, resource_type, entitlement_id)`); err != nil {
			return err
		}
	}

	// 清理游标是单行小表；后台每页与装备删除同事务推进，节点滚动或 leader 切换后
	// 不会回到表头反复扫描。表创建不触碰 equipment 大表，也不需要在线索引变更。
	if _, err = db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS wallet_resource_equipment_cleanup_state (
		  job_name VARCHAR(64) NOT NULL COMMENT 'Wallet owner 清理任务稳定名称',
		  cursor_app_code VARCHAR(32) NOT NULL DEFAULT '' COMMENT '装备主键游标 App',
		  cursor_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '装备主键游标用户',
		  cursor_resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '装备主键游标资源类型',
		  cursor_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '装备主键游标权益',
		  updated_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '游标提交时间，UTC epoch ms',
		  PRIMARY KEY (job_name)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Wallet 资源装备物理清理游标'`); err != nil {
		return err
	}

	// 命令表是 append-only 幂等事实，主键查询不会扫描 equipment 或 entitlement 大表。
	// response_json 固化首个成功回包，使资源随后到期/下架也不会改变同 command_id 的重试语义。
	_, err = db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS user_resource_equipment_commands (
		  app_code VARCHAR(32) NOT NULL COMMENT '应用编码，用于多租户隔离',
		  command_id VARCHAR(128) NOT NULL COMMENT '装备业务幂等 ID',
		  request_hash CHAR(64) NOT NULL COMMENT '用户、资源和指定 entitlement 的请求语义哈希',
		  user_id BIGINT NOT NULL COMMENT '操作用户 ID',
		  resource_id BIGINT NOT NULL COMMENT '目标资源 ID',
		  requested_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '客户端指定的 entitlement；空表示服务端选择',
		  result_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首个成功命中的 entitlement',
		  resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '首个成功结果的资源类型',
		  response_json JSON NULL COMMENT '首个成功 UserResourceEntitlement 回包快照',
		  created_at_ms BIGINT NOT NULL COMMENT '首次命令时间，UTC epoch ms',
		  updated_at_ms BIGINT NOT NULL COMMENT '结果完成时间，UTC epoch ms',
		  PRIMARY KEY (app_code, command_id)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源装备命令幂等事实表'`)
	return err
}

// ensureVipBenefitResourceBindingHistorySchema 保证任何曾被 tiered VIP 绑定的资源都不会在
// 后台改绑、删除权益或关闭 program 后退化成普通资源。历史表只随小型配置矩阵追加，运行时
// 装备门禁按 (app_code,resource_id) 覆盖索引点查，不扫描用户 entitlement/equipment 流水。
func ensureVipBenefitResourceBindingHistorySchema(ctx context.Context, db *sql.DB) error {
	if _, err := db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS vip_benefit_resource_binding_history (
		  app_code VARCHAR(32) NOT NULL COMMENT '应用编码，用于多租户隔离',
		  history_id CHAR(64) NOT NULL COMMENT '配置版本、阶段、等级和权益的稳定哈希',
		  config_version BIGINT NOT NULL COMMENT '写入该快照的 VIP 配置版本',
		  change_phase VARCHAR(16) NOT NULL COMMENT 'baseline/before/after',
		  program_type VARCHAR(32) NOT NULL COMMENT '快照时的 VIP program 类型',
		  level INT NOT NULL COMMENT 'VIP 等级',
		  benefit_code VARCHAR(64) NOT NULL COMMENT '权益编码',
		  benefit_status VARCHAR(32) NOT NULL COMMENT '权益快照状态',
		  trial_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '体验卡是否可用快照',
		  resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '当时绑定资源 ID；0 表示未绑定',
		  resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '当时要求的资源类型',
		  auto_equip BOOLEAN NOT NULL DEFAULT FALSE COMMENT '当时自动装备策略',
		  recorded_at_ms BIGINT NOT NULL COMMENT '记录时间，UTC epoch ms',
		  PRIMARY KEY (app_code, history_id),
		  UNIQUE KEY uk_vip_binding_history_snapshot (app_code, config_version, change_phase, level, benefit_code),
		  KEY idx_vip_binding_history_resource (app_code, resource_id, program_type, benefit_code, level)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 权益资源绑定不可变历史'`); err != nil {
		return err
	}
	// baseline 只复制当前配置小表，并通过唯一键保持启动幂等。历史资源从此即使被 DELETE
	// 也可被装备/appearance 门禁识别；不触碰任何用户级大表。
	_, err := db.ExecContext(ctx, `
		INSERT IGNORE INTO vip_benefit_resource_binding_history (
		  app_code, history_id, config_version, change_phase, program_type, level,
		  benefit_code, benefit_status, trial_enabled, resource_id, resource_type,
		  auto_equip, recorded_at_ms
		)
		SELECT b.app_code,
		       SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256),
		       p.config_version, 'baseline', p.program_type, b.level, b.benefit_code,
		       b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip,
		       GREATEST(b.updated_at_ms, p.updated_at_ms)
		FROM vip_level_benefits AS b
		JOIN vip_program_configs AS p ON p.app_code = b.app_code`)
	return err
}

func tablePrimaryKeyColumns(ctx context.Context, db *sql.DB, tableName string) ([]string, error) {
	rows, err := db.QueryContext(ctx, `
		SELECT COLUMN_NAME
		FROM information_schema.KEY_COLUMN_USAGE
		WHERE TABLE_SCHEMA = DATABASE()
		  AND TABLE_NAME = ?
		  AND CONSTRAINT_NAME = 'PRIMARY'
		ORDER BY ORDINAL_POSITION ASC`, tableName)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	columns := make([]string, 0, 4)
	for rows.Next() {
		var column string
		if err := rows.Scan(&column); err != nil {
			return nil, err
		}
		columns = append(columns, column)
	}
	return columns, rows.Err()
}

func tableIndexColumns(ctx context.Context, db *sql.DB, tableName string, indexName string) ([]string, error) {
	rows, err := db.QueryContext(ctx, `
		SELECT COLUMN_NAME
		FROM information_schema.STATISTICS
		WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?
		ORDER BY SEQ_IN_INDEX ASC`, tableName, indexName)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	columns := make([]string, 0, 4)
	for rows.Next() {
		var column string
		if err := rows.Scan(&column); err != nil {
			return nil, err
		}
		columns = append(columns, column)
	}
	return columns, rows.Err()
}
