import { ConfigService } from '@nestjs/config';
import * as jwt from 'jsonwebtoken';
// XXX: decodeがうまく使えないことがあるので応急対応 バージョン9以降だとなる？
import { decode as jwtDecode } from 'jsonwebtoken';

export type VerifyError = {
  reason: 'ExpiredError' | 'InvalidToken' | 'InvalidTimeStamp' | 'Unknown';
  message: string;
};

export const isVerifyError = (arg: unknown): arg is VerifyError => {
  const value = arg as VerifyError;
  if (value.message === undefined) {
    return false;
  }

  if (value.reason === undefined) {
    return false;
  }
  switch (value.reason) {
    case 'ExpiredError':
    case 'InvalidTimeStamp':
    case 'InvalidToken':
    case 'Unknown':
      return true;
    default:
      return false;
  }
};

/**
 * Payloadと秘密鍵を使用して署名されたJWTを生成します
 * @param {T} payload payloadの型
 * @param {number} expirationSeconds トークンの有効期限(秒)
 * @param {string} privateKey 署名に使用する秘密鍵
 * @return {string} 署名済みトークン
 * @throws {Error} 秘密鍵の形式が間違っている等の理由が格納されたErrorオブジェクト
 */
export const sign = <T extends object>(
  payload: T,
  expirationSeconds: number,
  privateKey: string,
): string => {
  try {
    const token = jwt.sign(payload, privateKey, {
      expiresIn: expirationSeconds,
      algorithm: 'RS256',
    });
    return token;
  } catch (e) {
    throw e;
  }
};

/**
 * tokenと公開鍵を使用して検証済みJWTのpayloadを取得します
 * @param {string} token JWT
 * @param {string} publicKey 検証に使用する公開鍵
 * @return {T | VerifyError} Payload または 検証エラーの内容を表すオブジェクト
 */
export const verify = <T extends object>(
  token: string,
  publicKey: string,
): T | VerifyError => {
  try {
    const payload = jwt.verify(token, publicKey, {
      algorithms: ['RS256'],
    }) as T;
    return payload;
  } catch (e) {
    if (e instanceof jwt.TokenExpiredError) {
      return {
        reason: 'ExpiredError',
        message: e.message,
      };
    } else if (e instanceof jwt.NotBeforeError) {
      return {
        reason: 'InvalidTimeStamp',
        message: e.message,
      };
    } else if (e instanceof jwt.JsonWebTokenError) {
      return {
        reason: 'InvalidToken',
        message: e.message,
      };
    } else {
      return {
        reason: 'Unknown',
        message: e.message,
      };
    }
  }
};

/**
 * tokenから未検証のJWTのpayloadを取得します
 * @param {string} token JWT
 * @return {T | VerifyError} Payload または デコードエラーの内容を表すオブジェクト
 */
export const decode = <T extends object>(token: string): T | VerifyError => {
  try {
    const payload = jwtDecode(token, {
      json: true,
    }) as T;
    return payload;
  } catch (e) {
    if (e instanceof jwt.TokenExpiredError) {
      return {
        reason: 'ExpiredError',
        message: e.message,
      };
    } else if (e instanceof jwt.NotBeforeError) {
      return {
        reason: 'InvalidTimeStamp',
        message: e.message,
      };
    } else if (e instanceof jwt.JsonWebTokenError) {
      return {
        reason: 'InvalidToken',
        message: e.message,
      };
    } else {
      return {
        reason: 'Unknown',
        message: e.message,
      };
    }
  }
};

export const getPrivateKey = (configService: ConfigService): string => {
  return (
    // 開発環境用に改行コードを置換する
    // 本番環境では\\nが含まれないため、置換が行われない想定
    configService.get<string>('JWT_PRIVATE_KEY')?.replace(/\\n/g, '\n') ?? ''
  );
};

export const getPublicKey = (configService: ConfigService): string => {
  return (
    // 開発環境用に改行コードを置換する
    // 本番環境では\\nが含まれないため、置換が行われない想定
    configService.get<string>('JWT_PUBLIC_KEY')?.replace(/\\n/g, '\n') ?? ''
  );
};
