import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { Inject, Injectable } from "@nestjs/common";
import { IamRepository } from "./iam.repository.js";
import { JwksService } from "./jwks.service.js";
import {
  ConflictError,
  UnauthorizedError,
  NotFoundError,
} from "../shared/errors/application-error.js";
import { env } from "../config/env.js";
import { getJwtKeyPair, ttlToSeconds } from "../config/jwt.js";
import { OutboxService } from "@edu/shared-ts/outbox";
import { TokenBlacklistService } from "../shared/cache/token-blacklist.service.js";
import { PermissionCacheService } from "../shared/cache/permission-cache.service.js";
import type { RegisterDto, LoginDto } from "./iam.dto.js";
import type { User } from "./iam.schema.js";

// 默认角色（种子数据 role_id）
const DEFAULT_ROLE_ID = "00000000-0000-0000-0000-000000000002"; // teacher

export interface TokenPair {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
}

export interface UserInfo {
  id: string;
  email: string;
  name: string;
  roles: string[];
  permissions: string[];
  dataScope: string;
  status: string;
}

export interface ViewportItem {
  key: string;
  label: string;
  route: string;
  icon: string | null;
  sortOrder: string;
  requiredPermission: string | null;
}

export interface ChildInfo {
  studentId: string;
  name: string;
  relation: string;
}

/**
 * IAM Application Service（双入口：REST Controller + gRPC Controller 共用）。
 *
 * 12 RPC 方法对齐 iam.proto：
 * Register / Login / RefreshToken / Logout /
 * GetUserInfo / BatchGetUsers /
 * GetEffectivePermissions / GetEffectiveAccess / GetEffectiveDataScope / GetViewports /
 * GetPublicKey / GetChildrenByParent
 *
 * 集成：
 * - RS256 签发（president §2.15）
 * - Outbox 事件发布（I5：UserEvent / RoleEvent / AuditEvent）
 * - 审计日志（president §5.5）
 * - Redis 权限缓存 + token 黑名单（I3 / I7）
 */
@Injectable()
export class IamService {
  constructor(
    @Inject(IamRepository) private readonly repository: IamRepository,
    private readonly jwksService: JwksService,
    private readonly outbox: OutboxService,
    private readonly tokenBlacklist: TokenBlacklistService,
    private readonly permissionCache: PermissionCacheService,
  ) {}

  // ============ 认证类 ============

  async register(
    dto: RegisterDto,
  ): Promise<{ user: UserInfo; tokens: TokenPair }> {
    const existing = await this.repository.findUserByEmail(dto.email);
    if (existing) {
      throw new ConflictError("Email already registered");
    }

    const userId = crypto.randomUUID();
    const passwordHash = await bcrypt.hash(dto.password, 12);
    const user = await this.repository.createUser({
      id: userId,
      email: dto.email,
      passwordHash,
      name: dto.name,
    });

    await this.repository.assignRole(userId, DEFAULT_ROLE_ID);

    const { tokens } = await this.issueTokens(user);

    // Outbox: UserEvent created
    await this.outbox.publish(
      "UserCreated",
      {
        event_id: crypto.randomUUID(),
        aggregate_id: userId,
        event_type: "UserCreated",
        occurred_at: Date.now(),
        user_id: userId,
        email: dto.email,
        name: dto.name,
        roles: ["teacher"],
        data_scope: "self",
        action: "created",
        metadata: {},
      },
      { aggregateId: userId },
    );

    // 审计日志
    await this.writeAuditLog(userId, "create", "user", userId, null, {
      id: userId,
      email: dto.email,
      name: dto.name,
    });

    const info = await this.buildUserInfo(user);
    return { user: info, tokens };
  }

  async login(dto: LoginDto): Promise<{ user: UserInfo; tokens: TokenPair }> {
    const user = await this.repository.findUserByEmail(dto.email);
    if (!user) {
      throw new UnauthorizedError("Invalid credentials");
    }

    const valid = await bcrypt.compare(dto.password, user.passwordHash);
    if (!valid) {
      throw new UnauthorizedError("Invalid credentials");
    }

    if (user.status !== "active") {
      throw new UnauthorizedError("Account is not active");
    }

    const { tokens } = await this.issueTokens(user);

    // 审计日志
    await this.writeAuditLog(user.id, "login", "user", user.id, null, null);

    const info = await this.buildUserInfo(user);
    return { user: info, tokens };
  }

