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 MockLoginRequest,
  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];
  }

  // ─────────────────────────────────────────────────────────────
  // Mock Login(开发 / 试部署用 — env 门控,生产关闭)
  // ─────────────────────────────────────────────────────────────
  //
  // 用途:试部署期间客服 / 演示用户没真 host SSO 也能登录工作台。
  // 安全:env 门控(NODE_ENV != production 或 PAC_ENABLE_MOCK_LOGIN=true);
  //      生产真 host 接入后,删 endpoint 或把 env 设 false。

  /// jvs-dw 两个 brand 的预制 user payload(对齐 manifest.yaml tenant_map)
  /// 来源:`apps/pac-service/data/jvs-dw/manifest.yaml` §"tenant 路由"
  ///       `docs/deployment-data-ingest.md` §clinic UUID 列表
  private readonly MOCK_PRESETS = {
    ruier: {
      tenantId: 'ba67e6cf30dc4f9c9c46adef188bbd04',
      tenantNameZh: '瑞尔',
      clinics: {
        '7d49539c7573490387c03e6496ff1a6c': '杭州大厦诊所',
        dad2f04a120947e2b82b41cbd108f3f4: '杭州高德诊所',
        '66701845dd2342e19f9e9f576c4ffe9c': '北京朝阳公园诊所',
      },
    },
    ruitai: {
      tenantId: '77057aed269f4a14957ae0ad0eff359a',
      tenantNameZh: '瑞泰',
      clinics: {
        c18cadf2d3cd4adda5527debd41356eb: '通善口腔学前街医院',
        e83d432a38bb4f6284713b36db4e7497: '上海世纪公园',
      },
    },
  } as const;

  isMockLoginEnabled(): boolean {
    if (process.env.PAC_ENABLE_MOCK_LOGIN === 'true') return true;
    if (process.env.PAC_ENABLE_MOCK_LOGIN === 'false') return false;
    return (this.config.get<string>('nodeEnv') ?? process.env.NODE_ENV) !== 'production';
  }

  async mockLogin(req: MockLoginRequest): Promise<ExchangeCodeResponse> {
    if (!this.isMockLoginEnabled()) {
      throw new BizError(ApiCode.AUTH_PERMISSION_DENIED, '模拟登录已在当前环境禁用');
    }
    const preset = this.MOCK_PRESETS[req.tenant];
    if (!preset) {
      throw new BizError(ApiCode.CLIENT_VALIDATION_FAILED, `unknown tenant: ${req.tenant}`);
    }

    // 找 jvs-dw host(seed 时已建)
    const host = await this.prisma.host.findFirst({ where: { name: 'jvs-dw' } });
    if (!host) {
      throw new BizError(
        ApiCode.HOST_NOT_FOUND,
        '请先 seed jvs-dw host(pnpm --filter @pac/service exec prisma db seed)',
      );
    }

    const clinicIds = Object.keys(preset.clinics);
    const dictionary: TokenDictionary = {
      clinics: preset.clinics as Record<string, string>,
      users: {
        // 给 sub 一个可读名,UI 头像右侧显示
        [`mock-${req.tenant}-${req.role}`]: `${preset.tenantNameZh}·${roleNameZh(req.role)}(模拟)`,
      },
    };
    const permissions = this.resolvePermissions(req.role);

    const accessToken = await this.signAccessToken({
      sub: `mock-${req.tenant}-${req.role}`,
      hostId: host.id,
      tenantId: preset.tenantId,
      clinicIds,
      role: req.role,
      permissions,
      dictionary,
    });
    const refreshToken = await this.signRefreshToken({
      sub: `mock-${req.tenant}-${req.role}`,
      hostId: host.id,
      tenantId: preset.tenantId,
      clinicIds,
      role: req.role,
      dictionary,
    });
    const expiresIn = parseDurationToSeconds(this.config.getOrThrow<string>('jwt.expiresIn'));

    this.logger.log(
      `mock-login: tenant=${req.tenant}(${preset.tenantNameZh}) role=${req.role} ` +
        `clinics=${clinicIds.length} hostId=${host.id}`,
    );
    return { accessToken, refreshToken, expiresIn };
  }
}

function roleNameZh(role: UserRole): string {
  return ({ staff: '员工', leader: '主管', admin: '管理员' } as const)[role] ?? role;
}
