import bcrypt from "bcryptjs";
import jwt, { JwtPayload } from "jsonwebtoken";
import { randomBytes, createCipheriv, createDecipheriv, createHash } from "crypto";

// 인증/보안 관련 공통 유틸리티 함수들을 제공한다.
// - 비밀번호 해시/검증 (bcrypt 기반)
// - JWT 액세스 토큰 발급/검증
// - 민감정보 JSON 암호화/복호화 (AES-256-GCM)

export interface AccessTokenPayload extends JwtPayload {
  sub: string;
  email: string;
  tokenVersion: number;
}

export interface JwtUserLike {
  id: string;
  email: string;
  tokenVersion: number;
}

// 내부에서 사용할 JWT 시크릿을 가져온다.
// - 설정되지 않은 경우에는 명확한 에러를 던져 조기에 문제를 발견한다.
function getJwtSecret(): string {
  const secret = process.env.AUTH_JWT_SECRET;
  if (!secret || secret.length === 0) {
    throw new Error("AUTH_JWT_SECRET 환경변수가 설정되지 않았습니다.");
  }
  return secret;
}

// 암호화 키 원본 문자열로부터 32바이트 AES 키를 파생한다.
// - 키 문자열 길이에 관계없이 SHA-256 해시를 사용해 32바이트를 만든다.
function deriveEncryptionKey(): Buffer {
  const raw = process.env.AUTH_ENCRYPTION_KEY;
  if (!raw || raw.length === 0) {
    throw new Error("AUTH_ENCRYPTION_KEY 환경변수가 설정되지 않았습니다.");
  }
  return createHash("sha256").update(raw).digest();
}

// 최소 비밀번호 길이 (보안 요구사항: 8자 이상)
const MIN_PASSWORD_LENGTH = 8;

// 비밀번호를 bcrypt 해시로 변환한다.
export async function hashPassword(plain: string): Promise<string> {
  if (typeof plain !== "string" || plain.length < MIN_PASSWORD_LENGTH) {
    throw new Error("비밀번호는 최소 8자 이상이어야 합니다.");
  }

  // bcryptjs 기본 라운드 수(10)를 사용한다. 필요 시 환경에 맞게 조정 가능.
  const saltRounds = 10;
  return await bcrypt.hash(plain, saltRounds);
}

// 평문 비밀번호와 저장된 해시가 일치하는지 검증한다.
export async function verifyPassword(plain: string, hash: string): Promise<boolean> {
  if (!hash) return false;
  if (!plain || plain.length < MIN_PASSWORD_LENGTH) {
    // 너무 짧은 비밀번호는 즉시 false 를 반환해 타이밍 공격을 줄인다.
    return false;
  }

  return await bcrypt.compare(plain, hash);
}

// 유저 정보를 기반으로 JWT 액세스 토큰을 발급한다.
export async function signAccessToken(user: JwtUserLike): Promise<string> {
  const secret = getJwtSecret();

  const payload: AccessTokenPayload = {
    sub: user.id,
    email: user.email,
    tokenVersion: user.tokenVersion,
  };

  // 만료 시간은 기본 15분으로 설정한다.
  const token = jwt.sign(payload, secret, {
    algorithm: "HS256",
    expiresIn: "15m",
  });

  return token;
}

// JWT 액세스 토큰을 검증하고, 유효하면 페이로드를 반환한다.
// - 검증 실패(시그니처 오류, 만료 등) 시 null 을 반환한다.
export async function verifyAccessToken(token: string): Promise<AccessTokenPayload | null> {
  const secret = getJwtSecret();

  try {
    const decoded = jwt.verify(token, secret) as AccessTokenPayload;
    if (!decoded || typeof decoded.sub !== "string") {
      return null;
    }
    return decoded;
  } catch {
    return null;
  }
}

// JSON 객체를 AES-256-GCM 으로 암호화한다.
// - 결과는 iv.tag.ciphertext 형태의 base64 조합 문자열이다.
export async function encryptJson<T extends object>(value: T): Promise<string> {
  const key = deriveEncryptionKey();
  const iv = randomBytes(12); // GCM 권장 IV 길이 96bit

  const plain = JSON.stringify(value);

  const cipher = createCipheriv("aes-256-gcm", key, iv);
  let encrypted = cipher.update(plain, "utf8", "base64");
  encrypted += cipher.final("base64");
  const authTag = cipher.getAuthTag();

  const ivB64 = iv.toString("base64");
  const tagB64 = authTag.toString("base64");

  return `${ivB64}.${tagB64}.${encrypted}`;
}

// AES-256-GCM 으로 암호화된 JSON 문자열을 복호화한다.
export async function decryptJson<T = unknown>(ciphertext: string): Promise<T> {
  const key = deriveEncryptionKey();

  try {
    const parts = ciphertext.split(".");
    if (parts.length !== 3) {
      throw new Error("잘못된 암호문 형식입니다.");
    }

    const [ivB64, tagB64, dataB64] = parts;
    const iv = Buffer.from(ivB64, "base64");
    const authTag = Buffer.from(tagB64, "base64");

    const decipher = createDecipheriv("aes-256-gcm", key, iv);
    decipher.setAuthTag(authTag);

    let decrypted = decipher.update(dataB64, "base64", "utf8");
    decrypted += decipher.final("utf8");

    return JSON.parse(decrypted) as T;
  } catch (error) {
    // 복호화 실패 시 명시적인 에러를 던져 상위에서 처리할 수 있게 한다.
    throw new Error("민감정보 복호화에 실패했습니다.");
  }
}
