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 MockUser,
  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';
import { loadMockUsers } from './mock-users';

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[];
  sourceUnits?: 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,
  ) {}

  /** access token TTL(秒)— 进程内固定,memoize 一次(收口多处 parseDurationToSeconds) */
  private _accessExpiresInSeconds?: number;
  private get accessExpiresInSeconds(): number {
    return (this._accessExpiresInSeconds ??= parseDurationToSeconds(
      this.config.getOrThrow<string>('jwt.expiresIn'),
    ));
  }

  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 = this.accessExpiresInSeconds;

    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 = this.accessExpiresInSeconds;
    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,
      sourceUnits: payload.sourceUnits,
      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,
      sourceUnits: payload.sourceUnits,
      role: payload.role,
      dictionary: payload.dictionary,
    });

    const expiresIn = this.accessExpiresInSeconds;
    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: this.accessExpiresInSeconds,
    });
  }

  private async signRefreshToken(input: {
    sub: string;
    hostId: string;
    tenantId: string;
    clinicIds: string[];
    sourceUnits?: 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 预制 user payload(集团模型:tenant=集团 slug,品牌=source_unit)
  /// 来源:`apps/pac-service/data/jvs-dw/manifest.yaml` §"集团模型"
  ///       `docs/deployment-data-ingest.md` §clinic UUID 列表
  private readonly MOCK_PRESETS = {
    ruier: {
      tenantId: 'ruier-grp', // 集团(= 租户)
      sourceUnit: '瑞尔', // 该用户所属品牌(数据范围按它圈本品牌)
      tenantNameZh: '瑞尔',
      clinics: {
        '7d49539c7573490387c03e6496ff1a6c': '杭州大厦诊所',
        dad2f04a120947e2b82b41cbd108f3f4: '杭州高德诊所',
        '66701845dd2342e19f9e9f576c4ffe9c': '北京朝阳公园诊所',
      },
    },
    ruitai: {
      tenantId: 'ruier-grp', // 同集团
      sourceUnit: '瑞泰',
      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)',
      );
    }

    // 默认:通用预制身份(sub=mock-<tenant>-<role>,看该 tenant 全部诊所)
    let sub = `mock-${req.tenant}-${req.role}`;
    let subName = MOCK_NAMES[req.tenant]?.[req.role] ?? roleNameZh(req.role);
    let clinicIds = Object.keys(preset.clinics);

    // 选了具体客服 → 用真实客服身份(sub=externalId / 真实姓名 / 该客服所属诊所)
    if (req.userExternalId) {
      const cs = loadMockUsers().find(
        (u) => u.tenant === req.tenant && u.externalId === req.userExternalId,
      );
      if (!cs) {
        throw new BizError(
          ApiCode.CLIENT_VALIDATION_FAILED,
          `unknown mock user: ${req.tenant}/${req.userExternalId}`,
        );
      }
      sub = cs.externalId;
      subName = cs.name;
      // 只保留落在该 tenant 预制诊所内的(防脏数据);为空兜底回全 tenant 诊所
      const own = cs.clinics.map((c) => c.clinicId).filter((id) => id in preset.clinics);
      clinicIds = own.length > 0 ? own : Object.keys(preset.clinics);
    }

    const dictionary: TokenDictionary = {
      clinics: preset.clinics as Record<string, string>,
      // 给 sub 一个可读**人名**(UI 头像显示 + 话术自报家门"我是X诊所的客服{姓名}"用)
      users: { [sub]: subName },
    };
    const permissions = this.resolvePermissions(req.role);

    const accessToken = await this.signAccessToken({
      sub,
      hostId: host.id,
      tenantId: preset.tenantId,
      clinicIds,
      sourceUnits: [preset.sourceUnit], // 数据范围按本品牌圈(集团内品牌隔离,复刻现状)
      role: req.role,
      permissions,
      dictionary,
    });
    const refreshToken = await this.signRefreshToken({
      sub,
      hostId: host.id,
      tenantId: preset.tenantId,
      clinicIds,
      sourceUnits: [preset.sourceUnit],
      role: req.role,
      dictionary,
    });
    const expiresIn = this.accessExpiresInSeconds;

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

  /**
   * 客服花名册(给"快速登录"对话框选人用)。
   * 派生自 data/jvs-dw/users.json,补诊所中文名(从 MOCK_PRESETS)。mock 关闭时返空。
   */
  listMockUsers(): MockUser[] {
    if (!this.isMockLoginEnabled()) return [];
    return loadMockUsers()
      .map((u) => {
        const preset = this.MOCK_PRESETS[u.tenant];
        const clinicDict = (preset?.clinics ?? {}) as Record<string, string>;
        return {
          externalId: u.externalId,
          name: u.name,
          tenant: u.tenant,
          lastActiveAt: u.lastActiveAt,
          contactCount: u.clinics.reduce((s, c) => s + (c.contactCount ?? 0), 0),
          clinics: u.clinics.map((c) => ({
            clinicId: c.clinicId,
            name: clinicDict[c.clinicId] ?? c.clinicId,
          })),
        };
      })
      .sort(
        (a, b) =>
          a.tenant.localeCompare(b.tenant) || b.contactCount - a.contactCount,
      );
  }
}

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

/// 模拟登录的演示人名(2 brand × 3 role);话术自报家门会用到("我是X诊所的客服小王")。
const MOCK_NAMES: Record<string, Partial<Record<UserRole, string>>> = {
  ruier: { staff: '小王', leader: '李莉', admin: '张敏' },
  ruitai: { staff: '小陈', leader: '周琳', admin: '刘洋' },
};
