import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import {
  ApiCode,
  ROLE_PERMISSIONS,
  type AccessTokenPayload,
  type ExchangeCodeResponse,
  type Permission,
  type RefreshTokenResponse,
  type TokenDictionary,
  type TokenExchangeResponse,
  type TokenExchangeRequest,
  type UserRole,
} from '@pac/types';
import { BizError } from '../../common/errors/biz-error';
import { randomCode, verifySecret } from '@pac/utils';
import { PrismaService } from '../../prisma/prisma.service';
import { RedisService } from '../../redis/redis.service';
import { parseDurationToSeconds } from './duration';

const CODE_PREFIX = 'pac:exchange-code:';
const REFRESH_PREFIX = 'pac:refresh:';

/**
 * Refresh token payload(W3 末 A 方案降级版)
 *
 * 容纳重签 access token 所需的全部 user 上下文(role/clinicIds/dictionary),
 * **无需** refresh 时回调 host SSO 拉权限即可重发 access。
 *
 * 安全考量:
 *   - refresh token 用独立 secret(JWT_REFRESH_SECRET),access 烂不代表 refresh 烂
 *   - Redis jti 白名单(7 天 TTL),被偷的 refresh 不会无限刷(rotation 后老 jti 立即作废)
 *   - 权限变更/禁用最长 7 天(refresh TTL)才生效 — POC 阶段可接受;
 *     生产前要加 host SSO `/users/{id}` 回调拉最新 role/clinics(见下方 TODO)
 */