  async refresh(refreshToken: string): Promise<TokenPair> {
    const keyPair = getJwtKeyPair();
    let payload: jwt.JwtPayload;
    try {
      const decoded = jwt.verify(refreshToken, keyPair.publicKey, {
        algorithms: ["RS256"],
        issuer: env.JWT_ISSUER,
        audience: env.JWT_AUDIENCE,
      });
      if (typeof decoded === "string") {
        throw new UnauthorizedError("Invalid refresh token");
      }
      payload = decoded;
    } catch {
      throw new UnauthorizedError("Invalid refresh token");
    }

    if (payload.type !== "refresh") {
      throw new UnauthorizedError("Invalid token type");
    }

    const sub = typeof payload.sub === "string" ? payload.sub : undefined;
    const jti = typeof payload.jti === "string" ? payload.jti : undefined;
    if (!sub) {
      throw new UnauthorizedError("Invalid token subject");
    }

    // 检查黑名单
    if (jti) {
      const blacklisted = await this.tokenBlacklist.isBlacklisted(jti);
      if (blacklisted) {
        throw new UnauthorizedError("Token has been revoked");
      }
    }

    const user = await this.repository.findUserById(sub);
    if (!user) {
      throw new NotFoundError("User", sub);
    }

    // 旧 token 加入黑名单（轮换）
    if (jti && payload.exp) {
      const ttl = payload.exp - Math.floor(Date.now() / 1000);
      await this.tokenBlacklist.blacklist(jti, ttl);
    }

    return this.issueTokens(user).then((r) => r.tokens);
  }

  async logout(refreshToken: string, userId: string): Promise<void> {
    const keyPair = getJwtKeyPair();
    try {
      const decoded = jwt.verify(refreshToken, keyPair.publicKey, {
        algorithms: ["RS256"],
        issuer: env.JWT_ISSUER,
        audience: env.JWT_AUDIENCE,
      });
      const payload = typeof decoded === "string" ? null : decoded;
      const jti = payload?.jti;
      const exp = payload?.exp;

      if (jti && exp) {
        const ttl = exp - Math.floor(Date.now() / 1000);
        await this.tokenBlacklist.blacklist(jti, ttl);
      }
    } catch {
      // token 无效也视为已登出，不抛错
    }

    await this.repository.revokeAllUserTokens(userId);
    await this.writeAuditLog(userId, "logout", "user", userId, null, null);
  }

  // ============ 用户信息类 ============

  async getUserInfo(userId: string): Promise<UserInfo> {
    const user = await this.repository.findUserById(userId);
    if (!user) {
      throw new NotFoundError("User", userId);
    }
    return this.buildUserInfo(user);
  }

  async batchGetUsers(userIds: string[]): Promise<UserInfo[]> {
    const users = await this.repository.batchFindUsers(userIds);
    return Promise.all(users.map((u) => this.buildUserInfo(u)));
  }

  // ============ 权限与视口类 ============

  async getEffectivePermissions(userId: string): Promise<string[]> {
    // 先查缓存
    const cached = await this.permissionCache.getPermissions(userId);
    if (cached) return cached;

    const perms = await this.repository.getUserPermissions(userId);
    const unique = [...new Set(perms.map((p) => p.name))];
    await this.permissionCache.setPermissions(userId, unique);
    return unique;
  }

  async getEffectiveAccess(
    userId: string,
    permission: string,
  ): Promise<{ allowed: boolean; dataScope: string }> {
    const user = await this.repository.findUserById(userId);
    if (!user) {
      throw new NotFoundError("User", userId);
    }

    const perms = await this.getEffectivePermissions(userId);
    const allowed = user.dataScope === "all" || perms.includes(permission);
    return { allowed, dataScope: user.dataScope };
  }

  async getEffectiveDataScope(userId: string): Promise<string> {
    const user = await this.repository.findUserById(userId);
    if (!user) {
      throw new NotFoundError("User", userId);
    }
    return user.dataScope;
  }

  async getViewports(userId: string): Promise<ViewportItem[]> {
    const viewports = await this.repository.getUserViewports(userId);
    const permissions = await this.getEffectivePermissions(userId);

    return viewports
      .filter((vp) => {
        if (!vp.requiredPermission) return true;
        return permissions.includes(vp.requiredPermission);
      })
      .map((vp) => ({
        key: vp.viewportKey,
        label: vp.label,
        route: vp.route,
        icon: vp.icon,
        sortOrder: vp.sortOrder,
        requiredPermission: vp.requiredPermission,
      }))
      .sort((a, b) => a.sortOrder.localeCompare(b.sortOrder));
  }

