// Package mysql 实现 user-service 的 MySQL 持久化边界。
package mysql

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

	"hyapp/pkg/xerr"
)

// Repository 是 user-service 的 MySQL 持久化入口。
// 方法按领域拆到 user_repository、identity_repository 和 auth_repository，避免一个文件混合多条业务线。
type Repository struct {
	// db 是标准库连接池，事务和普通查询都从这里创建。
	db *sql.DB
}

// Open 创建 MySQL 连接池并做启动 ping。
func Open(ctx context.Context, dsn string) (*Repository, error) {
	if strings.TrimSpace(dsn) == "" {
		// 本地和线上运行都必须显式提供 DSN；空 DSN 代表应用装配错误。
		return nil, xerr.New(xerr.InvalidArgument, "mysql_dsn is required")
	}

	// sql.Open 只创建连接池，真实连通性由 PingContext 验证。
	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 {
		// 启动探测失败时关闭连接池，避免返回半初始化 repository。
		_ = db.Close()
		return nil, err
	}

	return &Repository{db: db}, nil
}

// Migrate 补齐允许服务启动时在线迁移的向后兼容字段和索引。
func (r *Repository) Migrate(ctx context.Context) error {
	if r == nil || r.db == nil {
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	// app_version/build_number 必须先于新版登录审计 INSERT 落地；逐列探测使旧库可滚动升级，
	// 同时避免对已经执行 initdb 或离线 migration 的数据库重复 ALTER。
	if err := r.ensureColumn(ctx, "login_audit", "app_version", "app_version VARCHAR(64) NOT NULL DEFAULT '' COMMENT '本次认证客户端版本' AFTER platform"); err != nil {
		return err
	}
	if err := r.ensureColumn(ctx, "login_audit", "build_number", "build_number VARCHAR(64) NOT NULL DEFAULT '' COMMENT '本次认证客户端构建号' AFTER app_version"); err != nil {
		return err
	}
	// 外管团队字段全部是可空、无默认值的元数据列，滚动升级时允许 INSTANT 补列；
	// 历史 App 关系不做回填，索引构建仍由 024 离线 migration 完成，启动路径绝不能扫描 BD/邀请业务表。
	externalTeamColumns := []struct {
		table string
		name  string
		ddl   string
	}{
		{table: "bd_profiles", name: "parent_owner_user_id", ddl: "parent_owner_user_id BIGINT NULL COMMENT '外管团队直接 owner 用户 ID' AFTER parent_leader_user_id"},
		{table: "role_invitations", name: "external_operator_user_id", ddl: "external_operator_user_id BIGINT NULL COMMENT '外管邀请操作者 App 用户 ID' AFTER target_user_id"},
		{table: "role_invitations", name: "parent_owner_user_id", ddl: "parent_owner_user_id BIGINT NULL COMMENT '邀请创建时固化的外管团队 owner 用户 ID' AFTER parent_leader_user_id"},
		{table: "role_invitations", name: "parent_agency_id", ddl: "parent_agency_id BIGINT NULL COMMENT 'Host 邀请创建时固化的 Agency ID' AFTER parent_owner_user_id"},
	}
	for _, column := range externalTeamColumns {
		if err := r.ensureInstantColumn(ctx, column.table, column.name, column.ddl); err != nil {
			return err
		}
	}
	// 团队列表和邀请父级校验都要求先按 owner 缩小结果集；索引缺失时宁可拒绝启动，
	// 也不能在外管页面流量下退化成租户级全表扫描。
	if err := r.requireIndexDefinition(ctx, "bd_profiles", "idx_bd_profiles_parent_owner", []string{"app_code", "parent_owner_user_id", "status", "user_id"}, false); err != nil {
		return fmt.Errorf("%w; apply migration 024 before startup", err)
	}
	if err := r.requireIndexDefinition(ctx, "role_invitations", "idx_role_invitations_parent_owner", []string{"app_code", "parent_owner_user_id", "status", "created_at_ms"}, false); err != nil {
		return fmt.Errorf("%w; apply migration 024 before startup", err)
	}
	// auth_sessions 是热表；只允许 INSTANT 常量默认列在启动迁移中执行，绝不在进程启动时隐式 COPY 大表或构建 family 索引。
	instantSessionColumns := []struct {
		name string
		ddl  string
	}{
		{name: "token_family_id", ddl: "token_family_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '令牌族根 session ID'"},
		{name: "generation", ddl: "generation BIGINT NOT NULL DEFAULT 0 COMMENT '令牌族内单调代际，首代为 0'"},
		{name: "parent_session_id", ddl: "parent_session_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '直接父代 session ID'"},
		{name: "rotated_to_session_id", ddl: "rotated_to_session_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '轮换产生的直接子代 session ID'"},
		{name: "rotation_at_ms", ddl: "rotation_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '轮换时间，UTC epoch ms'"},
		{name: "rotation_request_id", ddl: "rotation_request_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首次轮换 refresh 幂等键'"},
	}
	for _, column := range instantSessionColumns {
		if err := r.ensureInstantColumn(ctx, "auth_sessions", column.name, column.ddl); err != nil {
			return err
		}
	}
	// family 索引会扫描登录热表，只能由离线 018 在低峰构建；缺失/同名错列时启动直接失败，不能静默全表查询。
	if err := r.requireIndexDefinition(ctx, "auth_sessions", "idx_auth_sessions_token_family", []string{"app_code", "token_family_id", "generation"}, false); err != nil {
		return err
	}
	// 独立小表创建不扫描 auth_sessions；family 索引仍只允许由离线 018 migration 在低峰用 INPLACE/LOCK=NONE 创建。
	if _, err := r.db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS auth_refresh_outcomes (
		  app_code VARCHAR(32) NOT NULL,
		  parent_session_id VARCHAR(96) NOT NULL,
		  child_session_id VARCHAR(96) NOT NULL,
		  refresh_request_id VARCHAR(96) NOT NULL,
		  token_ciphertext VARBINARY(4096) NOT NULL,
		  created_at_ms BIGINT NOT NULL,
		  expires_at_ms BIGINT NOT NULL,
		  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
	`); err != nil {
		return err
	}
	if _, err := r.db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS auth_session_denylist_jobs (
		  job_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
		  app_code VARCHAR(32) NOT NULL,
		  session_id VARCHAR(96) NOT NULL,
		  reason VARCHAR(64) NOT NULL,
		  status VARCHAR(16) NOT NULL DEFAULT 'pending',
		  attempts INT NOT NULL DEFAULT 0,
		  next_retry_at_ms BIGINT NOT NULL DEFAULT 0,
		  locked_by VARCHAR(96) NOT NULL DEFAULT '',
		  locked_until_ms BIGINT NOT NULL DEFAULT 0,
		  last_error VARCHAR(512) NOT NULL DEFAULT '',
		  deny_until_ms BIGINT NOT NULL DEFAULT 0,
		  created_at_ms BIGINT NOT NULL,
		  updated_at_ms BIGINT NOT NULL,
		  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
	`); err != nil {
		return err
	}
	if err := r.ensureInstantColumn(ctx, "auth_session_denylist_jobs", "deny_until_ms", "deny_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT '历史 access JWT 最晚拒绝时刻（含安全余量），UTC epoch ms'"); err != nil {
		return err
	}
	// 从旧三列 unique 收敛到每 sid 一行前必须先离线去重；启动时不能先 DROP 后因重复 ADD 失败而留下无唯一约束表。
	if err := r.requireIndexDefinition(ctx, "auth_session_denylist_jobs", "uk_auth_session_denylist_job", []string{"app_code", "session_id"}, true); err != nil {
		return err
	}
	if err := r.ensureIndexDefinition(ctx,
		"auth_session_denylist_jobs",
		"idx_auth_session_denylist_cleanup",
		[]string{"deny_until_ms", "job_id"},
		"ALTER TABLE auth_session_denylist_jobs ADD INDEX idx_auth_session_denylist_cleanup (deny_until_ms, job_id)",
	); err != nil {
		return err
	}
	// Dashboard 必须先按用户分区，再按 created_at_ms/id 取最近成功登录。校验完整列序而不只检查索引名，
	// 防止历史环境中同名但列定义不完整的索引让查询退化成全表排序。
	return r.ensureIndexDefinition(ctx,
		"login_audit",
		"idx_login_audit_latest_success",
		[]string{"app_code", "user_id", "result", "blocked", "login_type", "created_at_ms", "id"},
		"ALTER TABLE login_audit ADD INDEX idx_login_audit_latest_success (app_code, user_id, result, blocked, login_type, created_at_ms, id)",
	)
}

func (r *Repository) requireIndexDefinition(ctx context.Context, tableName string, indexName string, columns []string, unique bool) error {
	rows, err := r.db.QueryContext(ctx, `
		SELECT COLUMN_NAME, NON_UNIQUE
		FROM information_schema.STATISTICS
		WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ?
		ORDER BY SEQ_IN_INDEX
	`, tableName, indexName)
	if err != nil {
		return err
	}
	defer rows.Close()
	existing := make([]string, 0, len(columns))
	nonUnique := -1
	for rows.Next() {
		var column string
		var rowNonUnique int
		if err := rows.Scan(&column, &rowNonUnique); err != nil {
			return err
		}
		if nonUnique == -1 {
			nonUnique = rowNonUnique
		} else if nonUnique != rowNonUnique {
			return fmt.Errorf("required index %s.%s has inconsistent NON_UNIQUE metadata", tableName, indexName)
		}
		existing = append(existing, column)
	}
	if err := rows.Err(); err != nil {
		return err
	}
	wantNonUnique := 1
	if unique {
		wantNonUnique = 0
	}
	if !equalStringSlices(existing, columns) || nonUnique != wantNonUnique {
		return fmt.Errorf("required index %s.%s has columns %v and NON_UNIQUE=%d, want columns %v and NON_UNIQUE=%d; apply migration 018 before startup", tableName, indexName, existing, nonUnique, columns, wantNonUnique)
	}
	return nil
}

func (r *Repository) ensureInstantColumn(ctx context.Context, tableName string, columnName string, columnDDL string) error {
	var count int
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
		tableName, columnName,
	).Scan(&count); err != nil {
		return err
	}
	if count > 0 {
		return nil
	}
	// 固定 DDL 若不支持 INSTANT 会直接失败，让操作者改走低峰 Online DDL，而不是启动时静默全表重建。
	// MySQL 8.4 的 INSTANT 算法不接受显式 LOCK 子句；只指定算法仍保证不退化成 COPY/INPLACE。
	_, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` ADD COLUMN `+columnDDL+`, ALGORITHM=INSTANT`)
	return err
}