interface RefreshPayload {
  sub: string;
  hostId: string;
  tenantId: string;
  clinicIds: string[];
  role: UserRole;
  dictionary?: TokenDictionary;
  jti: string;
}

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly redis: RedisService,
    private readonly jwt: JwtService,
    private readonly config: ConfigService,
  ) {}

  async exchangeToken(req: TokenExchangeRequest): Promise<TokenExchangeResponse> {
    const host = await this.prisma.host.findUnique({ where: { appId: req.appId } });
    if (!host || !host.active) {
      throw new BizError(ApiCode.AUTH_INVALID_CREDENTIALS);
    }
    if (!verifySecret(req.appSecret, host.appSecretHash)) {
      throw new BizError(ApiCode.AUTH_INVALID_CREDENTIALS);
    }

    const tenantId = req.user.tenantId;
    const clinicIds = req.user.clinicIds.length > 0 ? req.user.clinicIds : ['default'];
    const permissions = this.resolvePermissions(req.user.role);

    const accessToken = await this.signAccessToken({
      sub: req.user.userId,
      hostId: host.id,
      tenantId,
      clinicIds,
      role: req.user.role,
      permissions,
      // 可选名字典(clinics/users id→名)透传进 JWT,前端用于 UI 显示
      ...(req.user.dictionary ? { dictionary: req.user.dictionary } : {}),
    });

    const refreshToken = await this.signRefreshToken({
      sub: req.user.userId,
      hostId: host.id,
      tenantId,
      clinicIds,
      role: req.user.role,
      dictionary: req.user.dictionary,
    });

    const code = randomCode(24);
    const codeTtl = this.config.getOrThrow<number>('exchangeCodeTtlSeconds');
    await this.redis.setEx(
      CODE_PREFIX + code,
      JSON.stringify({ accessToken, refreshToken }),
      codeTtl,
    );

    const expiresIn = parseDurationToSeconds(this.config.getOrThrow<string>('jwt.expiresIn'));

    return {
      accessToken,
      refreshToken,
      code,
      expiresIn,
      codeExpiresIn: codeTtl,
    };
  }

  async exchangeCode(code: string): Promise<ExchangeCodeResponse> {
    const raw = await this.redis.getDel(CODE_PREFIX + code);
    if (!raw) throw new BizError(ApiCode.AUTH_INVALID_CODE);
    const parsed = JSON.parse(raw) as { accessToken: string; refreshToken: string };
    const expiresIn = parseDurationToSeconds(this.config.getOrThrow<string>('jwt.expiresIn'));
    return {
      accessToken: parsed.accessToken,
      refreshToken: parsed.refreshToken,
      expiresIn,
    };
  }

  /**
   * Refresh access token(W3 末 A 方案 — 本地降级,无 host SSO 回调)
   *
   * 流程:
   *   1. 验证 refresh token 签名(独立 secret JWT_REFRESH_SECRET)
   *   2. Redis 校验 jti 是否还在白名单(未被 rotation 作废、未被人工撤销)
   *   3. 用 refresh payload 里的 sub/role/clinicIds/dictionary 重签新 access token
   *   4. **Rotation**:作废老 jti,签发新 refresh token(新 jti 入 Redis),
   *      防 refresh token 被偷后无限刷;被偷者一旦用了,真主人下次刷新就因老 jti 被撤销而失败 → 早发现
   *
   * TODO(host SSO 集成,W5+):
   *   生产前需在步骤 3 前回调 host `/users/{userId}` 拉最新 role/clinicIds —
   *   防"用户已禁用 / 权限已降级" 还能继续刷 token 用 7 天。
   *   联调 host SSO 后,把 refresh payload 里的 role/clinicIds 改为"仅缓存,以 host 返回为准"。
   */
  async refresh(refreshToken: string): Promise<RefreshTokenResponse> {
    let payload: RefreshPayload;
    try {
      payload = await this.jwt.verifyAsync<RefreshPayload>(refreshToken, {
        secret: this.config.getOrThrow<string>('jwt.refreshSecret'),
      });
    } catch {
      throw new BizError(ApiCode.AUTH_INVALID_REFRESH_TOKEN);
    }

    const stored = await this.redis.get(REFRESH_PREFIX + payload.jti);
    if (!stored) throw new BizError(ApiCode.AUTH_INVALID_REFRESH_TOKEN, 'refresh token revoked');

    // ── 步骤 3:重签 access token(payload 里有完整 user 上下文)──
    const permissions = this.resolvePermissions(payload.role);
    const accessToken = await this.signAccessToken({
      sub: payload.sub,
      hostId: payload.hostId,
      tenantId: payload.tenantId,
      clinicIds: payload.clinicIds,
      role: payload.role,
      permissions,
      ...(payload.dictionary ? { dictionary: payload.dictionary } : {}),
    });

    // ── 步骤 4:rotation — 作废老 jti,签发新 refresh token ──
    await this.redis.del(REFRESH_PREFIX + payload.jti);
    const newRefreshToken = await this.signRefreshToken({
      sub: payload.sub,
      hostId: payload.hostId,
      tenantId: payload.tenantId,
      clinicIds: payload.clinicIds,
      role: payload.role,
      dictionary: payload.dictionary,
    });

    const expiresIn = parseDurationToSeconds(this.config.getOrThrow<string>('jwt.expiresIn'));
    return { accessToken, refreshToken: newRefreshToken, expiresIn };
  }

  async signAccessToken(
    payload: Omit<AccessTokenPayload, 'iat' | 'exp'>,
  ): Promise<string> {
    return this.jwt.signAsync(payload, {
      secret: this.config.getOrThrow<string>('jwt.secret'),
      expiresIn: parseDurationToSeconds(this.config.getOrThrow<string>('jwt.expiresIn')),
    });
  }

  private async signRefreshToken(input: {
    sub: string;
    hostId: string;
    tenantId: string;
    clinicIds: string[];
    role: UserRole;
    dictionary?: TokenDictionary;
  }): Promise<string> {
    const jti = randomCode(16);
    const ttl = parseDurationToSeconds(this.config.getOrThrow<string>('jwt.refreshExpiresIn'));
    const token = await this.jwt.signAsync(
      { ...input, jti },
      {
        secret: this.config.getOrThrow<string>('jwt.refreshSecret'),
        expiresIn: ttl,
      },
    );
    await this.redis.setEx(REFRESH_PREFIX + jti, '1', ttl);
    return token;
  }

  resolvePermissions(role: UserRole): Permission[] {
    return ROLE_PERMISSIONS[role];
  }
}