  // ============ 密钥与关系类 ============

  getPublicKey(): { kid: string; alg: string; publicKeyPem: string } {
    return this.jwksService.getPublicKeyPem();
  }

  async getChildrenByParent(parentId: string): Promise<ChildInfo[]> {
    const children = await this.repository.getChildrenByGuardian(parentId);
    return children.map((c) => ({
      studentId: c.studentId,
      name: c.studentName,
      relation: c.relation,
    }));
  }

  // ============ 管理端查询 ============

  async getAllRoles() {
    return this.repository.getAllRoles();
  }

  async getAllPermissions() {
    return this.repository.getAllPermissions();
  }

  // ============ 私有方法 ============

  private async issueTokens(user: User): Promise<{ tokens: TokenPair }> {
    const keyPair = getJwtKeyPair();
    const roles = await this.repository.getUserRoles(user.id);
    const roleNames = roles.map((r) => r.name);
    const dataScope = user.dataScope;
    const jti = crypto.randomUUID();
    const accessTtl = env.ACCESS_TOKEN_TTL;
    const refreshTtlDays = Number.parseInt(env.REFRESH_TOKEN_TTL_DAYS, 10);
    const refreshTtlSeconds = refreshTtlDays * 86400;

    const accessToken = jwt.sign(
      {
        sub: user.id,
        email: user.email,
        roles: roleNames,
        dataScope,
        type: "access",
      },
      keyPair.privateKey,
      {
        algorithm: "RS256",
        issuer: env.JWT_ISSUER,
        audience: env.JWT_AUDIENCE,
        expiresIn: ttlToSeconds(accessTtl),
        keyid: keyPair.kid,
      },
    );

    const refreshToken = jwt.sign(
      { sub: user.id, type: "refresh", jti },
      keyPair.privateKey,
      {
        algorithm: "RS256",
        issuer: env.JWT_ISSUER,
        audience: env.JWT_AUDIENCE,
        expiresIn: refreshTtlSeconds,
        keyid: keyPair.kid,
      },
    );

    // 存储 refresh token hash + jti
    const tokenHash = await bcrypt.hash(refreshToken, 10);
    await this.repository.createRefreshToken({
      id: crypto.randomUUID(),
      userId: user.id,
      tokenHash,
      jti,
      expiresAt: new Date(Date.now() + refreshTtlSeconds * 1000),
    });

    return {
      tokens: {
        accessToken,
        refreshToken,
        expiresIn: ttlToSeconds(accessTtl),
      },
    };
  }

  private async buildUserInfo(user: User): Promise<UserInfo> {
    const roles = await this.repository.getUserRoles(user.id);
    const permissions = await this.repository.getUserPermissions(user.id);
    return {
      id: user.id,
      email: user.email,
      name: user.name,
      roles: roles.map((r) => r.name),
      permissions: permissions.map((p) => p.name),
      dataScope: user.dataScope,
      status: user.status,
    };
  }

  private async writeAuditLog(
    actorUserId: string,
    action: string,
    resourceType: string,
    resourceId: string,
    beforeState: unknown,
    afterState: unknown,
  ): Promise<void> {
    await this.repository.createAuditLog({
      id: crypto.randomUUID(),
      actorUserId,
      action,
      resourceType,
      resourceId,
      beforeState: beforeState ? JSON.stringify(beforeState) : null,
      afterState: afterState ? JSON.stringify(afterState) : null,
      ip: null,
      userAgent: null,
      traceId: null,
    });

    // Outbox: AuditEvent
    await this.outbox.publish(
      "AuditCreated",
      {
        event_id: crypto.randomUUID(),
        aggregate_id: resourceId,
        event_type: "AuditCreated",
        occurred_at: Date.now(),
        actor_user_id: actorUserId,
        action,
        resource_type: resourceType,
        resource_id: resourceId,
        before_state: beforeState ? JSON.stringify(beforeState) : "",
        after_state: afterState ? JSON.stringify(afterState) : "",
        ip: "",
        user_agent: "",
        trace_id: "",
        metadata: {},
      },
      { aggregateId: resourceId },
    );
  }
}
