import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { compare, hash } from 'bcrypt';
import jwt from 'jsonwebtoken';
import { Prisma } from '../generated/prisma/client.js';
import { CreateUserDto } from './dto/create-user.dto.js';
import { UpdateUserDto } from './dto/update-user.dto.js';
import { Role } from '../generated/prisma/enums.js';
import { PrismaService } from '../prisma.service.js';
import type { ActorContext } from '../common/actor-context.js';
import { AssignEngineerHospitalDto } from './dto/assign-engineer-hospital.dto.js';
import { LoginDto } from './dto/login.dto.js';
import { MESSAGES } from '../common/messages.js';
import { CreateSystemAdminDto } from '../auth/dto/create-system-admin.dto.js';
import { PasswordLoginConfirmDto } from '../auth/dto/password-login-confirm.dto.js';

const SAFE_USER_SELECT = {
  id: true,
  name: true,
  phone: true,
  openId: true,
  role: true,
  hospitalId: true,
  departmentId: true,
  groupId: true,
  hospital: {
    select:{
      name:true
    }
  }
} as const;

const DIRECTOR_VISIBLE_ROLES = [Role.LEADER, Role.DOCTOR] as const;
const LEADER_VISIBLE_ROLES = [Role.DOCTOR] as const;

type PasswordLoginTicketPayload = {
  purpose: 'PASSWORD_LOGIN_TICKET';
  userIds: number[];
};

