/**
 * File Upload Security - 파일 업로드 보안 유틸리티
 *
 * - Magic number (파일 시그니처) 검증
 * - 파일명 sanitize
 * - 확장자 화이트리스트 검증
 * - 파일 크기 제한
 *
 * @author TK-FB-Project
 * @since 2026-02-04
 */

const path = require('path');
const crypto = require('crypto');
const fs = require('fs').promises;

/**
 * 파일 시그니처 (Magic Numbers)
 * 파일의 실제 타입을 확인하기 위한 바이너리 시그니처
 */
const FILE_SIGNATURES = {
  // 이미지
  'ffd8ff': { mime: 'image/jpeg', ext: ['.jpg', '.jpeg'] },
  '89504e47': { mime: 'image/png', ext: ['.png'] },
  '47494638': { mime: 'image/gif', ext: ['.gif'] },
  '52494646': { mime: 'image/webp', ext: ['.webp'] }, // RIFF (WebP 시작)

  // 문서
  '25504446': { mime: 'application/pdf', ext: ['.pdf'] },
  '504b0304': { mime: 'application/zip', ext: ['.zip', '.xlsx', '.docx', '.pptx'] },

  // 주의: BMP, TIFF 등 추가 가능
};

/**
 * 허용된 이미지 확장자
 */
const ALLOWED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];

/**
 * 허용된 문서 확장자
 */
const ALLOWED_DOCUMENT_EXTENSIONS = ['.pdf', '.xlsx', '.docx', '.pptx', '.zip'];

/**
 * 위험한 확장자 (절대 허용 안 함)
 */
const DANGEROUS_EXTENSIONS = [
  '.exe', '.bat', '.cmd', '.sh', '.ps1', '.vbs', '.js', '.jar',
  '.php', '.asp', '.aspx', '.jsp', '.cgi', '.pl', '.py', '.rb',
  '.htaccess', '.htpasswd', '.ini', '.config', '.env'
];

/**
 * 파일 시그니처(Magic Number) 검증
 *
 * @param {Buffer} buffer - 파일 버퍼 (최소 8바이트)
 * @returns {Object|null} 매칭된 파일 정보 또는 null
 */
const checkMagicNumber = (buffer) => {
  if (!buffer || buffer.length < 4) {
    return null;
  }

  // 처음 8바이트를 hex로 변환
  const hex = buffer.slice(0, 8).toString('hex').toLowerCase();

  // 시그니처 매칭
  for (const [signature, info] of Object.entries(FILE_SIGNATURES)) {
    if (hex.startsWith(signature)) {
      return info;
    }
  }

  return null;
};

/**
 * 파일 버퍼에서 실제 MIME 타입 검증
 *
 * @param {Buffer} buffer - 파일 버퍼
 * @param {string} declaredMime - 선언된 MIME 타입
 * @returns {Object} { valid: boolean, actualType: string|null, message: string }
 */
const validateFileType = (buffer, declaredMime) => {
  const detected = checkMagicNumber(buffer);

  if (!detected) {
    return {
      valid: false,
      actualType: null,
      message: '알 수 없는 파일 형식입니다.'
    };
  }

  // MIME 타입이 일치하는지 확인
  if (detected.mime !== declaredMime) {
    return {
      valid: false,
      actualType: detected.mime,
      message: `파일 형식이 일치하지 않습니다. (선언: ${declaredMime}, 실제: ${detected.mime})`
    };
  }

  return {
    valid: true,
    actualType: detected.mime,
    message: 'OK'
  };
};

/**
 * 파일명 sanitize
 * 경로 조작 및 특수문자 제거
 *
 * @param {string} filename - 원본 파일명
 * @returns {string} 안전한 파일명
 */
const sanitizeFilename = (filename) => {
  if (!filename || typeof filename !== 'string') {
    return 'unnamed';
  }

  // 경로 구분자 제거 (path traversal 방지)
  let safe = path.basename(filename);

  // 특수문자 제거 (영문, 숫자, -, _, . 만 허용)
  safe = safe.replace(/[^a-zA-Z0-9가-힣._-]/g, '_');

  // 연속된 점 제거 (이중 확장자 방지)
  safe = safe.replace(/\.{2,}/g, '.');

  // 앞뒤 점/공백 제거
  safe = safe.replace(/^[\s.]+|[\s.]+$/g, '');

  // 빈 파일명 처리
  if (!safe || safe === '') {
    safe = 'unnamed';
  }

  // 최대 길이 제한 (255자)
  if (safe.length > 255) {
    const ext = path.extname(safe);
    const name = path.basename(safe, ext);
    safe = name.slice(0, 255 - ext.length) + ext;
  }

  return safe;
};

