package user

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

	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	userdomain "hyapp/services/user-service/internal/domain/user"
	userservice "hyapp/services/user-service/internal/service/user"
)

const adminBanStatusReasonPrefix = "admin_user_ban:"

// CreateAdminUserBan 在一个事务内写封禁事实、切换 users.status、写审计并吊销 refresh session。
// 目标已经因其他事实处于 banned 时只叠加事实，不重复写 banned->banned 日志；该约束也是历史永久封禁不被误恢复的判据。
func (r *Repository) CreateAdminUserBan(ctx context.Context, command userservice.AdminUserBanCommand) (userservice.AdminUserBanPersistenceResult, error) {
	if r == nil || r.db == nil {
		return userservice.AdminUserBanPersistenceResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	appCode := appcode.FromContext(ctx)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userservice.AdminUserBanPersistenceResult{}, err
	}
	defer func() { _ = tx.Rollback() }()

	var oldStatus string
	if err := tx.QueryRowContext(ctx, `
		SELECT status
		FROM users
		WHERE app_code = ? AND user_id = ?
		FOR UPDATE`, appCode, command.TargetUserID).Scan(&oldStatus); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return userservice.AdminUserBanPersistenceResult{}, xerr.New(xerr.NotFound, "user not found")
		}
		return userservice.AdminUserBanPersistenceResult{}, err
	}
	if userdomain.Status(oldStatus) != userdomain.StatusActive && userdomain.Status(oldStatus) != userdomain.StatusBanned {
		// disabled 是独立平台停用语义，不能用封禁事实覆盖；后台应先显式恢复后再封禁。
		return userservice.AdminUserBanPersistenceResult{}, xerr.New(xerr.InvalidArgument, "target user status cannot be banned")
	}

	banID := idgen.New("admin_ban")
	_, err = tx.ExecContext(ctx, `
		INSERT INTO admin_user_bans (
			app_code, ban_id, command_id, target_user_id, target_old_status,
			expires_at_ms, status, reason, operator_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)`,
		appCode, banID, command.CommandID, command.TargetUserID, oldStatus,
		command.ExpiresAtMs, command.Reason, command.OperatorAdminID, command.NowMs, command.NowMs,
	)
	if err != nil {
		if adminBanDuplicate(err) {
			// 幂等命令可能在上一次调用中已经完整提交；回滚当前事务后读取既有事实和当前用户状态即可。
			_ = tx.Rollback()
			ban, lookupErr := r.getAdminUserBanByCommand(ctx, command.CommandID)
			if lookupErr != nil {
				return userservice.AdminUserBanPersistenceResult{}, lookupErr
			}
			if ban.TargetUserID != command.TargetUserID || ban.OperatorAdminID != command.OperatorAdminID || ban.ExpiresAtMs != command.ExpiresAtMs || ban.Reason != command.Reason {
				// command_id 是完整请求的幂等边界；同一 key 不能静默复用到另一位用户或另一种封禁期限。
				return userservice.AdminUserBanPersistenceResult{}, xerr.New(xerr.Conflict, "admin ban command payload does not match existing fact")
			}
			user, lookupErr := r.GetUser(ctx, ban.TargetUserID)
			if lookupErr != nil {
				return userservice.AdminUserBanPersistenceResult{}, lookupErr
			}
			return userservice.AdminUserBanPersistenceResult{Ban: ban, Status: userservice.UserStatusPersistenceResult{User: user}}, nil
		}
		return userservice.AdminUserBanPersistenceResult{}, err
	}

	sessionIDs := make([]string, 0)
	statusChanged := userdomain.Status(oldStatus) == userdomain.StatusActive
	if statusChanged {
		if _, err := tx.ExecContext(ctx, `
			UPDATE users SET status = 'banned', updated_at_ms = ?
			WHERE app_code = ? AND user_id = ?`, command.NowMs, appCode, command.TargetUserID); err != nil {
			return userservice.AdminUserBanPersistenceResult{}, err
		}
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO user_status_change_logs (
				app_code, target_user_id, old_status, new_status, operator_type,
				operator_user_id, reason, request_id, created_at_ms
			) VALUES (?, ?, ?, 'banned', 'admin', ?, ?, ?, ?)`,
			appCode, command.TargetUserID, oldStatus, command.OperatorAdminID,
			adminBanStatusReasonPrefix+banID, command.RequestID, command.NowMs,
		); err != nil {
			return userservice.AdminUserBanPersistenceResult{}, err
		}
		var revokeErr error
		sessionIDs, revokeErr = revokeActiveUserSessions(ctx, tx, appCode, command.TargetUserID, command.OperatorAdminID, command.RequestID, command.NowMs)
		if revokeErr != nil {
			return userservice.AdminUserBanPersistenceResult{}, revokeErr
		}
	}

	if err := tx.Commit(); err != nil {
		return userservice.AdminUserBanPersistenceResult{}, err
	}
	ban, err := r.getAdminUserBan(ctx, banID)
	if err != nil {
		return userservice.AdminUserBanPersistenceResult{}, err
	}
	user, err := r.GetUser(ctx, command.TargetUserID)
	if err != nil {
		return userservice.AdminUserBanPersistenceResult{}, err
	}
	return userservice.AdminUserBanPersistenceResult{
		Ban:           ban,
		Status:        userservice.UserStatusPersistenceResult{User: user, RevokedSessionIDs: sessionIDs},
		StatusChanged: statusChanged,
	}, nil
}

func (r *Repository) ReleaseAdminUserBan(ctx context.Context, command userservice.ReleaseAdminUserBanCommand) (userservice.AdminUserBanRelease, error) {
	return r.releaseAdminUserBan(ctx, command)
}

func (r *Repository) ExpireAdminUserBans(ctx context.Context, nowMS int64, limit int32) ([]userservice.AdminUserBanRelease, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT ban_id, target_user_id
		FROM admin_user_bans
		WHERE app_code = ? AND status = 'active' AND expires_at_ms > 0 AND expires_at_ms <= ?
		ORDER BY expires_at_ms ASC, ban_id ASC
		LIMIT ?`, appcode.FromContext(ctx), nowMS, limit)
	if err != nil {
		return nil, err
	}
	type pendingBan struct {
		banID        string
		targetUserID int64
	}
	pending := make([]pendingBan, 0)
	for rows.Next() {
		var item pendingBan
		if err := rows.Scan(&item.banID, &item.targetUserID); err != nil {
			_ = rows.Close()
			return nil, err
		}
		pending = append(pending, item)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}

	releases := make([]userservice.AdminUserBanRelease, 0, len(pending))
	for _, item := range pending {
		release, err := r.releaseAdminUserBan(ctx, userservice.ReleaseAdminUserBanCommand{
			BanID:        item.banID,
			TargetUserID: item.targetUserID,
			Status:       userservice.AdminUserBanStatusExpired,
			Reason:       "admin_user_ban_expired",
			RequestID:    "admin_user_ban_expire:" + item.banID,
			NowMs:        nowMS,
		})
		if err != nil {
			return releases, err
		}
		releases = append(releases, release)
	}
	return releases, nil
}