@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  /**
   * 注册接口已关闭，避免绕过管理员创建链路。
   */
  async register() {
    throw new ForbiddenException(MESSAGES.AUTH.REGISTER_DISABLED);
  }

  /**
   * 创建系统管理员：要求引导密钥。
   */
  async createSystemAdmin(dto: CreateSystemAdminDto) {
    const name = this.normalizeRequiredString(dto.name, 'name');
    const phone = this.normalizePhone(dto.phone);
    const password = this.normalizePassword(dto.password);
    const openId = this.normalizeOptionalString(dto.openId);

    this.assertSystemAdminBootstrapKey(
      Role.SYSTEM_ADMIN,
      dto.systemAdminBootstrapKey,
    );
    await this.assertPhoneRoleScopeUnique(phone, Role.SYSTEM_ADMIN, null);

    const passwordHash = await hash(password, 12);

    return this.prisma.user.create({
      data: {
        name,
        phone,
        passwordHash,
        openId,
        role: Role.SYSTEM_ADMIN,
        hospitalId: null,
        departmentId: null,
        groupId: null,
      },
      select: SAFE_USER_SELECT,
    });
  }

  /**
   * 院内账号密码登录：支持后台和小程序复用。
   */
  async login(dto: LoginDto) {
    const role = dto.role != null ? this.normalizeRole(dto.role) : undefined;
    const phone = this.normalizePhone(dto.phone);
    const password = this.normalizePassword(dto.password);
    const hospitalId = this.normalizeOptionalInt(dto.hospitalId, 'hospitalId');

    const users = await this.prisma.user.findMany({
      where: {
        phone,
        ...(role != null ? { role } : {}),
        ...(hospitalId != null ? { hospitalId } : {}),
      },
      select: {
        ...SAFE_USER_SELECT,
        passwordHash: true,
        hospital: {
          select: {
            id: true,
            name: true,
          },
        },
      },
      orderBy: [{ hospitalId: 'asc' }, { id: 'asc' }],
      take: 10,
    });

    if (users.length === 0) {
      throw new UnauthorizedException(MESSAGES.AUTH.INVALID_CREDENTIALS);
    }

    const matchedUsers = (
      await Promise.all(
        users.map(async (user) => {
          if (!user.passwordHash) {
            return null;
          }

          const matched = await compare(password, user.passwordHash);
          return matched ? user : null;
        }),
      )
    ).filter((item) => item != null);

    if (matchedUsers.length === 0) {
      throw new UnauthorizedException(MESSAGES.AUTH.INVALID_CREDENTIALS);
    }
    if (matchedUsers.length === 1) {
      return this.buildUserLoginResponse(matchedUsers[0]);
    }

    return {
      needSelect: true,
      loginTicket: this.signPasswordLoginTicket({
        purpose: 'PASSWORD_LOGIN_TICKET',
        userIds: matchedUsers.map((user) => user.id),
      }),
      accounts: matchedUsers.map((user) => ({
        id: user.id,
        name: user.name,
        role: user.role,
        hospitalId: user.hospitalId,
        hospitalName: user.hospital?.name ?? null,
        departmentId: user.departmentId,
        groupId: user.groupId,
      })),
    };
  }

  /**
   * 院内账号密码多账号确认登录。
   */
  async confirmLogin(dto: PasswordLoginConfirmDto) {
    const payload = this.verifyPasswordLoginTicket(dto.loginTicket);
    if (!payload.userIds.includes(dto.userId)) {
      throw new BadRequestException(
        MESSAGES.AUTH.PASSWORD_ACCOUNT_SELECTION_INVALID,
      );
    }

    return this.loginByUserId(dto.userId);
  }

  /**
   * 按用户 ID 直接签发院内登录态，供小程序快捷登录复用。
   */
  async loginByUserId(userId: number) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: SAFE_USER_SELECT,
    });
    if (!user) {
      throw new NotFoundException(MESSAGES.USER.NOT_FOUND);
    }

    return this.buildUserLoginResponse(user);
  }

  /**
   * 小程序登录时绑定 openId。
   * 同一个微信账号允许绑定多个院内账号，但单个院内账号仅允许绑定一个微信账号。
   */
  async bindOpenIdForMiniAppLogin(userId: number, openId: string) {
    const normalizedOpenId = this.normalizeRequiredString(openId, 'openId');
    const current = await this.prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        openId: true,
      },
    });
    if (!current) {
      throw new NotFoundException(MESSAGES.USER.NOT_FOUND);
    }
    if (current.openId && current.openId !== normalizedOpenId) {
      throw new ConflictException(
        MESSAGES.AUTH.MINIAPP_OPEN_ID_BOUND_OTHER_USER,
      );
    }

    if (!current.openId) {
      await this.prisma.user.update({
        where: { id: current.id },
        data: { openId: normalizedOpenId },
      });
    }
  }

  /**
   * 获取当前登录用户详情。
   */
  async me(actor: ActorContext) {
    return this.findOne(actor, actor.id);
  }

  /**
   * B 端创建用户（通常由管理员使用）。
   */
  async create(actor: ActorContext, createUserDto: CreateUserDto) {
    const role = this.normalizeRole(createUserDto.role);
    const name = this.normalizeRequiredString(createUserDto.name, 'name');
    const phone = this.normalizePhone(createUserDto.phone);
    const password = createUserDto.password
      ? this.normalizePassword(createUserDto.password)
      : null;
    const openId = this.normalizeOptionalString(createUserDto.openId);
    const hospitalId = this.normalizeOptionalInt(
      createUserDto.hospitalId,
      'hospitalId',
    );
    const departmentId = this.normalizeOptionalInt(
      createUserDto.departmentId,
      'departmentId',
    );
    const groupId = this.normalizeOptionalInt(createUserDto.groupId, 'groupId');

    const scoped = this.resolveCreateScope(
      actor,
      role,
      hospitalId,
      departmentId,
      groupId,
    );

    await this.assertOrganizationScope(
      role,
      scoped.hospitalId,
      scoped.departmentId,
      scoped.groupId,
    );
    await this.assertPhoneRoleScopeUnique(phone, role, scoped.hospitalId);

    return this.prisma.user.create({
      data: {
        name,
        phone,
        passwordHash: password ? await hash(password, 12) : null,
        openId,
        role,
        hospitalId: scoped.hospitalId,
        departmentId: scoped.departmentId,
        groupId: scoped.groupId,
      },
      select: SAFE_USER_SELECT,
    });
  }

  /**
   * 查询用户列表：按角色自动收敛可见范围。
   */
  async findAll(actor: ActorContext) {
    const where: Prisma.UserWhereInput = {};

    if (actor.role === Role.HOSPITAL_ADMIN) {
      where.hospitalId = this.requireActorScopeInt(
        actor.hospitalId,
        MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
      );
    } else if (actor.role === Role.DIRECTOR) {
      where.departmentId = this.requireActorScopeInt(
        actor.departmentId,
        MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED,
      );
      where.role = { in: [...DIRECTOR_VISIBLE_ROLES] };
    } else if (actor.role === Role.LEADER) {
      where.groupId = this.requireActorScopeInt(
        actor.groupId,
        MESSAGES.ORG.ACTOR_GROUP_REQUIRED,
      );
      where.role = { in: [...LEADER_VISIBLE_ROLES] };
    } else if (actor.role !== Role.SYSTEM_ADMIN) {
      throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
    }

    return this.prisma.user.findMany({
      where,
      select: SAFE_USER_SELECT,
      orderBy: { id: 'desc' },
    });
  }

  /**
   * 查询用户详情。
   */
  async findOne(actor: ActorContext, id: number) {
    const userId = this.normalizeRequiredInt(id, 'id');

    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: SAFE_USER_SELECT,
    });
    if (!user) {
      throw new NotFoundException(MESSAGES.USER.NOT_FOUND);
    }

    this.assertUserReadable(actor, user);

    return user;
  }

  /**
   * 更新用户信息（含可选密码重置）。
   */
  async update(actor: ActorContext, id: number, updateUserDto: UpdateUserDto) {
    const userId = this.normalizeRequiredInt(id, 'id');
    const current = await this.prisma.user.findUnique({
      where: { id: userId },
      select: {
        ...SAFE_USER_SELECT,
        passwordHash: true,
      },
    });
    if (!current) {
      throw new NotFoundException(MESSAGES.USER.NOT_FOUND);
    }

    this.assertUserWritable(actor, current);

    const nextRole =
      updateUserDto.role != null
        ? this.normalizeRole(updateUserDto.role)
        : current.role;
    const nextHospitalId =
      updateUserDto.hospitalId !== undefined
        ? this.normalizeOptionalInt(updateUserDto.hospitalId, 'hospitalId')
        : current.hospitalId;
    const nextDepartmentId =
      updateUserDto.departmentId !== undefined
        ? this.normalizeOptionalInt(updateUserDto.departmentId, 'departmentId')
        : current.departmentId;
    const nextGroupId =
      updateUserDto.groupId !== undefined
        ? this.normalizeOptionalInt(updateUserDto.groupId, 'groupId')
        : current.groupId;

    this.assertUpdateTargetRoleAllowed(actor, nextRole);
    this.assertUpdateHospitalScopeAllowed(actor, nextHospitalId);
    this.assertUpdateDepartmentScopeAllowed(actor, nextDepartmentId);

    const assigningDepartmentOrGroup =
      (updateUserDto.departmentId !== undefined && nextDepartmentId != null) ||
      (updateUserDto.groupId !== undefined && nextGroupId != null);
    if (
      assigningDepartmentOrGroup &&
      nextRole !== Role.DOCTOR &&
      nextRole !== Role.DIRECTOR &&
      nextRole !== Role.LEADER
    ) {
      throw new BadRequestException(MESSAGES.USER.DOCTOR_ONLY_SCOPE_CHANGE);
    }

    await this.assertOrganizationScope(
      nextRole,
      nextHospitalId,
      nextDepartmentId,
      nextGroupId,
    );

    const nextOpenId =
      updateUserDto.openId !== undefined
        ? this.normalizeOptionalString(updateUserDto.openId)
        : current.openId;
    const nextPhone =
      updateUserDto.phone !== undefined
        ? this.normalizePhone(updateUserDto.phone)
        : current.phone;
    await this.assertPhoneRoleScopeUnique(
      nextPhone,
      nextRole,
      nextHospitalId,
      userId,
    );

    const data: Record<string, unknown> = {};
    if (updateUserDto.name !== undefined) {
      data.name = this.normalizeRequiredString(updateUserDto.name, 'name');
    }
    if (updateUserDto.phone !== undefined) {
      data.phone = nextPhone;
    }
    if (updateUserDto.role !== undefined) {
      data.role = nextRole;
    }
    if (updateUserDto.hospitalId !== undefined) {
      data.hospitalId = nextHospitalId;
    }
    if (updateUserDto.departmentId !== undefined) {
      data.departmentId = nextDepartmentId;
    }
    if (updateUserDto.groupId !== undefined) {
      data.groupId = nextGroupId;
    }
    if (updateUserDto.openId !== undefined) {
      data.openId = nextOpenId;
    }
    if (updateUserDto.password) {
      // 密码变更后立即吊销旧 token，避免旧会话继续使用。
      data.passwordHash = await hash(
        this.normalizePassword(updateUserDto.password),
        12,
      );
      data.tokenValidAfter = new Date();
    }

    return this.prisma.user.update({
      where: { id: userId },
      data,
      select: SAFE_USER_SELECT,
    });
  }

  /**
   * 删除用户。
   */
  async remove(actor: ActorContext, id: number) {
    const userId = this.normalizeRequiredInt(id, 'id');
    const target = await this.findOne(actor, userId);
    this.assertUserWritable(actor, target);

    try {
      return await this.prisma.user.delete({
        where: { id: userId },
        select: SAFE_USER_SELECT,
      });
    } catch (error) {
      if (
        error instanceof Prisma.PrismaClientKnownRequestError &&
        error.code === 'P2003'
      ) {
        throw new ConflictException(MESSAGES.USER.DELETE_CONFLICT);
      }
      throw error;
    }
  }

  /**
   * 系统管理员将工程师绑定到指定医院。
   */
  async assignEngineerHospital(
    actor: ActorContext,
    targetUserId: number,
    dto: AssignEngineerHospitalDto,
  ) {
    if (actor.role !== Role.SYSTEM_ADMIN) {
      throw new ForbiddenException(MESSAGES.USER.ENGINEER_BIND_FORBIDDEN);
    }
    if (!Number.isInteger(dto.hospitalId)) {
      throw new BadRequestException(MESSAGES.USER.HOSPITAL_ID_INVALID);
    }

    const hospital = await this.prisma.hospital.findUnique({
      where: { id: dto.hospitalId },
      select: { id: true },
    });
    if (!hospital) {
      throw new NotFoundException(MESSAGES.ORG.HOSPITAL_NOT_FOUND);
    }

    const user = await this.prisma.user.findUnique({
      where: { id: targetUserId },
      select: { id: true, role: true },
    });
    if (!user) {
      throw new NotFoundException(MESSAGES.USER.NOT_FOUND);
    }
    if (user.role !== Role.ENGINEER) {
      throw new BadRequestException(MESSAGES.USER.TARGET_NOT_ENGINEER);
    }

    return this.prisma.user.update({
      where: { id: targetUserId },
      data: {
        hospitalId: dto.hospitalId,
        departmentId: null,
        groupId: null,
      },
      select: SAFE_USER_SELECT,
    });
  }

  /**
   * 去除密码摘要，避免泄露敏感信息。
   */
  private toSafeUser(
    user: { passwordHash?: string | null } & Record<string, unknown>,
  ) {
    const { passwordHash, ...safe } = user;
    return safe;
  }

  /**
   * 统一构造院内账号登录响应。
   */
  private buildUserLoginResponse(user: {
    id: number;
    name: string;
    phone: string;
    openId: string | null;
    role: Role;
    hospitalId: number | null;
    departmentId: number | null;
    groupId: number | null;
    passwordHash?: string | null;
  }) {
    const actor: ActorContext = {
      id: user.id,
      role: user.role,
      hospitalId: user.hospitalId,
      departmentId: user.departmentId,
      groupId: user.groupId,
    };

    return {
      tokenType: 'Bearer',
      accessToken: this.signAccessToken(actor),
      actor,
      user: this.toSafeUser(user),
    };
  }

  /**
   * 校验系统管理员注册引导密钥。
   */
  private assertSystemAdminBootstrapKey(
    role: Role,
    providedBootstrapKey?: string,
  ) {
    if (role !== Role.SYSTEM_ADMIN) {
      return;
    }

    const expectedBootstrapKey = process.env.SYSTEM_ADMIN_BOOTSTRAP_KEY;
    if (!expectedBootstrapKey) {
      throw new ForbiddenException(MESSAGES.USER.SYSTEM_ADMIN_REG_DISABLED);
    }
    if (providedBootstrapKey !== expectedBootstrapKey) {
      throw new ForbiddenException(
        MESSAGES.USER.SYSTEM_ADMIN_BOOTSTRAP_KEY_INVALID,
      );
    }
  }

  /**
   * 校验“手机号 + 角色 + 医院”唯一性。
   */
  private async assertPhoneRoleScopeUnique(
    phone: string,
    role: Role,
    hospitalId: number | null,
    selfId?: number,
  ) {
    const exists = await this.prisma.user.findFirst({
      where: {
        phone,
        role,
        hospitalId,
      },
      select: { id: true },
    });
    if (exists && exists.id !== selfId) {
      throw new ConflictException(MESSAGES.USER.DUPLICATE_PHONE_ROLE_SCOPE);
    }
  }

  /**
   * 校验角色与组织归属关系是否合法。
   */
  private async assertOrganizationScope(
    role: Role,
    hospitalId: number | null,
    departmentId: number | null,
    groupId: number | null,
  ) {
    if (role === Role.SYSTEM_ADMIN) {
      if (hospitalId || departmentId || groupId) {
        throw new BadRequestException(MESSAGES.USER.SYSTEM_ADMIN_SCOPE_INVALID);
      }
      return;
    }

    if (!hospitalId) {
      throw new BadRequestException(MESSAGES.USER.HOSPITAL_REQUIRED);
    }

    const hospital = await this.prisma.hospital.findUnique({
      where: { id: hospitalId },
      select: { id: true },
    });
    if (!hospital) {
      throw new BadRequestException(MESSAGES.USER.HOSPITAL_NOT_FOUND);
    }

    const needsDepartment =
      role === Role.DIRECTOR || role === Role.LEADER || role === Role.DOCTOR;
    if (needsDepartment && !departmentId) {
      throw new BadRequestException(MESSAGES.USER.DEPARTMENT_REQUIRED);
    }

    const needsGroup = role === Role.LEADER || role === Role.DOCTOR;
    if (needsGroup && !groupId) {
      throw new BadRequestException(MESSAGES.USER.GROUP_REQUIRED);
    }

    if (role === Role.ENGINEER && (departmentId || groupId)) {
      throw new BadRequestException(MESSAGES.USER.ENGINEER_SCOPE_INVALID);
    }

    if (departmentId) {
      const department = await this.prisma.department.findUnique({
        where: { id: departmentId },
        select: { id: true, hospitalId: true },
      });
      if (!department || department.hospitalId !== hospitalId) {
        throw new BadRequestException(
          MESSAGES.USER.DEPARTMENT_HOSPITAL_MISMATCH,
        );
      }
    }

    if (groupId) {
      if (!departmentId) {
        throw new BadRequestException(MESSAGES.USER.GROUP_DEPARTMENT_REQUIRED);
      }
      const group = await this.prisma.group.findUnique({
        where: { id: groupId },
        select: { id: true, departmentId: true },
      });
      if (!group || group.departmentId !== departmentId) {
        throw new BadRequestException(MESSAGES.USER.GROUP_DEPARTMENT_MISMATCH);
      }
    }
  }

  /**
   * 必填整数标准化。
   */
  private normalizeRequiredInt(value: unknown, fieldName: string): number {
    const parsed = Number(value);
    if (!Number.isInteger(parsed)) {
      throw new BadRequestException(`${fieldName} 必须为整数`);
    }
    return parsed;
  }

  /**
   * 从 actor 上下文读取必填范围字段（医院/科室等）。
   */
  private requireActorScopeInt(value: unknown, message: string): number {
    if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
      throw new BadRequestException(message);
    }
    return value;
  }

  /**
   * 可空整数标准化。
   */
  private normalizeOptionalInt(
    value: unknown,
    fieldName: string,
  ): number | null {
    if (value === undefined || value === null || value === '') {
      return null;
    }
    return this.normalizeRequiredInt(value, fieldName);
  }

  /**
   * 必填字符串标准化。
   */
  private normalizeRequiredString(value: unknown, fieldName: string): string {
    if (typeof value !== 'string') {
      throw new BadRequestException(`${fieldName} 必须为字符串`);
    }
    const trimmed = value.trim();
    if (!trimmed) {
      throw new BadRequestException(`${fieldName} 不能为空`);
    }
    return trimmed;
  }

  /**
   * 可空字符串标准化。
   */
  private normalizeOptionalString(value: unknown): string | null {
    if (value === undefined || value === null) {
      return null;
    }
    if (typeof value !== 'string') {
      throw new BadRequestException(MESSAGES.USER.INVALID_OPEN_ID);
    }

    const trimmed = value.trim();
    return trimmed ? trimmed : null;
  }

  /**
   * 手机号标准化与格式校验。
   */
  private normalizePhone(phone: unknown): string {
    const normalized = this.normalizeRequiredString(phone, 'phone');
    if (!/^1\d{10}$/.test(normalized)) {
      throw new BadRequestException(MESSAGES.USER.INVALID_PHONE);
    }
    return normalized;
  }

  /**
   * 密码标准化与长度校验。
   */
  private normalizePassword(password: unknown): string {
    const normalized = this.normalizeRequiredString(password, 'password');
    if (normalized.length < 8) {
      throw new BadRequestException(MESSAGES.USER.INVALID_PASSWORD);
    }
    return normalized;
  }

  /**
   * 角色枚举校验。
   */
  private normalizeRole(role: unknown): Role {
    if (typeof role !== 'string') {
      throw new BadRequestException(MESSAGES.USER.INVALID_ROLE);
    }

    if (!Object.values(Role).includes(role as Role)) {
      throw new BadRequestException(MESSAGES.USER.INVALID_ROLE);
    }

    return role as Role;
  }

  /**
   * 解析创建用户作用域并执行角色链路约束。
   */
  private resolveCreateScope(
    actor: ActorContext,
    targetRole: Role,
    hospitalId: number | null,
    departmentId: number | null,
    groupId: number | null,
  ) {
    if (actor.role === Role.SYSTEM_ADMIN) {
      if (targetRole === Role.SYSTEM_ADMIN) {
        return { hospitalId: null, departmentId: null, groupId: null };
      }

      // 系统管理员可创建任意角色，具体归属由后续组织范围校验保证合法。
      return { hospitalId, departmentId, groupId };
    }

    if (actor.role !== Role.HOSPITAL_ADMIN) {
      throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN);
    }

    if (
      targetRole === Role.SYSTEM_ADMIN ||
      targetRole === Role.HOSPITAL_ADMIN
    ) {
      throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN);
    }

    const actorHospitalId = this.requireActorScopeInt(
      actor.hospitalId,
      MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
    );
    const scopedHospitalId = hospitalId ?? actorHospitalId;
    if (scopedHospitalId !== actorHospitalId) {
      throw new ForbiddenException(
        MESSAGES.USER.HOSPITAL_ADMIN_SCOPE_FORBIDDEN,
      );
    }

    return {
      hospitalId: scopedHospitalId,
      departmentId,
      groupId,
    };
  }

  /**
   * 读取权限：系统管理员可读全量；院管可读本院。
   */
  private assertUserReadable(
    actor: ActorContext,
    target: Pick<typeof SAFE_USER_SELECT, never> & {
      id: number;
      role: Role;
      hospitalId: number | null;
      departmentId: number | null;
      groupId: number | null;
    },
  ) {
    if (actor.role === Role.SYSTEM_ADMIN) {
      return;
    }
    if (actor.id === target.id) {
      return;
    }

    if (actor.role === Role.HOSPITAL_ADMIN) {
      const actorHospitalId = this.requireActorScopeInt(
        actor.hospitalId,
        MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
      );
      if (target.hospitalId === actorHospitalId) {
        return;
      }
    }

    if (actor.role === Role.DIRECTOR) {
      const actorDepartmentId = this.requireActorScopeInt(
        actor.departmentId,
        MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED,
      );
      if (
        target.departmentId === actorDepartmentId &&
        (DIRECTOR_VISIBLE_ROLES as readonly Role[]).includes(target.role)
      ) {
        return;
      }
    }

    if (actor.role === Role.LEADER) {
      const actorGroupId = this.requireActorScopeInt(
        actor.groupId,
        MESSAGES.ORG.ACTOR_GROUP_REQUIRED,
      );
      if (
        target.groupId === actorGroupId &&
        (LEADER_VISIBLE_ROLES as readonly Role[]).includes(target.role)
      ) {
        return;
      }
    }

    throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
  }

  /**
   * 写权限：院管可写本院非管理员账号。
   */
  private assertUserWritable(
    actor: ActorContext,
    target: {
      id: number;
      role: Role;
      hospitalId: number | null;
      departmentId: number | null;
    },
  ) {
    if (actor.role === Role.SYSTEM_ADMIN) {
      return;
    }

    if (actor.role !== Role.HOSPITAL_ADMIN) {
      throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN);
    }

    const actorHospitalId = this.requireActorScopeInt(
      actor.hospitalId,
      MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
    );
    if (target.hospitalId !== actorHospitalId) {
      throw new ForbiddenException(
        MESSAGES.USER.HOSPITAL_ADMIN_SCOPE_FORBIDDEN,
      );
    }
    if (
      target.role === Role.HOSPITAL_ADMIN ||
      target.role === Role.SYSTEM_ADMIN
    ) {
      throw new ForbiddenException(
        MESSAGES.USER.HOSPITAL_ADMIN_SCOPE_FORBIDDEN,
      );
    }
  }

  /**
   * 写入时角色边界校验，避免医院管理员提升权限。
   */
  private assertUpdateTargetRoleAllowed(actor: ActorContext, nextRole: Role) {
    if (actor.role === Role.SYSTEM_ADMIN) {
      return;
    }

    if (
      actor.role === Role.HOSPITAL_ADMIN &&
      (nextRole === Role.SYSTEM_ADMIN || nextRole === Role.HOSPITAL_ADMIN)
    ) {
      throw new ForbiddenException(
        MESSAGES.USER.HOSPITAL_ADMIN_SCOPE_FORBIDDEN,
      );
    }
  }

  /**
   * 写入时医院范围校验，避免医院管理员跨院更新。
   */
  private assertUpdateHospitalScopeAllowed(
    actor: ActorContext,
    hospitalId: number | null,
  ) {
    if (actor.role !== Role.HOSPITAL_ADMIN) {
      return;
    }

    const actorHospitalId = this.requireActorScopeInt(
      actor.hospitalId,
      MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED,
    );
    if (hospitalId !== actorHospitalId) {
      throw new ForbiddenException(
        MESSAGES.USER.HOSPITAL_ADMIN_SCOPE_FORBIDDEN,
      );
    }
  }

  /**
   * 写入时科室范围校验，避免主任跨科调整医生归属。
   */
  private assertUpdateDepartmentScopeAllowed(
    actor: ActorContext,
    departmentId: number | null,
  ) {
    if (actor.role !== Role.HOSPITAL_ADMIN) {
      return;
    }
  }

  /**
   * 签发访问令牌。
   */
  private signAccessToken(actor: ActorContext): string {
    const secret = this.requireAuthSecret();

    return jwt.sign(actor, secret, {
      algorithm: 'HS256',
      expiresIn: '7d',
      issuer: 'tyt-api-nest',
    });
  }

  /**
   * 密码登录候选账号票据签发。
   */
  private signPasswordLoginTicket(payload: PasswordLoginTicketPayload) {
    return jwt.sign(payload, this.requireAuthSecret(), {
      algorithm: 'HS256',
      expiresIn: '5m',
      issuer: 'tyt-api-nest',
    });
  }

  /**
   * 校验密码登录候选账号票据。
   */
  private verifyPasswordLoginTicket(token: string): PasswordLoginTicketPayload {
    let payload: string | jwt.JwtPayload;

    try {
      payload = jwt.verify(token, this.requireAuthSecret(), {
        algorithms: ['HS256'],
        issuer: 'tyt-api-nest',
      });
    } catch {
      throw new UnauthorizedException(
        MESSAGES.AUTH.PASSWORD_LOGIN_TICKET_INVALID,
      );
    }

    if (
      typeof payload !== 'object' ||
      payload.purpose !== 'PASSWORD_LOGIN_TICKET' ||
      !Array.isArray(payload.userIds) ||
      payload.userIds.some(
        (item) => typeof item !== 'number' || !Number.isInteger(item),
      )
    ) {
      throw new UnauthorizedException(
        MESSAGES.AUTH.PASSWORD_LOGIN_TICKET_INVALID,
      );
    }

    return payload as PasswordLoginTicketPayload;
  }

  /**
   * 统一读取认证密钥。
   */
  private requireAuthSecret() {
    const secret = process.env.AUTH_TOKEN_SECRET;
    if (!secret) {
      throw new UnauthorizedException(MESSAGES.AUTH.TOKEN_SECRET_MISSING);
    }
    return secret;
  }
}