/**
 * 확장자 검증
 *
 * @param {string} filename - 파일명
 * @param {string[]} allowedExtensions - 허용된 확장자 배열
 * @returns {Object} { valid: boolean, extension: string, message: string }
 */
const validateExtension = (filename, allowedExtensions = ALLOWED_IMAGE_EXTENSIONS) => {
  const ext = path.extname(filename).toLowerCase();

  // 위험한 확장자 체크
  if (DANGEROUS_EXTENSIONS.includes(ext)) {
    return {
      valid: false,
      extension: ext,
      message: `보안상 허용되지 않는 파일 형식입니다: ${ext}`
    };
  }

  // 허용된 확장자 체크
  if (!allowedExtensions.includes(ext)) {
    return {
      valid: false,
      extension: ext,
      message: `허용된 파일 형식: ${allowedExtensions.join(', ')}`
    };
  }

  return {
    valid: true,
    extension: ext,
    message: 'OK'
  };
};

/**
 * 안전한 랜덤 파일명 생성
 *
 * @param {string} originalFilename - 원본 파일명 (확장자 추출용)
 * @returns {string} 랜덤 파일명
 */
const generateSafeFilename = (originalFilename) => {
  const ext = path.extname(originalFilename).toLowerCase();
  const randomName = crypto.randomBytes(16).toString('hex');
  const timestamp = Date.now();

  return `${timestamp}_${randomName}${ext}`;
};

/**
 * 안전한 업로드 경로 생성
 * 경로 조작(path traversal) 방지
 *
 * @param {string} baseDir - 기본 업로드 디렉토리
 * @param {string} filename - 파일명
 * @returns {string} 안전한 전체 경로
 */
const getSafeUploadPath = (baseDir, filename) => {
  const safeName = sanitizeFilename(filename);
  const fullPath = path.join(baseDir, safeName);

  // 결과 경로가 baseDir 안에 있는지 확인
  const resolvedBase = path.resolve(baseDir);
  const resolvedFull = path.resolve(fullPath);

  if (!resolvedFull.startsWith(resolvedBase)) {
    throw new Error('경로 조작이 감지되었습니다.');
  }

  return resolvedFull;
};

/**
 * Multer 파일 필터 생성
 *
 * @param {Object} options - 옵션
 * @param {string[]} options.allowedExtensions - 허용된 확장자
 * @param {string[]} options.allowedMimes - 허용된 MIME 타입
 * @param {boolean} options.checkMagicNumber - Magic number 검증 여부
 * @returns {Function} Multer fileFilter 함수
 */
const createFileFilter = (options = {}) => {
  const {
    allowedExtensions = ALLOWED_IMAGE_EXTENSIONS,
    allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
    checkMagicNumber = false // Multer에서는 버퍼 접근이 제한적이므로 기본 false
  } = options;

  return (req, file, cb) => {
    // 확장자 검증
    const extResult = validateExtension(file.originalname, allowedExtensions);
    if (!extResult.valid) {
      return cb(new Error(extResult.message), false);
    }

    // MIME 타입 검증
    if (!allowedMimes.includes(file.mimetype)) {
      return cb(new Error(`허용된 MIME 타입: ${allowedMimes.join(', ')}`), false);
    }

    cb(null, true);
  };
};

/**
 * 업로드된 파일 검증 (후처리용)
 * Multer 업로드 후 파일 내용을 검증
 *
 * @param {string} filePath - 업로드된 파일 경로
 * @param {string} declaredMime - 선언된 MIME 타입
 * @returns {Promise<Object>} 검증 결과
 */
const validateUploadedFile = async (filePath, declaredMime) => {
  try {
    // 파일 시작 부분 읽기
    const fd = await fs.open(filePath, 'r');
    const buffer = Buffer.alloc(8);
    await fd.read(buffer, 0, 8, 0);
    await fd.close();

    // Magic number 검증
    const typeResult = validateFileType(buffer, declaredMime);

    if (!typeResult.valid) {
      // 위험한 파일이면 삭제
      await fs.unlink(filePath);
      return {
        valid: false,
        deleted: true,
        message: typeResult.message
      };
    }

    return {
      valid: true,
      deleted: false,
      message: 'OK',
      actualType: typeResult.actualType
    };
  } catch (error) {
    return {
      valid: false,
      deleted: false,
      message: `파일 검증 중 오류: ${error.message}`
    };
  }
};

module.exports = {
  // 상수
  ALLOWED_IMAGE_EXTENSIONS,
  ALLOWED_DOCUMENT_EXTENSIONS,
  DANGEROUS_EXTENSIONS,
  FILE_SIGNATURES,

  // 함수
  checkMagicNumber,
  validateFileType,
  sanitizeFilename,
  validateExtension,
  generateSafeFilename,
  getSafeUploadPath,
  createFileFilter,
  validateUploadedFile
};