func (r *Repository) ensureColumn(ctx context.Context, tableName string, columnName string, columnDDL string) error {
	var count int
	if err := r.db.QueryRowContext(ctx,
		`SELECT COUNT(*)
		   FROM information_schema.COLUMNS
		  WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
		tableName, columnName,
	).Scan(&count); err != nil {
		return err
	}
	if count > 0 {
		return nil
	}
	// tableName/columnDDL 只来自上方固定常量，不接受外部输入；这里不能用占位符绑定 DDL 标识符。
	_, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` ADD COLUMN `+columnDDL)
	return err
}

func (r *Repository) ensureIndexDefinition(ctx context.Context, tableName string, indexName string, columns []string, createDDL string) error {
	rows, err := r.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`,
		tableName, indexName,
	)
	if err != nil {
		return err
	}
	defer rows.Close()
	existing := make([]string, 0, len(columns))
	for rows.Next() {
		var column string
		if err := rows.Scan(&column); err != nil {
			return err
		}
		existing = append(existing, column)
	}
	if err := rows.Err(); err != nil {
		return err
	}
	if equalStringSlices(existing, columns) {
		return nil
	}
	if len(existing) > 0 {
		// 同名索引定义不一致时必须先移除，否则 ADD INDEX 会以 duplicate key name 失败。
		if _, err := r.db.ExecContext(ctx, `ALTER TABLE `+tableName+` DROP INDEX `+indexName); err != nil {
			return err
		}
	}
	_, err = r.db.ExecContext(ctx, createDDL)
	return err
}

func equalStringSlices(left []string, right []string) bool {
	if len(left) != len(right) {
		return false
	}
	for index := range left {
		if left[index] != right[index] {
			return false
		}
	}
	return true
}

// Close 释放 MySQL 连接池。
func (r *Repository) Close() error {
	if r == nil || r.db == nil {
		// Close 允许重复调用和 nil 调用，方便 App.Close 幂等释放。
		return nil
	}

	return r.db.Close()
}

// Ping 验证 MySQL 当前可用。
func (r *Repository) Ping(ctx context.Context) error {
	if r == nil || r.db == nil {
		// 没有连接池时明确返回 unavailable。
		return xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}

	return r.db.PingContext(ctx)
}
