import { Inject, Injectable, UnauthorizedException, Logger, BadRequestException, ConflictException, ForbiddenException, ServiceUnavailableException, forwardRef } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { AuditService } from '@core/observability/audit/audit.service';
import { AuditAction, AuditStatus, ComplianceLevel, RiskLevel, Prisma } from '@prisma/client';
import { LdapService } from '../ldap/ldap.service';
import { EntraService } from '../entra/entra.service';
import * as bcrypt from 'bcrypt';
import { LoginDto, RegisterDto, ChangePasswordDto, DevEmailLoginDto } from './dto/auth.dto';
import { TokenService } from './services/token.service';
import { AuthCacheService, CachedUserAuth, OrgAuthSlice } from './services/auth-cache.service';
import { PermissionDelegationService } from '@common/services/permission-delegation.service';
import { SsoConfigService } from './sso/sso-config.service';
import { SsoError } from './sso/sso-errors';
import { SkipAssertAccess } from '@common/decorators/skip-assert-access.decorator';

@Injectable()
export class AuthService {
  private readonly logger = new Logger(AuthService.name);

  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService,
    private ldapService: LdapService,
    private entraService: EntraService,
    private configService: ConfigService,
    private tokenService: TokenService,
    private authCache: AuthCacheService,
    @Inject(forwardRef(() => PermissionDelegationService))
    private delegationService: PermissionDelegationService,
    private ssoConfig: SsoConfigService,
    private auditService: AuditService,
  ) {}

  async register(registerDto: RegisterDto) {
    
    const { username, email, password, displayName } = registerDto;

    // 检查用户是否已存在
    const existingUser = await this.prisma.user.findFirst({
      where: {
        OR: [{ username }, { email }],
      },
    });

    if (existingUser) {
      // 根据文档 (docs/modules/organization/07-api.md line 737-738)
      // 应该返回 409 ConflictException 而不是 401
      if (existingUser.username === username) {
        throw new ConflictException('Username already exists');
      } else {
        throw new ConflictException('Email already exists');
      }
    }

    // 哈希密码
    const passwordHash = await bcrypt.hash(password, 10);

    // 创建用户
    // 根据文档 (docs/modules/organization/07-api.md line 725)
    // 返回对象应包含 source 字段
    const user = await this.prisma.user.create({
      data: {
        username,
        email,
        passwordHash,
        displayName,
        status: 'ACTIVE',
        source: 'LOCAL', // 默认为LOCAL用户
      },
      select: {
        id: true,
        username: true,
        email: true,
        displayName: true,
        status: true,
        source: true, // 添加source字段到返回
        createdAt: true,
      },
    });

    // 生成 JWT
    const token = await this.generateToken(user.id, username, email);

    return {
      user,
      token,
    };
  }

  async login(loginDto: LoginDto) {
    const { username, password } = loginDto;
    
    this.logger.log(`Login attempt for username: ${username}`);

    // 1. 先查找用户并获取身份源信息
    const user = await this.prisma.user.findFirst({
      where: {
        OR: [{ username }, { email: username }],
        deletedAt: null,
      },
      include: {
        roles: {
          include: {
            role: {
              include: {
                permissions: { include: { permission: true } },
              },
            },
          },
        },
        departmentMemberships: {
          where: {
            leftAt: null,
          },
          include: {
            department: true,
            position: true,
            manager: {
              select: {
                id: true,
                displayName: true,
                email: true,
              },
            },
          },
        },
      },
    });

    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    // 2. 检查用户状态
    if (user.status !== 'ACTIVE') {
      if (user.status === 'TERMINATED') {
        throw new UnauthorizedException('Account has been terminated');
      }
      throw new UnauthorizedException('Account is inactive, please contact administrator');
    }

    // 3. 根据身份源选择认证方式（多源 fallback：见 docs/modules/organization/03-architecture.md）
    //    LOCAL  → bcrypt
    //    LDAP   → LDAP → (fail) Entra ROPC 兜底（覆盖远程员工连不上 AD 的场景）
    //    ENTRA  → Entra ROPC → (fail) LDAP 兜底（hash-synced 账户）→ (fail) 本地密码兜底
    let authenticated = false;
    const entraUpn = user.email || user.username;

    switch (user.source) {
      case 'LOCAL':
        if (!user.passwordHash) {
          throw new UnauthorizedException('Password not set for local user');
        }
        authenticated = await bcrypt.compare(password, user.passwordHash);
        if (authenticated) {
          this.logger.log(`Local authentication successful for: ${user.username}`);
        }
        break;

      case 'LDAP': {
        try {
          this.logger.log(`Attempting LDAP authentication for: ${user.username}`);
          const ldapUser = await this.ldapService.authenticate(user.username, password);
          authenticated = !!ldapUser;
          if (authenticated && ldapUser) {
            this.logger.log(`LDAP authentication successful for: ${user.email}`);
            await this.updateLdapUserInfo(user.id, ldapUser);
          }
        } catch (ldapError) {
          this.logger.warn(`LDAP authentication failed for ${user.username}: ${ldapError.message}`);
        }

        // LDAP 失败 → Entra ROPC 兜底（远程员工 / LDAP 不可达场景）
        if (!authenticated) {
          this.logger.log(`Falling back to Entra ROPC for LDAP user: ${user.username}`);
          authenticated = await this.entraService.authenticatePassword(entraUpn, password);
        }
        break;
      }

      case 'ENTRA': {
        // cloud-only 用户密码在云端，先试 Entra ROPC
        this.logger.log(`Attempting Entra ROPC for: ${user.username}`);
        authenticated = await this.entraService.authenticatePassword(entraUpn, password);

        // Entra ROPC 失败 → LDAP 兜底（hash-synced 账户在本地 AD 也能验）
        if (!authenticated) {
          try {
            this.logger.log(`Falling back to LDAP for Entra user: ${user.username}`);
            const ldapUser = await this.ldapService.authenticate(user.username, password);
            authenticated = !!ldapUser;
            if (authenticated && ldapUser) {
              await this.updateLdapUserInfo(user.id, ldapUser);
            }
          } catch (ldapError) {
            this.logger.warn(
              `LDAP fallback failed for Entra user ${user.username}: ${ldapError.message}`,
            );
          }
        }

        // 都失败 → 本地密码兜底（首次启动 / 灾备账号场景）
        if (!authenticated && user.passwordHash) {
          authenticated = await bcrypt.compare(password, user.passwordHash);
          if (authenticated) {
            this.logger.log(`Local password fallback for Entra user: ${user.username}`);
          }
        }
        break;
      }

      default:
        throw new UnauthorizedException('Unsupported user source');
    }

    // 4. 认证失败处理
    if (!authenticated) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const roles = user.roles.map((ur) => ur.role.code);
    const permissions = aggregatePermissionCodes(user.roles);

    const token = await this.generateToken(user.id, user.username, user.email, roles);

    // 获取用户可访问的区域和各区域权限
    const accessibleRegions = await this.getAccessibleRegions(user.id, roles);
    const regionPermissions = await this.getRegionPermissions(user.id);

    // 按照文档规范返回（docs/modules/organization/07-api.md line 548-569）
    const result = {
      accessToken: token.accessToken,
      refreshToken: token.refreshToken,
      tokenType: 'Bearer',
      expiresIn: this.tokenService.getAccessTtl(),
      user: {
        id: user.id,
        username: user.username,
        email: user.email,
        displayName: user.displayName,
        avatar: user.avatar,
        source: user.source,
        status: user.status,
        employeeId: user.employeeId || null,
        department: user.departmentMemberships?.find(dm => dm.isPrimary)?.department?.name,
        position: user.departmentMemberships?.find(dm => dm.isPrimary)?.position?.name,
        roles,
        permissions,
        defaultRegion: user.defaultRegion || 'CN',
        accessibleRegions,
        regionPermissions,
      },
    };

    this.logger.log(`Login successful for user: ${user.username} (source: ${user.source})`);
    return result;
  }

  /**
   * SSO 登录入口（v2.4 Entra ID OIDC）
   *
   * 事实源：docs/modules/organization/07-api.md §「5. 用户匹配 / JIT / 绑定回填」（步骤 5-7）
   *        docs/modules/organization/03-architecture.md §「JIT 建账号 / 绑定回填流程」
   *
   * 流程（事务内）：
   *  1) lower-case email 查 User WHERE deletedAt IS NULL
   *  2a) 命中 + externalId 为 null → CAS UPDATE 回填 + audit SSO_BINDING_FILLED
   *  2b) 命中 + externalSource=entra + externalId=oid → 直接登录（path: existing）
   *  2c) 命中 + externalSource=entra + externalId≠oid → 409 SSO_BINDING_CONFLICT + audit
   *  2d) 命中 + externalSource=ldap + externalId≠oid → 覆盖 + audit SSO_BINDING_UPGRADED_FROM_LDAP
   *  2e) 未命中 → 域名校验 + JIT (upsert + P2002 fallback) + audit SSO_JIT_CREATED
   *  3) status 检查（!= ACTIVE → 403 IAM_USER_SUSPENDED）
   *  4) audit SSO_LOGIN_SUCCESS
   *  5) 事务提交 → 调 tokenService.issuePair（与密码登录共用）
   *
   * 任何 step 失败 → 抛 SsoError；事务级 prisma 失败 → SSO_PROVIDER_UNAVAILABLE
   */
  async loginViaSSO(params: {
    email: string;
    oid: string;
    entraTid: string;
    ipAddress: string;
    userAgent: string;
  }): Promise<{ accessToken: string; refreshToken: string; userId: string }> {
    const email = (params.email || '').trim().toLowerCase();
    const { oid, entraTid, ipAddress, userAgent } = params;

    if (!email) {
      // 调用方 controller 应在更早阶段挡掉，这里兜底
      throw new SsoError('SSO_EMAIL_MISSING', 'email 为空');
    }

    // 事务内完成 1-4
    let resolvedUserId: string;
    let resolvedPath: 'existing' | 'jit' | 'binding_filled' | 'ldap_upgraded';

    // 成功路径 audit 事件累积到这里，tx 提交后再写。
    // 原因：AuditService 自建独立 prisma 连接，无法看到 tx 内新建/未提交的 user，
    //   JIT 路径下会触发 P2003 FK 违例丢 audit。post-commit 写保证 user 已落库 FK 校验通过。
    // 失败路径事件（SSO_BINDING_CONFLICT / LOGIN_FAILED）仍在 tx 内即时写，
    //   因为它们要求"即使业务回滚 audit 也要落库"（合规审计要求）且不涉及未提交 user FK。
    // 详见 .learnings/ERRORS/ERR-20260519-010-sso-audit-fk-cross-tx.md
    type PendingAuditEvent = {
      action: AuditAction;
      userId: string;
      metadata: Record<string, any>;
    };
    const pendingAuditEvents: PendingAuditEvent[] = [];

    try {
      const txResult = await this.prisma.$transaction(async (tx) => {
        // 1) 用 lower-case email 查
        let user = await tx.user.findFirst({
          where: { email, deletedAt: null },
        });

        let path: 'existing' | 'jit' | 'binding_filled' | 'ldap_upgraded' = 'existing';

        if (user) {
          const externalId = user.externalId;
          // 归一化为小写：EntraSync 历史写入用 'ENTRA' / 'LDAP'（大写，沿用 enum 名），
          // v2.4 SSO 设计约定是小写 'entra' / 'ldap'。容忍历史大写避免 unknown-externalSource
          // 兜底分支误判 BINDING_CONFLICT。详见 .learnings/ERRORS/ERR-20260520-002
          const externalSource = (user.externalSource || '').toLowerCase();

          if (externalId == null) {
            // 2a) CAS UPDATE 回填
            const updated = await tx.user.updateMany({
              where: { id: user.id, externalId: null },
              data: { externalId: oid, externalSource: 'entra' },
            });
            if (updated.count === 0) {
              // 并发场景：另一个请求抢先回填，重查走 entra 路径
              const refetched = await tx.user.findFirst({
                where: { email, deletedAt: null },
              });
              if (!refetched) {
                throw new SsoError('SSO_PROVIDER_UNAVAILABLE', '并发后 user 消失（异常）');
              }
              user = refetched;
              const reExternalId = user.externalId;
              const reExternalSource = (user.externalSource || '').toLowerCase();
              if (reExternalSource === 'entra' && reExternalId !== oid) {
                await this.writeSsoAudit(tx, {
                  action: AuditAction.SSO_BINDING_CONFLICT,
                  status: AuditStatus.FAILED,
                  userId: user.id,
                  email,
                  ipAddress,
                  userAgent,
                  metadata: {
                    email,
                    existingExternalId: reExternalId,
                    attemptedExternalId: oid,
                    entraTid,
                  },
                });
                throw new SsoError('SSO_BINDING_CONFLICT', 'email 已绑定其它 Entra oid', {
                  email,
                });
              }
              path = 'existing';
            } else {
              path = 'binding_filled';
              // post-commit 写：统一成功路径 audit 时机
              pendingAuditEvents.push({
                action: AuditAction.SSO_BINDING_FILLED,
                userId: user.id,
                metadata: {
                  userId: user.id,
                  email,
                  externalId: oid,
                  previousExternalId: null,
                  entraTid,
                },
              });
              user = { ...user, externalId: oid, externalSource: 'entra' };
            }
          } else if (externalSource === 'entra') {
            if (externalId === oid) {
              path = 'existing';
            } else {
              // 2c) 真冲突
              await this.writeSsoAudit(tx, {
                action: AuditAction.SSO_BINDING_CONFLICT,
                status: AuditStatus.FAILED,
                userId: user.id,
                email,
                ipAddress,
                userAgent,
                metadata: {
                  email,
                  existingExternalId: externalId,
                  attemptedExternalId: oid,
                  entraTid,
                },
              });
              throw new SsoError('SSO_BINDING_CONFLICT', 'email 已绑定其它 Entra oid', {
                email,
              });
            }
          } else if (externalSource === 'ldap') {
            if (externalId === oid) {
              path = 'existing';
            } else {
              // 2d) LDAP 升级
              await tx.user.update({
                where: { id: user.id },
                data: { externalId: oid, externalSource: 'entra' },
              });
              path = 'ldap_upgraded';
              // post-commit 写：统一成功路径 audit 时机
              pendingAuditEvents.push({
                action: AuditAction.SSO_BINDING_UPGRADED_FROM_LDAP,
                userId: user.id,
                metadata: {
                  userId: user.id,
                  email,
                  previousExternalId: externalId,
                  newExternalId: oid,
                  entraTid,
                },
              });
              user = { ...user, externalId: oid, externalSource: 'entra' };
            }
          } else {
            // externalSource 为其它值（按数据契约不应存在，但兜底视为冲突，避免静默覆盖）
            await this.writeSsoAudit(tx, {
              action: AuditAction.SSO_BINDING_CONFLICT,
              status: AuditStatus.FAILED,
              userId: user.id,
              email,
              ipAddress,
              userAgent,
              metadata: {
                email,
                existingExternalId: externalId,
                existingExternalSource: externalSource,
                attemptedExternalId: oid,
                entraTid,
              },
            });
            throw new SsoError('SSO_BINDING_CONFLICT', `unknown externalSource=${externalSource}`, {
              email,
            });
          }
        } else {
          // 2e) JIT 新建
          if (!this.ssoConfig.isDomainAllowed(email)) {
            await this.writeSsoAudit(tx, {
              action: AuditAction.LOGIN_FAILED,
              status: AuditStatus.FAILED,
              userId: undefined,
              email,
              ipAddress,
              userAgent,
              why: 'SSO_DOMAIN_NOT_ALLOWED',
              metadata: { email, externalId: oid, entraTid },
            });
            throw new SsoError('SSO_DOMAIN_NOT_ALLOWED', '域名不在白名单', { email });
          }

          // 运行时再查默认 org（防启动后被运营软删）
          const defaultOrgId = this.ssoConfig.jitDefaultOrgId;
          const defaultOrg = await tx.organization.findFirst({
            where: { id: defaultOrgId, deletedAt: null },
            include: { primaryRegion: true },
          });
          if (!defaultOrg) {
            throw new SsoError(
              'SSO_PROVIDER_UNAVAILABLE',
              `运行时 SSO_JIT_DEFAULT_ORG_ID=${defaultOrgId} 已不存在或被软删`,
            );
          }
          const region = (defaultOrg.primaryRegion?.code || 'CN').toUpperCase();

          // 用 upsert + P2002 fallback 处理并发
          let createdUser;
          try {
            createdUser = await tx.user.upsert({
              where: { email },
              create: {
                username: email,
                email,
                displayName: email,
                status: 'ACTIVE',
                source: 'ENTRA',
                passwordHash: null,
                externalId: oid,
                externalSource: 'entra',
                defaultRegion: region,
              },
              update: {},
            });
          } catch (err: any) {
            if (err?.code === 'P2002') {
              const refetched = await tx.user.findFirst({
                where: { email, deletedAt: null },
              });
              if (!refetched) throw err;
              createdUser = refetched;
            } else {
              throw err;
            }
          }

          // 仅在确实是本次创建（externalId 已写入且时间戳新）时建 UserRole + audit
          // 用 upsert 避免 TOCTOU（findFirst → create 之间被并发抢建会撞 unique）
          // 依赖 schema `@@unique([userId, roleId, organizationId])`
          const employeeRole = await tx.role.findFirst({
            where: { code: { in: ['Employee', 'EMPLOYEE'] } },
          });

          if (employeeRole) {
            await tx.userRole.upsert({
              where: {
                userId_roleId_organizationId: {
                  userId: createdUser.id,
                  roleId: employeeRole.id,
                  organizationId: defaultOrgId,
                },
              },
              create: {
                userId: createdUser.id,
                roleId: employeeRole.id,
                organizationId: defaultOrgId,
              },
              update: {},
            });
          } else {
            this.logger.warn(
              `JIT 建账号时未找到 Employee/EMPLOYEE 角色，user=${createdUser.id} 暂无角色`,
            );
          }

          user = createdUser;
          path = 'jit';

          // post-commit 写：必须！AuditService 自建连接看不到 tx 内新建 user，
          // tx 内写会触发 P2003 FK 违例丢 audit。详见 ERR-20260519-002。
          pendingAuditEvents.push({
            action: AuditAction.SSO_JIT_CREATED,
            userId: createdUser.id,
            metadata: {
              userId: createdUser.id,
              email,
              externalId: oid,
              defaultOrgId,
              entraTid,
            },
          });
        }

        // 3) 状态检查
        if (user.status !== 'ACTIVE') {
          throw new SsoError(
            // 协议层用 SSO 错误码，但 controller 把 IAM_USER_SUSPENDED 暴露给前端
            // 这里直接抛 SsoError 加一个特殊 meta 让 controller 决定
            'SSO_TOKEN_INVALID', // 占位，下面 controller 用 meta 改成 IAM_USER_SUSPENDED
            'user not active',
            { iamUserSuspended: true, userId: user.id },
          );
        }

        // 4) SSO_LOGIN_SUCCESS audit 累积到 post-commit 写
        // 必须 post-commit：JIT 路径下 user 在本 tx 内新建，AuditService 自建连接看不到，
        // 会触发 FK 违例丢 audit。即使非 JIT 路径，统一时机简化逻辑 + 失败容忍。
        pendingAuditEvents.push({
          action: AuditAction.SSO_LOGIN_SUCCESS,
          userId: user.id,
          metadata: {
            userId: user.id,
            email,
            externalId: oid,
            path,
            entraTid,
          },
        });

        return { userId: user.id, path };
      });

      resolvedUserId = txResult.userId;
      resolvedPath = txResult.path;
    } catch (err: any) {
      if (err instanceof SsoError) throw err;
      this.logger.error(`SSO 事务失败：${err?.message ?? err}`);
      throw new SsoError('SSO_PROVIDER_UNAVAILABLE', `DB 事务失败：${err?.message ?? 'unknown'}`);
    }

    // 5) 事务提交后写成功路径 audit（user 已 commit，FK 校验通过）
    // 每条独立 try/catch：audit 失败不重抛、不影响业务结果（业务 tx 已 commit）。
    // 崩溃窗口（tx commit → audit 写入前进程崩溃）audit 会丢——
    // 接受的代价，与"audit 失败不应回滚业务"原则一致（见 spec deviation 注释）。
    for (const evt of pendingAuditEvents) {
      try {
        await this.auditService.log(
          this.buildSsoAuditPayload({
            action: evt.action,
            userId: evt.userId,
            email,
            ip: ipAddress,
            ua: userAgent,
            metadata: evt.metadata,
          }),
        );
      } catch (err: any) {
        this.logger.warn(
          `SSO post-commit audit 写入失败 action=${evt.action} userId=${evt.userId}：${err?.message ?? err}`,
        );
        // 不重抛——业务已 commit，audit 失败不影响登录结果
      }
    }

    // 6) 签发 JWT（不在事务内 IO）
    const pair = await this.tokenService.issuePair(resolvedUserId);
    this.logger.log(
      `SSO 登录成功 userId=${resolvedUserId} email=${email} path=${resolvedPath}`,
    );
    return { ...pair, userId: resolvedUserId };
  }

  /**
   * 构造 SSO audit payload（成功 / 失败路径共用，避免 9+ 字段重复 boilerplate）。
   *
   * 设计说明：
   *  - `region` / `tenantId` / `where` / `how` / `module` / `entityType` 等为 SSO 通道固定值
   *  - `entityId` 在 userId 为空时退化到 zero-UUID（合规审计要求 entityId 非空）
   *  - `status` 默认 SUCCESS；失败路径显式传 FAILED + why
   */
  private buildSsoAuditPayload(opts: {
    action: AuditAction;
    status?: AuditStatus;
    userId?: string;
    email: string;
    ip: string;
    ua: string;
    why?: string;
    metadata: Record<string, any>;
  }) {
    return {
      region: 'cn',
      tenantId: 'default',
      who: opts.email || 'anonymous',
      what: opts.action.toString(),
      where: '/auth/sso/callback',
      why: opts.why,
      how: 'OIDC',
      module: 'auth',
      action: opts.action,
      entityType: 'User',
      entityId: opts.userId || '00000000-0000-0000-0000-000000000000',
      userId: opts.userId,
      ipAddress: opts.ip,
      userAgent: opts.ua,
      status: opts.status ?? AuditStatus.SUCCESS,
      riskLevel: RiskLevel.MEDIUM,
      complianceLevel: ComplianceLevel.HIGH,
      isSensitive: true,
      newValue: opts.metadata,
    };
  }

  /**
   * 在 tx 执行期间即时写一条 SSO **失败路径**的审计日志（SSO_BINDING_CONFLICT / LOGIN_FAILED 等）。
   *
   * 失败路径 audit 的特殊性：业务会抛 SsoError 触发 tx rollback，但 audit 必须**保留**
   * （合规审计要求 + 排查需要）。因为 AuditService 用独立 prisma 连接，
   * 即使业务 tx rollback，audit 也已经独立 commit。
   *
   * **成功路径 audit**（BINDING_FILLED / BINDING_UPGRADED_FROM_LDAP / JIT_CREATED / LOGIN_SUCCESS）
   * 不走这里，由 loginViaSSO 累积到 pendingAuditEvents 并在 tx 提交后统一写入，
   * 避免 JIT 新建 user 在 tx 内 audit 写入触发 P2003 FK 违例（详见 ERR-20260519-010）。
   *
   * tx 参数当前实参未用，保留供未来 tx-aware audit 改造（如 AuditService 接受 tx client）。
   */
  private async writeSsoAudit(
    _tx: Prisma.TransactionClient,
    p: {
      action: AuditAction;
      status?: AuditStatus;
      userId?: string;
      email: string;
      ipAddress: string;
      userAgent: string;
      why?: string;
      metadata: Record<string, any>;
    },
  ): Promise<void> {
    try {
      await this.auditService.log(
        this.buildSsoAuditPayload({
          action: p.action,
          status: p.status,
          userId: p.userId,
          email: p.email,
          ip: p.ipAddress,
          ua: p.userAgent,
          why: p.why,
          metadata: p.metadata,
        }),
      );
    } catch (err: any) {
      // 按 audit.service 自己的注释：审计失败不阻塞主流程
      this.logger.warn(`写 SSO audit 失败 action=${p.action}：${err?.message ?? err}`);
    }
  }

  async devEmailLogin(body: DevEmailLoginDto) {
    const deployEnv = String(this.configService.get<string>('DEPLOY_ENV') || '').toLowerCase();
    const nodeEnv = String(this.configService.get<string>('NODE_ENV') || '').toLowerCase();
    const enabledFlag = String(this.configService.get<string>('ENABLE_DEV_EMAIL_LOGIN') || '').toLowerCase();
    const enabled = deployEnv === 'development' && nodeEnv !== 'production' && enabledFlag !== 'false';
    if (!enabled) {
      throw new UnauthorizedException('Dev email login is disabled');
    }

    const email = body.email.trim().toLowerCase();
    const whitelist = String(this.configService.get<string>('DEV_EMAIL_LOGIN_WHITELIST') || '')
      .split(',')
      .map((item) => item.trim().toLowerCase())
      .filter(Boolean);

    if (whitelist.length > 0 && !whitelist.includes(email)) {
      throw new UnauthorizedException('Email not allowed for dev login');
    }

    const user = await this.prisma.user.findFirst({
      where: {
        email,
        status: 'ACTIVE',
        deletedAt: null,
      },
      include: {
        roles: {
          include: {
            role: {
              include: {
                permissions: { include: { permission: true } },
              },
            },
          },
        },
        departmentMemberships: {
          where: {
            leftAt: null,
          },
          include: {
            department: true,
            position: true,
          },
        },
      },
    });

    if (!user) {
      throw new UnauthorizedException('User not found or inactive');
    }

    const roles = user.roles.map((ur) => ur.role.code);
    const permissions = aggregatePermissionCodes(user.roles);
    const token = await this.generateToken(user.id, user.username, user.email, roles);
    const accessibleRegions = await this.getAccessibleRegions(user.id, roles);
    const regionPermissions = await this.getRegionPermissions(user.id);

    this.logger.warn(`Dev email login used by: ${user.username} (${email})`);

    return {
      accessToken: token.accessToken,
      refreshToken: token.refreshToken,
      tokenType: 'Bearer',
      expiresIn: this.tokenService.getAccessTtl(),
      user: {
        id: user.id,
        username: user.username,
        email: user.email,
        displayName: user.displayName,
        avatar: user.avatar,
        source: user.source,
        status: user.status,
        employeeId: user.employeeId || null,
        department: user.departmentMemberships?.find(dm => dm.isPrimary)?.department?.name,
        position: user.departmentMemberships?.find(dm => dm.isPrimary)?.position?.name,
        roles,
        permissions,
        defaultRegion: user.defaultRegion || 'CN',
        accessibleRegions,
        regionPermissions,
      },
    };
  }

  /**
   * 更新 LDAP 用户信息（认证成功后同步）
   */
  private async updateLdapUserInfo(userId: string, ldapUser: any) {
    try {
      await this.prisma.user.update({
        where: { id: userId },
        data: {
          displayName: ldapUser.displayName,
          ldapDn: ldapUser.distinguishedName,
          employeeId: ldapUser.employeeID,
          ldapSyncedAt: new Date(),
          metadata: {
            ldapGroups: ldapUser.memberOf || [],
            ldapTitle: ldapUser.title,
            ldapDepartment: ldapUser.department,
            ldapCompany: ldapUser.company,
            ldapLocation: ldapUser.physicalDeliveryOfficeName,
            proxyAddresses: ldapUser.proxyAddresses || [],
          },
        },
      });
      this.logger.log(`Updated LDAP user info for userId: ${userId}`);
    } catch (error) {
      this.logger.warn(`Failed to update LDAP user info: ${error.message}`);
    }
  }

  /**
   * 验证用户并返回用户信息（包含组织级权限 v2.1）
   * 
   * 权限逻辑：
   * 1. 全局角色（organizationId=null）的权限在所有组织生效
   * 2. 组织角色的权限只在指定组织生效
   * 3. 返回的 permissions 是当前请求组织的权限（由 Guard 根据 Header 确定）
   * 4. 返回的 organizationPermissions 包含所有组织的权限映射
   * 
   * 注意：region 字段已废弃，保留用于向后兼容
   */
  async validateUser(userId: string, currentOrganizationId?: string) {
    // 阶段 1：优先查 Redis 缓存；缓存命中直接切片返回；缓存未命中再走 DB 并回填
    const cached = await this.authCache.get(userId);
    if (cached) {
      // 委托入站 scope 已写入 cacheEntry.inboundDelegationScopes（§5.3.14），
      // 命中时直接读，避免每请求一次 permission_delegations 表查询。
      // 旧版本缓存条目可能没有该字段（升级回填窗口），fallback 到现场加载。
      const inboundScopes =
        cached.inboundDelegationScopes ??
        (await this.loadInboundDelegationScopes(userId));
      return this.sliceAuthPayload(cached, currentOrganizationId, inboundScopes);
    }

    const user = await this.prisma.user.findUnique({
      where: { id: userId, status: 'ACTIVE', deletedAt: null },
      include: {
        roles: {
          include: {
            role: {
              include: {
                permissions: {
                  include: {
                    permission: true,
                  },
                },
                // 规则 §5.3.5：DataScope 随角色一同加载，写入 Redis 缓存
                dataScopes: {
                  include: {
                    dataScope: true,
                  },
                },
              },
            },
          },
        },
      },
    });

    if (!user) {
      return null;
    }

    const defaultRegion = user.defaultRegion || 'CN';

    // 按组织分组收集权限（v2.1）
    const globalPermissions = new Set<string>();  // 全局权限（organizationId=null）
    const organizationPermissionsMap: Record<string, Set<string>> = {};  // 组织权限
    const organizationRolesMap: Record<string, string[]> = {};  // 组织角色映射

    // 向后兼容：同时支持 region 和 organizationId
    const regionPermissionsMap: Record<string, Set<string>> = {};  // 区域权限（已废弃）
    const rolesMap: Record<string, string[]> = {};  // 区域角色映射（已废弃）

    // Layer 4: DataScope 配置，按组织分层收集（与权限同 pass 完成，避免二次遍历 user.roles）
    const systemDataScopes: Array<{ resource: string; scopeType: string }> = [];
    const orgDataScopesMap: Record<string, Array<{ resource: string; scopeType: string }>> = {};

    for (const userRole of user.roles) {
      const organizationId = (userRole as any).organizationId;  // v2.1: organizationId
      const roleRegion = (userRole as any).region;  // 已废弃，保留向后兼容
      const roleCode = userRole.role.code;

      // Layer 4: DataScope 按组织分层收集
      const rdsList = (userRole.role as any).dataScopes || [];
      for (const rds of rdsList) {
        const entry = { resource: rds.resource, scopeType: rds.dataScope.scopeType };
        if (organizationId == null) {
          systemDataScopes.push(entry);
        } else {
          if (!orgDataScopesMap[organizationId]) orgDataScopesMap[organizationId] = [];
          orgDataScopesMap[organizationId].push(entry);
        }
      }

      // v2.1: 按组织收集权限
      if (organizationId == null) {  // 使用 == 匹配 null 和 undefined
        // 全局角色（在所有组织生效）
        // 不需要添加到 organizationRolesMap，因为会在所有组织查询时自动包含
      } else {
        // 组织角色
        if (!organizationRolesMap[organizationId]) {
          organizationRolesMap[organizationId] = [];
        }
        if (!organizationRolesMap[organizationId].includes(roleCode)) {
          organizationRolesMap[organizationId].push(roleCode);
        }
      }

      // 向后兼容：按区域收集角色
      if (roleRegion == null) {  // 使用 == 匹配 null 和 undefined
        ['CN', 'US', 'UAE'].forEach(r => {
          if (!rolesMap[r]) rolesMap[r] = [];
          if (!rolesMap[r].includes(roleCode)) rolesMap[r].push(roleCode);
        });
      } else {
        if (!rolesMap[roleRegion]) rolesMap[roleRegion] = [];
        if (!rolesMap[roleRegion].includes(roleCode)) rolesMap[roleRegion].push(roleCode);
      }

      // 收集权限
      for (const rolePermission of userRole.role.permissions) {
        const perm = rolePermission.permission;
        const permCode = `${perm.resource}:${perm.action}`;

        // v2.1: 按组织收集权限
        if (organizationId == null) {  // 使用 == 匹配 null 和 undefined
          // 全局角色的权限
          globalPermissions.add(permCode);
        } else {
          // 组织角色的权限
          if (!organizationPermissionsMap[organizationId]) {
            organizationPermissionsMap[organizationId] = new Set();
          }
          organizationPermissionsMap[organizationId].add(permCode);
        }

        // 向后兼容：按区域收集权限
        if (roleRegion == null) {  // 使用 == 匹配 null 和 undefined
          globalPermissions.add(permCode);
        } else {
          if (!regionPermissionsMap[roleRegion]) {
            regionPermissionsMap[roleRegion] = new Set();
          }
          regionPermissionsMap[roleRegion].add(permCode);
        }
      }
    }

    // v2.1: 合并当前组织的权限（全局 + 组织特定）
    let currentPermissions = new Set<string>(globalPermissions);
    if (currentOrganizationId && organizationPermissionsMap[currentOrganizationId]) {
      organizationPermissionsMap[currentOrganizationId].forEach(p => currentPermissions.add(p));
    }

    // 向后兼容：区域权限
    const region = defaultRegion;
    const currentRegionPermissions = new Set<string>(globalPermissions);
    if (regionPermissionsMap[region]) {
      regionPermissionsMap[region].forEach(p => currentRegionPermissions.add(p));
    }

    // 构建所有区域的权限映射（用于前端切换区域时使用 - 向后兼容）
    const allRegionPermissions: Record<string, string[]> = {};
    ['CN', 'US', 'UAE'].forEach(r => {
      const perms = new Set<string>(globalPermissions);
      if (regionPermissionsMap[r]) {
        regionPermissionsMap[r].forEach(p => perms.add(p));
      }
      allRegionPermissions[r] = Array.from(perms);
    });

    // 构建所有组织的权限映射（v2.1）
    const allOrganizationPermissions: Record<string, string[]> = {};
    for (const [orgId, perms] of Object.entries(organizationPermissionsMap)) {
      const merged = new Set<string>(globalPermissions);
      perms.forEach(p => merged.add(p));
      allOrganizationPermissions[orgId] = Array.from(merged);
    }

    // 规则 §5.3.3：按 resource 独立合并 + 仅当前组织（systemDataScopes / orgDataScopesMap 已在
    // 主循环中收集完毕）
    const dataScopes: Array<{ resource: string; scopeType: string }> = [];
    // 当前请求切片：systemRoles + 当前组织（若有）
    dataScopes.push(...systemDataScopes);
    if (currentOrganizationId && orgDataScopesMap[currentOrganizationId]) {
      dataScopes.push(...orgDataScopesMap[currentOrganizationId]);
    }

    // 构造缓存结构（按组织分层）— 供后续切片使用
    const cacheEntry: CachedUserAuth = {
      userId: user.id,
      username: user.username,
      email: user.email,
      defaultRegion,
      systemRoles: {
        permissions: Array.from(globalPermissions),
        dataScopes: systemDataScopes,
        roles: (rolesMap[region] || []).filter((r) =>
          Object.values(organizationRolesMap).every((orgRs) => !orgRs.includes(r)),
        ),
      },
      orgRoles: {},
      // 前端兼容字段：保留旧版 region 维度数据，避免前端 13 处依赖断裂
      regionPermissions: allRegionPermissions,
      regionRoles: rolesMap,
    };
    for (const [orgId, perms] of Object.entries(organizationPermissionsMap)) {
      const merged = new Set<string>(globalPermissions);
      perms.forEach((p) => merged.add(p));
      cacheEntry.orgRoles[orgId] = {
        permissions: Array.from(merged),
        dataScopes: orgDataScopesMap[orgId] || [],
        roles: organizationRolesMap[orgId] || [],
      };
    }

    // 委托入站 scope（§5.3.14）— 一并写入缓存以避免 cache-hit 路径每请求一次 DB
    const inboundDelegationScopes = await this.loadInboundDelegationScopes(
      user.id,
    );
    cacheEntry.inboundDelegationScopes = inboundDelegationScopes;
    await this.authCache.set(cacheEntry);
    const finalDataScopes = [...dataScopes, ...inboundDelegationScopes];

    return {
      userId: user.id,
      username: user.username,
      email: user.email,
      defaultRegion,
      // v2.1: 当前组织的角色和权限
      currentOrganizationId,
      roles: currentOrganizationId
        ? (organizationRolesMap[currentOrganizationId] || [])
        : rolesMap[region] || [],
      permissions: Array.from(currentOrganizationId ? currentPermissions : currentRegionPermissions),
      // v2.1: 所有组织的权限映射
      organizationPermissions: allOrganizationPermissions,
      organizationRoles: organizationRolesMap,
      // 向后兼容：区域权限映射
      regionPermissions: allRegionPermissions,
      regionRoles: rolesMap,
      // Layer 4: 数据权限配置（多角色已取最宽 + 委托入站）
      dataScopes: finalDataScopes,
    };
  }

  /**
   * 从缓存切片出当前请求需要的权限 payload。
   * 对齐规则 §5.3.3：按 currentOrg 切片，跨组织不合并。
   * 委托（§5.3.14）：把当前用户作为受托人加载到的"入站 scope"并入 dataScopes。
   */
  private sliceAuthPayload(
    cached: CachedUserAuth,
    currentOrganizationId?: string,
    inboundDelegationScopes: Array<{ resource: string; scopeType: string }> = [],
  ) {
    const orgSlice: OrgAuthSlice | undefined = currentOrganizationId
      ? cached.orgRoles[currentOrganizationId]
      : undefined;

    const permissions = new Set<string>(cached.systemRoles.permissions);
    const dataScopes = [...cached.systemRoles.dataScopes];
    const roles = new Set<string>(cached.systemRoles.roles);

    if (orgSlice) {
      orgSlice.permissions.forEach((p) => permissions.add(p));
      orgSlice.dataScopes.forEach((d) => dataScopes.push(d));
      orgSlice.roles.forEach((r) => roles.add(r));
    }

    // 委托入站 scope 作为附加来源并入（DataScopeService 后续按 resource 取最宽）
    inboundDelegationScopes.forEach((d) => dataScopes.push(d));

    // 所有组织的权限映射（供前端切换组织用）
    const allOrgPerms: Record<string, string[]> = {};
    const allOrgRoles: Record<string, string[]> = {};
    for (const [oid, slice] of Object.entries(cached.orgRoles)) {
      allOrgPerms[oid] = slice.permissions;
      allOrgRoles[oid] = slice.roles;
    }

    return {
      userId: cached.userId,
      username: cached.username,
      email: cached.email,
      defaultRegion: cached.defaultRegion,
      currentOrganizationId,
      roles: Array.from(roles),
      permissions: Array.from(permissions),
      organizationPermissions: allOrgPerms,
      organizationRoles: allOrgRoles,
      // 前端兼容字段：从 cache 透传旧版 region 维度数据
      regionPermissions: cached.regionPermissions || {},
      regionRoles: cached.regionRoles || {},
      dataScopes,
    };
  }

  /**
   * 加载当前用户作为受托人收到的活跃委托 scope（规则 §5.3.14）
   * 失败不阻塞主流程（容错记日志）
   */
  private async loadInboundDelegationScopes(
    userId: string,
  ): Promise<Array<{ resource: string; scopeType: string }>> {
    try {
      return await this.delegationService.loadActiveInboundScopes(userId);
    } catch (err: any) {
      this.logger.warn(
        `加载委托入站 scope 失败 userId=${userId}: ${err.message}`,
      );
      return [];
    }
  }

  /**
   * Refresh access token
   */
  /**
   * 使用 refresh token 换新 access + refresh（一次性使用 + 旋转）
   */
  async refresh(refreshToken: string) {
    const { accessToken, refreshToken: newRefresh, userId } =
      await this.tokenService.rotateRefresh(refreshToken);

    this.logger.log(`Token 已刷新 userId=${userId}`);

    return {
      accessToken,
      refreshToken: newRefresh,
      tokenType: 'Bearer',
      expiresIn: this.tokenService.getAccessTtl(),
    };
  }

  /**
   * 登出：把 JTI 加入黑名单 + 从 active_jtis 移除
   */
  async logout(accessToken: string) {
    try {
      const decoded: any = this.jwtService.decode(accessToken);
      if (decoded?.sub && decoded?.jti) {
        await this.tokenService.blacklist(decoded.jti, decoded.sub);
        this.logger.log(`用户已登出 userId=${decoded.sub} jti=${decoded.jti}`);
      }
      return { message: 'Logged out successfully' };
    } catch (error: any) {
      this.logger.warn(`Logout 失败（可能 token 已过期或格式错）：${error.message}`);
      return { message: 'Logged out successfully' };
    }
  }

  /**
   * 检查 JTI 是否在黑名单（供 JwtStrategy / 其他检查点使用）
   */
  async isJtiBlacklisted(jti: string): Promise<boolean> {
    return this.tokenService.isBlacklisted(jti);
  }

  /**
   * 主动失效用户所有 token（解雇场景）
   */
  async revokeAllTokens(userId: string): Promise<number> {
    await this.authCache.invalidate(userId);
    return this.tokenService.revokeAllForUser(userId);
  }

  private async generateToken(
    userId: string,
    _username?: string,
    _email?: string,
    _roles: string[] = [],
  ) {
    // JWT payload 瘦身：只携带 sub + jti；用户身份 / 权限 / roles 从 Redis 缓存读取
    return this.tokenService.issuePair(userId);
  }

  /**
   * 同步 LDAP 用户到本地数据库
   */
  private async syncLdapUser(ldapUser: any) {
    const email = ldapUser.mail.toLowerCase();
    const username = ldapUser.sAMAccountName;

    // Note: 部门同步已禁用 - 部门应由管理员手动管理
    // 用户的 departmentId 将保持不变（新用户为 null）

    // 同步职位信息（如果 LDAP 有提供）
    let positionId: string | null = null;
    if (ldapUser.title) {
      const position = await this.syncPosition(ldapUser.title);
      positionId = position?.id || null;
    }

    // 查找用户（包括软删除的用户）
    let user = await this.prisma.user.findFirst({
      where: {
        email,
        deletedAt: null, // 只查找未删除的用户
      },
      include: {
        roles: {
          include: {
            role: true,
          },
        },
        // Note: department, position are now in UserDepartment table
        departmentMemberships: {
          where: {
            leftAt: null,
          },
          include: {
            department: true,
            position: true,
          },
        },
      },
    });

    // 准备用户数据
    const userData = {
      username: username,
      email: email,
      displayName: ldapUser.displayName,
      ldapDn: ldapUser.distinguishedName,
      employeeId: ldapUser.employeeID,
      phone: null, // LDAP 中没有提供
      status: 'ACTIVE' as const,
      ldapSyncedAt: new Date(),
      // Note: departmentId 和 positionId 不再在 User 表中
      // 现在通过 UserDepartment 关联表管理，由管理员手动配置
      metadata: {
        ldapGroups: ldapUser.memberOf || [],
        ldapTitle: ldapUser.title,
        ldapDepartment: ldapUser.department,
        ldapCompany: ldapUser.company,
        ldapLocation: ldapUser.physicalDeliveryOfficeName,
        proxyAddresses: ldapUser.proxyAddresses || [],
      },
    };

    if (!user) {
      // 创建新用户（不自动分配角色，由管理员手动分配）
      this.logger.log(`Creating new user from LDAP: ${email} (no default roles)`);
      const createdUser = await this.prisma.user.create({
        data: {
          ...userData,
          passwordHash: null as any, // LDAP 用户不需要本地密码
        },
      });

      // ⚠️ 不再自动分配角色，由管理员手动分配
      // await this.assignDefaultRoles(createdUser.id, ldapUser.memberOf || []);
      
      // 重新查询以获取完整的用户信息（包含关联）
      user = await this.prisma.user.findUnique({
        where: { id: createdUser.id },
        include: {
          roles: {
            include: {
              role: true,
            },
          },
          departmentMemberships: {
            where: {
              leftAt: null,
            },
            include: {
              department: true,
              position: true,
            },
          },
        },
      });
    } else {
      // 更新现有用户
      this.logger.log(`Updating existing user from LDAP: ${email}`);
      user = await this.prisma.user.update({
        where: { id: user.id },
        data: userData as any,
        include: {
          roles: {
            include: {
              role: true,
            },
          },
          departmentMemberships: {
            where: {
              leftAt: null,
            },
            include: {
              department: true,
              position: true,
            },
          },
        },
      });

      // ⚠️ 不再自动同步角色，保持管理员分配的角色
      // await this.syncUserRoles(user.id, ldapUser.memberOf || []);
    }

    return user;
  }

  /**
   * 为新用户分配默认角色
   */
  private async assignDefaultRoles(userId: string, memberOf: string[]) {
    // 获取默认员工角色
    const employeeRole =
      (await this.prisma.role.findUnique({ where: { code: 'Employee' } })) ||
      (await this.prisma.role.findUnique({ where: { code: 'EMPLOYEE' } }));

    if (employeeRole) {
      await this.prisma.userRole.create({
        data: {
          userId,
          roleId: employeeRole.id,
          organizationId: null, // v2.1: 默认角色为全局角色
        },
      });
    }

    // 根据 LDAP 组映射角色
    await this.mapLdapGroupsToRoles(userId, memberOf);
  }

  /**
   * 同步用户角色（基于 LDAP 组）
   */
  private async syncUserRoles(userId: string, memberOf: string[]) {
    // 这里可以根据 LDAP 组的变化来更新用户角色
    // 简化版：只添加新角色，不删除旧角色
    await this.mapLdapGroupsToRoles(userId, memberOf);
  }

  /**
   * 将 LDAP 组映射到系统角色
   */
  private async mapLdapGroupsToRoles(userId: string, memberOf: string[]) {
    const groupNames = this.ldapService.extractGroupNames(memberOf);
    
    // 定义 LDAP 组到角色的映射关系
    const groupRoleMapping: Record<string, string> = {
      'Domain Admins': 'Administrator',
      'IT_CN': 'Administrator',
      'CN Org Admins': 'Administrator',
      // 可以根据实际需求扩展更多映射
    };

    for (const [groupPattern, roleCode] of Object.entries(groupRoleMapping)) {
      if (groupNames.some(g => g.includes(groupPattern))) {
        const role = await this.prisma.role.findUnique({
          where: { code: roleCode },
        });

        if (role) {
          // 检查用户是否已有此全局角色（organizationId=null）
          const existing = await this.prisma.userRole.findFirst({
            where: {
              userId,
              roleId: role.id,
              organizationId: null, // v2.1: 全局角色（在所有组织生效）
            },
          });

          if (!existing) {
            await this.prisma.userRole.create({
              data: {
                userId,
                roleId: role.id,
                organizationId: null, // v2.1: LDAP 同步的角色默认为全局角色
              },
            });
            this.logger.log(`Assigned global role ${roleCode} to user ${userId} based on LDAP group ${groupPattern}`);
          }
        }
      }
    }
  }

  /**
   * 同步职位信息（自动创建不存在的职位）
   */
  private async syncPosition(title: string) {
    if (!title || title.trim() === '') {
      return null;
    }

    try {
      // 生成职位代码（去除空格并转为大写）
      const code = title.trim().toUpperCase().replace(/\s+/g, '_');

      // 查找或创建职位
      const position = await this.prisma.position.upsert({
        where: { code },
        update: {
          name: title.trim(),
        },
        create: {
          code,
          name: title.trim(),
          description: `从 LDAP 自动同步: ${title}`,
        },
      });

      this.logger.log(`Synced position: ${title} (${position.id})`);
      return position;
    } catch (error) {
      this.logger.error(`Failed to sync position ${title}:`, error.message);
      return null;
    }
  }

  /**
   * 获取用户在各组织的权限映射（v2.1）
   * 
   * 返回格式：{ orgId1: ['user:read', ...], orgId2: ['user:read', ...] }
   * 
   * 注意：为向后兼容，同时返回区域权限映射
   */
  private async getRegionPermissions(userId: string): Promise<Record<string, string[]>> {
    const allRegions = ['CN', 'US', 'UAE'];

    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      include: {
        roles: {
          include: {
            role: {
              include: {
                permissions: {
                  include: {
                    permission: true,
                  },
                },
              },
            },
          },
        },
      },
    });

    if (!user) {
      return { CN: [], US: [], UAE: [] };
    }

    // 全局权限（region=null 的角色）
    const globalPermissions = new Set<string>();
    // 区域特定权限
    const regionPermissionsMap: Record<string, Set<string>> = {};

    for (const userRole of user.roles) {
      const roleRegion = (userRole as any).region;

      for (const rolePermission of userRole.role.permissions) {
        const perm = rolePermission.permission;
        const permCode = `${perm.resource}:${perm.action}`;

        if (roleRegion === null) {
          // 全局角色的权限
          globalPermissions.add(permCode);
        } else {
          // 区域角色的权限
          if (!regionPermissionsMap[roleRegion]) {
            regionPermissionsMap[roleRegion] = new Set();
          }
          regionPermissionsMap[roleRegion].add(permCode);
        }
      }
    }

    // 合并各区域的权限（全局 + 区域特定）
    const result: Record<string, string[]> = {};
    for (const region of allRegions) {
      const perms = new Set<string>(globalPermissions);
      if (regionPermissionsMap[region]) {
        regionPermissionsMap[region].forEach(p => perms.add(p));
      }
      result[region] = Array.from(perms);
    }

    return result;
  }

  /**
   * 获取用户可访问的区域列表（向后兼容）
   * 
   * 逻辑：
   * 1. 用户的默认区域总是可以访问
   * 2. 如果用户有全局角色（organizationId=null），可以访问所有区域
   * 3. 如果用户在某区域有角色，可以访问该区域
   * 4. 如果用户有 region:*:access 权限，可以访问所有区域
   * 
   * 注意：region 字段已废弃，该方法保留用于向后兼容
   */
  private async getAccessibleRegions(userId: string, roles: string[]): Promise<string[]> {
    const allRegions = ['CN', 'US', 'UAE'];

    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      include: {
        roles: {
          include: {
            role: {
              include: {
                permissions: {
                  include: {
                    permission: true,
                  },
                },
              },
            },
          },
        },
      },
    });

    if (!user) {
      return ['CN'];
    }

    const defaultRegion = user.defaultRegion || 'CN';
    const accessibleRegions = new Set<string>();
    
    // 默认区域总是可以访问
    accessibleRegions.add(defaultRegion);

    // 检查用户的角色
    let hasGlobalRole = false;
    const permissions = new Set<string>();

    for (const userRole of user.roles) {
      const roleRegion = (userRole as any).region;

      // 全局角色（region=null）可以访问所有区域
      if (roleRegion === null) {
        hasGlobalRole = true;
      } else {
        // 区域角色可以访问对应区域
        accessibleRegions.add(roleRegion);
      }

      // 收集权限
      for (const rolePermission of userRole.role.permissions) {
        const perm = rolePermission.permission;
        permissions.add(`${perm.resource}:${perm.action}`);
      }
    }

    // 全局角色或 region:*:access 权限可以访问所有区域
    if (hasGlobalRole || permissions.has('region:*:access')) {
      return allRegions;
    }

    // 检查 region:xxx:access 权限
    for (const region of allRegions) {
      const permission = `region:${region.toLowerCase()}:access`;
      if (permissions.has(permission)) {
        accessibleRegions.add(region);
      }
    }

    return Array.from(accessibleRegions);
  }

  /**
   * 修改用户密码（用户自己修改）
   * 只有 LOCAL 用户可以修改密码
   */
  @SkipAssertAccess('用户自助改自己密码，userId 取自 JWT（controller 通过 req.user.userId 注入），无 IDOR 风险')
  async changePassword(userId: string, changePasswordDto: ChangePasswordDto) {
    const { oldPassword, newPassword } = changePasswordDto;

    // 查找用户
    const user = await this.prisma.user.findUnique({
      where: { id: userId, deletedAt: null },
      select: {
        id: true,
        username: true,
        source: true,
        passwordHash: true,
      },
    });

    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    // 检查身份源：只有 LOCAL 用户可以修改密码
    if (user.source !== 'LOCAL') {
      const errorMessage = this.getPasswordChangeErrorMessage(user.source);
      // 根据文档(docs/modules/organization/07-api.md line 825)
      // 应该返回包含错误码的BadRequestException
      throw new BadRequestException({
        message: errorMessage,
        error: 'CANNOT_CHANGE_PASSWORD',
      });
    }

    // 验证密码哈希存在
    if (!user.passwordHash) {
      throw new BadRequestException('Password not set');
    }

    // 验证旧密码
    const isOldPasswordValid = await bcrypt.compare(oldPassword, user.passwordHash);
    if (!isOldPasswordValid) {
      throw new UnauthorizedException('Current password is incorrect');
    }

    // 验证新密码强度（至少8位，包含大小写+数字）
    const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
    if (!passwordRegex.test(newPassword)) {
      throw new BadRequestException('Password must be at least 8 characters long and contain uppercase, lowercase letters and numbers');
    }

    // 检查新旧密码不同
    const isSameAsOld = await bcrypt.compare(newPassword, user.passwordHash);
    if (isSameAsOld) {
      throw new BadRequestException('New password cannot be the same as old password');
    }

    // 哈希新密码
    const newPasswordHash = await bcrypt.hash(newPassword, 10);

    // 更新密码
    await this.prisma.user.update({
      where: { id: userId },
      data: { 
        passwordHash: newPasswordHash,
        updatedAt: new Date(),
      },
    });

    this.logger.log(`Password changed for user: ${user.username} (source: ${user.source})`);
  }

  /**
   * 获取不同身份源的密码修改错误消息
   */
  private getPasswordChangeErrorMessage(source: string): string {
    switch (source) {
      case 'LDAP':
        return 'LDAP users cannot change password through this system. Please contact IT administrator or change it through AD domain controller.';
      case 'ENTRA':
        return 'Entra ID users cannot change password through this system. Please change it through Microsoft 365 portal.';
      default:
        return 'Current user type does not support password change.';
    }
  }

  /**
   * 重置用户密码（管理员操作）
   */
  async resetPassword(userId: string, newPassword: string) {
    // 查找用户
    const user = await this.prisma.user.findUnique({
      where: { id: userId, deletedAt: null },
    });

    if (!user) {
      throw new BadRequestException('User not found');
    }

    // 验证新密码强度
    const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
    if (!passwordRegex.test(newPassword)) {
      throw new BadRequestException('Password must be at least 8 characters long and contain uppercase, lowercase letters and numbers');
    }

    // 哈希新密码
    const newPasswordHash = await bcrypt.hash(newPassword, 10);

    // 更新密码
    await this.prisma.user.update({
      where: { id: userId },
      data: { 
        passwordHash: newPasswordHash,
        // 如果是LDAP用户，这将覆盖null的passwordHash，使其成为本地用户
      },
    });

    this.logger.log(`Password reset for user: ${user.username}`);
  }
}

function aggregatePermissionCodes(userRoles: any[]): string[] {
  const codes = new Set<string>();
  for (const ur of userRoles ?? []) {
    if (ur?.role?.enabled === false) continue;
    for (const rp of ur?.role?.permissions ?? []) {
      const p = rp?.permission;
      if (p?.resource && p?.action) codes.add(`${p.resource}:${p.action}`);
    }
  }
  return Array.from(codes);
}