func (r *Repository) releaseAdminUserBan(ctx context.Context, command userservice.ReleaseAdminUserBanCommand) (userservice.AdminUserBanRelease, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return userservice.AdminUserBanRelease{}, err
	}
	defer func() { _ = tx.Rollback() }()

	// 先锁定命令幂等事实，再选择具体 ban。空 ban_id 的重试会复用第一次解析出的 resolved_ban_id，
	// 不会因为上一条已 released 而误释放下一条重叠管理员封禁。
	existingCommand, resolvedBanID, err := claimAdminBanReleaseCommand(ctx, tx, command)
	if err != nil {
		return userservice.AdminUserBanRelease{}, err
	}
	var ban userservice.AdminUserBan
	if existingCommand {
		ban, err = r.getAdminUserBanForUpdate(ctx, tx, resolvedBanID, command.TargetUserID)
	} else if strings.TrimSpace(command.BanID) == "" {
		// 后台摘要可能正在展示更晚到期的经理封禁；按 target 选最新管理员事实，使重叠场景仍能逐条手动释放。
		ban, err = r.getLatestActiveAdminUserBanForUpdate(ctx, tx, command.TargetUserID)
	} else {
		ban, err = r.getAdminUserBanForUpdate(ctx, tx, command.BanID, command.TargetUserID)
	}
	if err != nil {
		return userservice.AdminUserBanRelease{}, err
	}
	if !existingCommand {
		if _, err := tx.ExecContext(ctx, `
			UPDATE admin_user_ban_release_commands
			SET resolved_ban_id = ?, updated_at_ms = ?
			WHERE app_code = ? AND command_id = ?`,
			ban.BanID, command.NowMs, appcode.FromContext(ctx), command.RequestID,
		); err != nil {
			return userservice.AdminUserBanRelease{}, err
		}
	}
	if ban.Status != userservice.AdminUserBanStatusActive {
		if err := tx.Commit(); err != nil {
			return userservice.AdminUserBanRelease{}, err
		}
		user, err := r.GetUser(ctx, ban.TargetUserID)
		return userservice.AdminUserBanRelease{Ban: ban, User: user}, err
	}

	currentStatus, err := lockUserStatus(ctx, tx, appcode.FromContext(ctx), ban.TargetUserID)
	if err != nil {
		return userservice.AdminUserBanRelease{}, err
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE admin_user_bans
		SET status = ?, released_by_admin_id = ?, released_reason = ?, released_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND ban_id = ?`,
		command.Status, command.OperatorAdminID, command.Reason, command.NowMs, command.NowMs,
		appcode.FromContext(ctx), ban.BanID,
	); err != nil {
		return userservice.AdminUserBanRelease{}, err
	}
	ban.Status = command.Status
	ban.ReleasedByAdminID = command.OperatorAdminID
	ban.ReleasedReason = command.Reason
	ban.ReleasedAtMs = command.NowMs
	ban.UpdatedAtMs = command.NowMs

	restored, err := restoreUserAfterManagedBanRelease(ctx, tx, managedBanRestoreCommand{
		TargetUserID:  ban.TargetUserID,
		CurrentStatus: currentStatus,
		OperatorType:  releaseOperatorType(command.Status),
		OperatorID:    command.OperatorAdminID,
		Reason:        "admin_user_unban:" + ban.BanID,
		RequestID:     command.RequestID,
		NowMs:         command.NowMs,
	})
	if err != nil {
		return userservice.AdminUserBanRelease{}, err
	}
	if err := tx.Commit(); err != nil {
		return userservice.AdminUserBanRelease{}, err
	}
	user, err := r.GetUser(ctx, ban.TargetUserID)
	if err != nil {
		return userservice.AdminUserBanRelease{}, err
	}
	return userservice.AdminUserBanRelease{Ban: ban, User: user, Restored: restored}, nil
}

// claimAdminBanReleaseCommand 把一次手动/自动释放固定为不可变命令载荷。
// INSERT IGNORE 会在同一 command_id 的并发请求间形成行级串行化，第二个请求提交后只读取首个 resolved_ban_id。
func claimAdminBanReleaseCommand(ctx context.Context, tx *sql.Tx, command userservice.ReleaseAdminUserBanCommand) (bool, string, error) {
	commandID := strings.TrimSpace(command.RequestID)
	if commandID == "" {
		return false, "", xerr.New(xerr.InvalidArgument, "admin unban request_id is required")
	}
	requestedBanID := strings.TrimSpace(command.BanID)
	result, err := tx.ExecContext(ctx, `
		INSERT IGNORE INTO admin_user_ban_release_commands (
			app_code, command_id, target_user_id, requested_ban_id, resolved_ban_id,
			release_status, operator_admin_id, reason, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, '', ?, ?, ?, ?, ?)`,
		appcode.FromContext(ctx), commandID, command.TargetUserID, requestedBanID,
		command.Status, command.OperatorAdminID, command.Reason, command.NowMs, command.NowMs,
	)
	if err != nil {
		return false, "", err
	}
	inserted, err := result.RowsAffected()
	if err != nil {
		return false, "", err
	}
	if inserted == 1 {
		return false, "", nil
	}

	var targetUserID int64
	var storedRequestedBanID, resolvedBanID, releaseStatus, reason string
	var operatorAdminID int64
	if err := tx.QueryRowContext(ctx, `
		SELECT target_user_id, requested_ban_id, resolved_ban_id, release_status, operator_admin_id, reason
		FROM admin_user_ban_release_commands
		WHERE app_code = ? AND command_id = ?
		FOR UPDATE`, appcode.FromContext(ctx), commandID).Scan(
		&targetUserID, &storedRequestedBanID, &resolvedBanID, &releaseStatus, &operatorAdminID, &reason,
	); err != nil {
		return false, "", err
	}
	if targetUserID != command.TargetUserID || storedRequestedBanID != requestedBanID ||
		releaseStatus != command.Status || operatorAdminID != command.OperatorAdminID || reason != command.Reason {
		return false, "", xerr.New(xerr.Conflict, "admin unban command payload does not match existing command")
	}
	if strings.TrimSpace(resolvedBanID) == "" {
		return false, "", xerr.New(xerr.Conflict, "admin unban command has no resolved ban")
	}
	return true, resolvedBanID, nil
}

type managedBanRestoreCommand struct {
	TargetUserID  int64
	CurrentStatus userdomain.Status
	OperatorType  string
	OperatorID    int64
	Reason        string
	RequestID     string
	NowMs         int64
}

// restoreUserAfterManagedBanRelease 在持有 users 行锁时检查全部可管理封禁来源并恢复账号。
// 最新 banned 日志必须来自 admin/manager 事实；历史无日志或其他平台封禁即使没有 active 事实也不会被自动恢复。
func restoreUserAfterManagedBanRelease(ctx context.Context, tx *sql.Tx, command managedBanRestoreCommand) (bool, error) {
	if command.CurrentStatus != userdomain.StatusBanned {
		return false, nil
	}
	activeCount, err := countActiveManagedBans(ctx, tx, command.TargetUserID)
	if err != nil || activeCount > 0 {
		return false, err
	}
	managed, err := latestBanBelongsToManagedFact(ctx, tx, command.TargetUserID)
	if err != nil || !managed {
		return false, err
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE users SET status = 'active', updated_at_ms = ?
		WHERE app_code = ? AND user_id = ? AND status = 'banned'`,
		command.NowMs, appcode.FromContext(ctx), command.TargetUserID,
	); err != nil {
		return false, err
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO user_status_change_logs (
			app_code, target_user_id, old_status, new_status, operator_type,
			operator_user_id, reason, request_id, created_at_ms
		) VALUES (?, ?, 'banned', 'active', ?, ?, ?, ?, ?)`,
		appcode.FromContext(ctx), command.TargetUserID, command.OperatorType,
		command.OperatorID, command.Reason, command.RequestID, command.NowMs,
	); err != nil {
		return false, err
	}
	return true, nil
}

func lockUserStatus(ctx context.Context, tx *sql.Tx, appCode string, userID int64) (userdomain.Status, error) {
	var status string
	if err := tx.QueryRowContext(ctx, `
		SELECT status FROM users WHERE app_code = ? AND user_id = ? FOR UPDATE`, appCode, userID).Scan(&status); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return "", xerr.New(xerr.NotFound, "user not found")
		}
		return "", err
	}
	return userdomain.Status(status), nil
}

func countActiveManagedBans(ctx context.Context, tx *sql.Tx, targetUserID int64) (int64, error) {
	var count int64
	err := tx.QueryRowContext(ctx, `
		SELECT
		  (SELECT COUNT(*) FROM manager_user_blocks WHERE app_code = ? AND target_user_id = ? AND status = 'active') +
		  (SELECT COUNT(*) FROM admin_user_bans WHERE app_code = ? AND target_user_id = ? AND status = 'active')`,
		appcode.FromContext(ctx), targetUserID, appcode.FromContext(ctx), targetUserID,
	).Scan(&count)
	return count, err
}

func latestBanBelongsToManagedFact(ctx context.Context, tx *sql.Tx, targetUserID int64) (bool, error) {
	var reason string
	err := tx.QueryRowContext(ctx, `
		SELECT reason
		FROM user_status_change_logs
		WHERE app_code = ? AND target_user_id = ? AND new_status = 'banned'
		ORDER BY created_at_ms DESC, id DESC
		LIMIT 1`, appcode.FromContext(ctx), targetUserID).Scan(&reason)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return strings.HasPrefix(reason, "manager_center_block:") || strings.HasPrefix(reason, adminBanStatusReasonPrefix), nil
}

func releaseOperatorType(status string) string {
	if status == userservice.AdminUserBanStatusExpired || status == userservice.ManagerUserBlockStatusExpired {
		return userservice.OperatorTypeSystem
	}
	return userservice.OperatorTypeAdmin
}

func revokeActiveUserSessions(ctx context.Context, tx *sql.Tx, appCode string, targetUserID int64, operatorUserID int64, requestID string, nowMs int64) ([]string, error) {
	rows, err := tx.QueryContext(ctx, `
		SELECT session_id
		FROM auth_sessions
		WHERE app_code = ? AND user_id = ? AND expires_at_ms > ? AND revoked_at_ms IS NULL
		FOR UPDATE`, appCode, targetUserID, nowMs)
	if err != nil {
		return nil, err
	}
	sessionIDs := make([]string, 0)
	for rows.Next() {
		var sessionID string
		if err := rows.Scan(&sessionID); err != nil {
			_ = rows.Close()
			return nil, err
		}
		sessionIDs = append(sessionIDs, sessionID)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	_, err = tx.ExecContext(ctx, `
		UPDATE auth_sessions
		SET revoked_at_ms = ?, revoked_reason = 'USER_BANNED', revoked_request_id = ?, revoked_by = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ? AND revoked_at_ms IS NULL`,
		nowMs, requestID, revokedBy(operatorUserID), nowMs, appCode, targetUserID)
	return sessionIDs, err
}

func (r *Repository) getAdminUserBan(ctx context.Context, banID string) (userservice.AdminUserBan, error) {
	return scanAdminUserBan(r.db.QueryRowContext(ctx, adminBanSelectSQL()+` WHERE app_code = ? AND ban_id = ?`, appcode.FromContext(ctx), banID))
}

func (r *Repository) getAdminUserBanByCommand(ctx context.Context, commandID string) (userservice.AdminUserBan, error) {
	return scanAdminUserBan(r.db.QueryRowContext(ctx, adminBanSelectSQL()+` WHERE app_code = ? AND command_id = ?`, appcode.FromContext(ctx), commandID))
}

func (r *Repository) getAdminUserBanForUpdate(ctx context.Context, tx *sql.Tx, banID string, targetUserID int64) (userservice.AdminUserBan, error) {
	return scanAdminUserBan(tx.QueryRowContext(ctx, adminBanSelectSQL()+`
		WHERE app_code = ? AND ban_id = ? AND target_user_id = ? FOR UPDATE`,
		appcode.FromContext(ctx), banID, targetUserID))
}

func (r *Repository) getLatestActiveAdminUserBanForUpdate(ctx context.Context, tx *sql.Tx, targetUserID int64) (userservice.AdminUserBan, error) {
	return scanAdminUserBan(tx.QueryRowContext(ctx, adminBanSelectSQL()+`
		WHERE app_code = ? AND target_user_id = ? AND status = 'active'
		ORDER BY created_at_ms DESC, ban_id DESC
		LIMIT 1 FOR UPDATE`, appcode.FromContext(ctx), targetUserID))
}

func adminBanSelectSQL() string {
	return `SELECT ban_id, command_id, target_user_id, target_old_status, expires_at_ms,
	              status, reason, operator_admin_id, created_at_ms, updated_at_ms,
	              released_by_admin_id, released_reason, released_at_ms
	       FROM admin_user_bans`
}

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

func scanAdminUserBan(scanner adminBanScanner) (userservice.AdminUserBan, error) {
	var ban userservice.AdminUserBan
	var oldStatus string
	if err := scanner.Scan(
		&ban.BanID, &ban.CommandID, &ban.TargetUserID, &oldStatus, &ban.ExpiresAtMs,
		&ban.Status, &ban.Reason, &ban.OperatorAdminID, &ban.CreatedAtMs, &ban.UpdatedAtMs,
		&ban.ReleasedByAdminID, &ban.ReleasedReason, &ban.ReleasedAtMs,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return userservice.AdminUserBan{}, xerr.New(xerr.NotFound, "admin user ban not found")
		}
		return userservice.AdminUserBan{}, err
	}
	ban.TargetOldStatus = userdomain.Status(oldStatus)
	return ban, nil
}

func adminBanDuplicate(err error) bool {
	if err == nil {
		return false
	}
	return strings.Contains(err.Error(), "Duplicate entry") || strings.Contains(err.Error(), "Error 1062")
}
