SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;

USE hyapp_user;

-- refresh token rotation family + 30 秒幂等结果。
--
-- 性能边界：auth_sessions 是登录热表，新增列均为 NOT NULL 常量默认值，MySQL 8.0 使用
-- ALGORITHM=INSTANT 只改数据字典，不回填全表。MySQL 8.4 禁止 INSTANT 同时显式指定 LOCK 子句；
-- 若目标实例不支持 INSTANT，语句会直接失败，
-- 不能静默退化成 COPY；应在低峰期用云数据库 Online DDL 工具执行同列变更。
-- family 索引必须扫描 auth_sessions 构建 B-Tree，因此单独使用 INPLACE/LOCK=NONE；它不复制表，但会消耗 IO/CPU，
-- 6 列合并为一次 INSTANT ALTER，只获取一次 MDL 并消耗一个 instant row version；目标表接近 row-version
-- 上限时仍应先检查 INFORMATION_SCHEMA.INNODB_TABLES.TOTAL_ROW_VERSIONS。
SET @auth_session_column_ddl := CONCAT_WS(', ',
  IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND COLUMN_NAME = 'token_family_id') = 0,
    'ADD COLUMN token_family_id VARCHAR(96) NOT NULL DEFAULT '''' COMMENT ''令牌族根 session ID''', NULL),
  IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND COLUMN_NAME = 'generation') = 0,
    'ADD COLUMN generation BIGINT NOT NULL DEFAULT 0 COMMENT ''令牌族内单调代际，首代为 0''', NULL),
  IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND COLUMN_NAME = 'parent_session_id') = 0,
    'ADD COLUMN parent_session_id VARCHAR(96) NOT NULL DEFAULT '''' COMMENT ''直接父代 session ID''', NULL),
  IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND COLUMN_NAME = 'rotated_to_session_id') = 0,
    'ADD COLUMN rotated_to_session_id VARCHAR(96) NOT NULL DEFAULT '''' COMMENT ''轮换产生的直接子代 session ID''', NULL),
  IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND COLUMN_NAME = 'rotation_at_ms') = 0,
    'ADD COLUMN rotation_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT ''轮换时间，UTC epoch ms''', NULL),
  IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND COLUMN_NAME = 'rotation_request_id') = 0,
    'ADD COLUMN rotation_request_id VARCHAR(96) NOT NULL DEFAULT '''' COMMENT ''首次轮换 refresh 幂等键''', NULL)
);
SET @ddl := IF(
  @auth_session_column_ddl = '',
  'SELECT 1',
  CONCAT('ALTER TABLE auth_sessions ', @auth_session_column_ddl, ', ALGORITHM=INSTANT')
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS auth_refresh_outcomes (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  parent_session_id VARCHAR(96) NOT NULL COMMENT '被消费的父 session ID',
  child_session_id VARCHAR(96) NOT NULL COMMENT '首次轮换产生的子 session ID',
  refresh_request_id VARCHAR(96) NOT NULL COMMENT '客户端 refresh 幂等键；旧客户端由 token hash 派生',
  token_ciphertext VARBINARY(4096) NOT NULL COMMENT 'AES-GCM 加密后的完整 token pair',
  created_at_ms BIGINT NOT NULL COMMENT '轮换时间，UTC epoch ms',
  expires_at_ms BIGINT NOT NULL COMMENT '幂等密文过期时间，UTC epoch ms',
  PRIMARY KEY (app_code, parent_session_id),
  KEY idx_auth_refresh_outcomes_request (app_code, refresh_request_id),
  KEY idx_auth_refresh_outcomes_expires (expires_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='refresh token 轮换短期幂等结果';

CREATE TABLE IF NOT EXISTS auth_session_denylist_jobs (
  job_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '补偿任务 ID',
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  session_id VARCHAR(96) NOT NULL COMMENT '需要写入 gateway Redis denylist 的 session ID',
  reason VARCHAR(64) NOT NULL COMMENT 'REFRESH_ROTATED、USER_LOGOUT 或 REFRESH_TOKEN_REUSE',
  status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/processing/retryable/delivered',
  attempts INT NOT NULL DEFAULT 0 COMMENT '认领次数',
  next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '下次重试时间，UTC epoch ms',
  locked_by VARCHAR(96) NOT NULL DEFAULT '' COMMENT '当前 worker ID',
  locked_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT '处理租约截止时间，UTC epoch ms',
  last_error VARCHAR(512) NOT NULL DEFAULT '' COMMENT '最近失败摘要，不含 token 原文',
  deny_until_ms BIGINT NOT NULL COMMENT '该 sid 历史 access JWT 最晚拒绝时刻（含安全余量），UTC epoch ms',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  UNIQUE KEY uk_auth_session_denylist_job (app_code, session_id),
  KEY idx_auth_session_denylist_pending (status, next_retry_at_ms, job_id),
  KEY idx_auth_session_denylist_cleanup (deny_until_ms, job_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='session family 撤销到 Redis denylist 的持久补偿任务';

-- 本 migration 可能在早期试运行版本创建过 job 表；补列使用 INSTANT，不扫描业务热表。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_session_denylist_jobs' AND COLUMN_NAME = 'deny_until_ms') = 0,
  'ALTER TABLE auth_session_denylist_jobs ADD COLUMN deny_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT ''历史 access JWT 最晚拒绝时刻（含安全余量），UTC epoch ms'', ALGORITHM=INSTANT',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 仅早期试运行 job 表需要一次有界回填；它不是 auth_sessions 热表。按历史最大 7d access TTL + 60s
-- 安全余量恢复 deadline，避免新增列默认 0 后把仍有效保护事实立即清理；生产仍应在低峰观察受影响行数。
UPDATE auth_session_denylist_jobs
SET deny_until_ms = created_at_ms + 604860000
WHERE deny_until_ms = 0;

-- 上线前版本允许同 sid 按 reason 多行；先合并为 deny_until 最大的一行，再改成每 sid 唯一，避免短 TTL job 覆盖长 TTL。
DELETE older
FROM auth_session_denylist_jobs older
JOIN auth_session_denylist_jobs keeper
  ON keeper.app_code = older.app_code
 AND keeper.session_id = older.session_id
 AND (keeper.deny_until_ms > older.deny_until_ms
   OR (keeper.deny_until_ms = older.deny_until_ms AND keeper.job_id > older.job_id));

SET @denylist_unique_columns := (
  SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ',')
  FROM information_schema.STATISTICS
  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_session_denylist_jobs' AND INDEX_NAME = 'uk_auth_session_denylist_job'
);
SET @denylist_unique_non_unique := (
  SELECT MIN(NON_UNIQUE)
  FROM information_schema.STATISTICS
  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_session_denylist_jobs' AND INDEX_NAME = 'uk_auth_session_denylist_job'
);
SET @ddl := CASE
  WHEN @denylist_unique_columns IS NULL THEN
    'ALTER TABLE auth_session_denylist_jobs ADD UNIQUE INDEX uk_auth_session_denylist_job (app_code, session_id), ALGORITHM=INPLACE, LOCK=NONE'
  WHEN @denylist_unique_columns <> 'app_code,session_id' OR @denylist_unique_non_unique <> 0 THEN
    'ALTER TABLE auth_session_denylist_jobs DROP INDEX uk_auth_session_denylist_job, ADD UNIQUE INDEX uk_auth_session_denylist_job (app_code, session_id), ALGORITHM=INPLACE, LOCK=NONE'
  ELSE 'SELECT 1'
END;
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @denylist_cleanup_columns := (
  SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ',')
  FROM information_schema.STATISTICS
  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_session_denylist_jobs' AND INDEX_NAME = 'idx_auth_session_denylist_cleanup'
);
SET @ddl := CASE
  WHEN @denylist_cleanup_columns IS NULL THEN
    'ALTER TABLE auth_session_denylist_jobs ADD INDEX idx_auth_session_denylist_cleanup (deny_until_ms, job_id), ALGORITHM=INPLACE, LOCK=NONE'
  WHEN @denylist_cleanup_columns <> 'deny_until_ms,job_id' THEN
    'ALTER TABLE auth_session_denylist_jobs DROP INDEX idx_auth_session_denylist_cleanup, ADD INDEX idx_auth_session_denylist_cleanup (deny_until_ms, job_id), ALGORITHM=INPLACE, LOCK=NONE'
  ELSE 'SELECT 1'
END;
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @family_index_columns := (
  SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ',')
  FROM information_schema.STATISTICS
  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND INDEX_NAME = 'idx_auth_sessions_token_family'
);
SET @family_index_non_unique := (
  SELECT MIN(NON_UNIQUE)
  FROM information_schema.STATISTICS
  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'auth_sessions' AND INDEX_NAME = 'idx_auth_sessions_token_family'
);
SET @ddl := CASE
  WHEN @family_index_columns IS NULL THEN
    'ALTER TABLE auth_sessions ADD INDEX idx_auth_sessions_token_family (app_code, token_family_id, generation), ALGORITHM=INPLACE, LOCK=NONE'
  WHEN @family_index_columns <> 'app_code,token_family_id,generation' OR @family_index_non_unique <> 1 THEN
    'ALTER TABLE auth_sessions DROP INDEX idx_auth_sessions_token_family, ADD INDEX idx_auth_sessions_token_family (app_code, token_family_id, generation), ALGORITHM=INPLACE, LOCK=NONE'
  ELSE 'SELECT 1'
END;
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
