package egovframework.util.excel;

import org.apache.poi.poifs.crypt.EncryptionInfo;
import org.apache.poi.poifs.crypt.EncryptionMode;
import org.apache.poi.poifs.crypt.Encryptor;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.security.GeneralSecurityException;
import java.util.List;

import static egovframework.util.excel.SuperClassReflectionUtils.getAllFieldsWithExcelColumn;

/**
 * SXSSF(Streaming) 방식의 엑셀 파일 생성을 위한 추상 베이스 클래스
 *
 * <p>이 클래스는 대용량 엑셀 파일 생성을 위해 Apache POI의 SXSSF(Streaming) API를 활용합니다.
 * SXSSF는 메모리에 일정 개수의 행만 유지하고 나머지는 디스크에 flush하여 메모리 사용량을 최소화합니다.</p>
 *
 * <p><b>주요 기능:</b></p>
 * <ul>
 *   <li>대용량 데이터 엑셀 파일 생성 (메모리 효율적)</li>
 *   <li>제목 행, 헤더 행, 데이터 행 자동 렌더링</li>
 *   <li>컬럼 너비 자동 조정 또는 수동 지정</li>
 *   <li>암호화 지원 (AES Agile 방식)</li>
 * </ul>
 *
 * @see SxssfExcelFile
 * @see SxssfMultiSheetExcelFile
 * @author eGovFrame
 */
public abstract class BaseSxssfExcelFile implements ExcelFile {

	// ==================== 상수 정의 ====================

	/** SXSSF 워크북의 메모리 윈도우 크기 (메모리에 유지할 행의 개수) */
	protected static final int ROW_ACCESS_WINDOW_SIZE = 10000;

	/** 엑셀 행 시작 인덱스 */
	protected static final int ROW_START_INDEX = 0;

	/** 엑셀 컬럼 시작 인덱스 */
	protected static final int COLUMN_START_INDEX = 0;

	/** 제목 폰트 크기 (포인트) */
	private static final short TITLE_FONT_SIZE = 22;

	/** 제목 행 높이 (포인트) */
	private static final float TITLE_ROW_HEIGHT = 33f;

	/** 자동 조정 시 컬럼 최소 너비 (POI 단위: 1/256 문자) */
	private static final int MIN_COLUMN_WIDTH = 3000;

	/** 자동 조정 시 컬럼 최대 너비 (POI 단위: 1/256 문자) */
	private static final int MAX_COLUMN_WIDTH = 15000;

	/** 자동 조정 시 너비 증가 비율 (한글 폰트 보정) */
	private static final double AUTO_SIZE_MULTIPLIER = 1.3;

	/** POI 컬럼 너비 단위 (1 문자 = 256 POI 단위) */
	private static final int POI_WIDTH_UNIT = 256;

	// ==================== 필드 ====================

	/** SXSSF 워크북 인스턴스 */
	protected SXSSFWorkbook workbook;

	/** 현재 작업 중인 시트 */
	protected Sheet sheet;

	/** 제목 행 오프셋 (제목이 있으면 1, 없으면 0) */
	protected int titleRowOffset = 0;

	// ==================== 생성자 ====================

	/**
	 * 기본 생성자
	 * <p>ROW_ACCESS_WINDOW_SIZE 크기의 SXSSF 워크북을 생성합니다.</p>
	 */
	public BaseSxssfExcelFile() {
		this.workbook = new SXSSFWorkbook(ROW_ACCESS_WINDOW_SIZE);
	}

	// ==================== 렌더링 메서드 ====================

	/**
	 * 엑셀 시트에 제목, 헤더, 데이터를 모두 렌더링합니다.
	 *
	 * <p>이 메서드는 엑셀 시트 생성의 전체 프로세스를 처리합니다:</p>
	 * <ol>
	 *   <li>제목 행 렌더링 (제목이 있는 경우)</li>
	 *   <li>헤더 행 렌더링</li>
	 *   <li>데이터 행 렌더링</li>
	 *   <li>컬럼 너비 자동 조정</li>
	 * </ol>
	 *
	 * @param data 엑셀 시트 데이터 (데이터 리스트, 타입, 제목 포함)
	 * @param metadata 엑셀 메타데이터 (헤더명, 컬럼 너비, 시트명 포함)
	 */
	protected void renderSheetContent(ExcelSheetData data, ExcelMetadata metadata) {
		// 1. 제목 행 렌더링 (있는 경우)
		if (data.getTitle() != null && !data.getTitle().trim().isEmpty()) {
			int columnCount = metadata.getDataFieldNames().size();
			renderTitleRow(metadata.getSheetName(), data.getTitle(), columnCount);
		}

		// 2. 헤더 행 렌더링
		renderHeaders(metadata);

		// 3. 데이터 행 렌더링
		renderDataLines(data, metadata);
	}

	/**
	 * 제목 행을 렌더링합니다.
	 *
	 * <p>첫 번째 행에 제목을 생성하고, 모든 컬럼에 걸쳐 셀을 병합합니다.
	 * 제목 스타일은 굵은 22포인트 폰트, 왼쪽 정렬, 33포인트 행 높이로 설정됩니다.</p>
	 *
	 * @param sheetName 시트명
	 * @param title 제목 텍스트
	 * @param columnCount 전체 컬럼 개수
	 */
	protected void renderTitleRow(String sheetName, String title, int columnCount) {
		// 시트가 없으면 생성
		if (sheet == null) {
			sheet = workbook.createSheet(sheetName);
		}

		// 제목 행 생성
		Row titleRow = sheet.createRow(ROW_START_INDEX);
		titleRow.setHeightInPoints(TITLE_ROW_HEIGHT);

		// 제목 스타일 생성
		CellStyle titleStyle = createTitleCellStyle();

		// 첫 번째 셀에 제목 입력
		createCell(titleRow, COLUMN_START_INDEX, title, titleStyle);

		// 모든 컬럼에 걸쳐 셀 병합
		if (columnCount > 1) {
			sheet.addMergedRegion(new CellRangeAddress(
				ROW_START_INDEX,        // 시작 행
				ROW_START_INDEX,        // 종료 행
				COLUMN_START_INDEX,     // 시작 컬럼
				columnCount - 1         // 종료 컬럼
			));
		}

		// 제목 행 오프셋 설정 (다음 행부터 헤더가 시작됨)
		titleRowOffset = 1;
	}

	/**
	 * 헤더 행을 렌더링합니다.
	 *
	 * <p>각 필드의 {@link ExcelColumn} 어노테이션에 정의된 헤더명을 사용하여 헤더 행을 생성합니다.
	 * 헤더는 굵은 폰트, 가운데 정렬로 표시됩니다.</p>
	 *
	 * <p>또한 SXSSF에서 autoSizeColumn을 사용하기 위해 각 컬럼을 추적 대상으로 등록합니다.</p>
	 *
	 * @param excelMetadata 엑셀 메타데이터 (헤더명, 필드명, 시트명 포함)
	 */
	protected void renderHeaders(ExcelMetadata excelMetadata) {
		// 시트가 없으면 생성 (제목이 없는 경우)
		if (sheet == null) {
			sheet = workbook.createSheet(excelMetadata.getSheetName());
		}

		// 헤더 행 생성 (제목이 있으면 row 1, 없으면 row 0)
		Row row = sheet.createRow(ROW_START_INDEX + titleRowOffset);

		// 헤더 스타일 생성 (굵게, 가운데 정렬)
		CellStyle headerStyle = createHeaderCellStyle();

		// 각 필드의 헤더명 렌더링
		int columnIndex = COLUMN_START_INDEX;
		for (String fieldName : excelMetadata.getDataFieldNames()) {
			createCell(row, columnIndex++, excelMetadata.getHeaderName(fieldName), headerStyle);
		}

		// SXSSF에서 autoSizeColumn 사용을 위한 컬럼 추적 등록
		trackColumnsForAutoSizing(excelMetadata.getDataFieldNames().size());
	}

	/**
	 * 데이터 행들을 렌더링합니다.
	 *
	 * <p>리플렉션을 사용하여 각 데이터 객체의 {@link ExcelColumn} 어노테이션이 붙은 필드 값을 읽어
	 * 엑셀 행으로 변환합니다.</p>
	 *
	 * <p>데이터 렌더링 후 각 컬럼의 너비를 조정합니다:
	 * <ul>
	 *   <li>headerWidth가 지정된 경우: 지정된 값 사용</li>
	 *   <li>headerWidth가 0인 경우: 자동 조정 (내용에 맞게 + 한글 보정)</li>
	 * </ul>
	 * </p>
	 *
	 * @param data 엑셀 시트 데이터 (데이터 리스트, 타입)
	 * @param metadata 엑셀 메타데이터 (헤더 너비 정보 포함)
	 * @throws RuntimeException 필드 접근 실패 시
	 */
	protected void renderDataLines(ExcelSheetData data, ExcelMetadata metadata) {
		CellStyle dataStyle = createCellStyle(workbook, false);

		// 숫자 서식 스타일 생성(정수/소수) - 기본 스타일을 복제하여 데이터 포맷만 부여
		CellStyle integerNumberStyle = workbook.createCellStyle();
		integerNumberStyle.cloneStyleFrom(dataStyle);
		short intDf = workbook.createDataFormat().getFormat("#,##0");
		integerNumberStyle.setDataFormat(intDf);

		CellStyle decimalNumberStyle = workbook.createCellStyle();
		decimalNumberStyle.cloneStyleFrom(dataStyle);
		short decDf = workbook.createDataFormat().getFormat("#,##0.##");
		decimalNumberStyle.setDataFormat(decDf);

		// 한글 중요 주석: 정렬 옵션 지원을 위해 기본/숫자 스타일에 대해 좌/중앙/우 정렬 변형을 미리 생성해 재사용한다.
		CellStyle baseLeft = workbook.createCellStyle();
		baseLeft.cloneStyleFrom(dataStyle);
		baseLeft.setAlignment(HorizontalAlignment.LEFT);
		CellStyle baseCenter = workbook.createCellStyle();
		baseCenter.cloneStyleFrom(dataStyle);
		baseCenter.setAlignment(HorizontalAlignment.CENTER);
		CellStyle baseRight = workbook.createCellStyle();
		baseRight.cloneStyleFrom(dataStyle);
		baseRight.setAlignment(HorizontalAlignment.RIGHT);

		CellStyle intLeft = workbook.createCellStyle();
		intLeft.cloneStyleFrom(integerNumberStyle);
		intLeft.setAlignment(HorizontalAlignment.LEFT);
		CellStyle intCenter = workbook.createCellStyle();
		intCenter.cloneStyleFrom(integerNumberStyle);
		intCenter.setAlignment(HorizontalAlignment.CENTER);
		CellStyle intRight = workbook.createCellStyle();
		intRight.cloneStyleFrom(integerNumberStyle);
		intRight.setAlignment(HorizontalAlignment.RIGHT);

		CellStyle decLeft = workbook.createCellStyle();
		decLeft.cloneStyleFrom(decimalNumberStyle);
		decLeft.setAlignment(HorizontalAlignment.LEFT);
		CellStyle decCenter = workbook.createCellStyle();
		decCenter.cloneStyleFrom(decimalNumberStyle);
		decCenter.setAlignment(HorizontalAlignment.CENTER);
		CellStyle decRight = workbook.createCellStyle();
		decRight.cloneStyleFrom(decimalNumberStyle);
		decRight.setAlignment(HorizontalAlignment.RIGHT);

		// 데이터 시작 행 (제목이 있으면 row 2, 없으면 row 1)
		int rowIndex = ROW_START_INDEX + titleRowOffset + 1;
		List<Field> fields = getAllFieldsWithExcelColumn(data.getType());

		// 각 데이터 객체를 행으로 변환
		for (Object record : data.getDataList()) {
			Row row = sheet.createRow(rowIndex++);
			renderDataRow(row, record, fields, dataStyle, integerNumberStyle, decimalNumberStyle,
				baseLeft, baseCenter, baseRight,
				intLeft, intCenter, intRight,
				decLeft, decCenter, decRight);
		}

		// 컬럼 너비 조정
		adjustColumnWidths(fields, metadata);
	}

	// ==================== private 헬퍼 메서드 ====================

	/**
	 * 제목 셀 스타일을 생성합니다.
	 *
	 * @return 제목용 CellStyle (굵은 22포인트 폰트, 왼쪽/중앙 정렬)
	 */
	private CellStyle createTitleCellStyle() {
		CellStyle titleStyle = workbook.createCellStyle();
		titleStyle.setAlignment(HorizontalAlignment.LEFT);
		titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);

		Font titleFont = workbook.createFont();
		titleFont.setFontHeightInPoints(TITLE_FONT_SIZE);
		titleFont.setBold(true);
		titleStyle.setFont(titleFont);

		// 중요 로직(한글): 제목 셀에도 실선(THIN) 보더를 적용하여 표의 일관성 유지
		titleStyle.setBorderTop(BorderStyle.THIN);
		titleStyle.setBorderBottom(BorderStyle.THIN);
		titleStyle.setBorderLeft(BorderStyle.THIN);
		titleStyle.setBorderRight(BorderStyle.THIN);

		// 중요 로직(한글): 제목 배경색 지정 - 요구사항에 따라 #be8e00 색상을 적용
		// SXSSF에서는 내부적으로 XSSFCellStyle을 사용하므로 캐스팅하여 사용자 색상 설정
		if (titleStyle instanceof org.apache.poi.xssf.usermodel.XSSFCellStyle) {
			org.apache.poi.xssf.usermodel.XSSFCellStyle xssf = (org.apache.poi.xssf.usermodel.XSSFCellStyle) titleStyle;
			xssf.setFillForegroundColor(new org.apache.poi.xssf.usermodel.XSSFColor(new java.awt.Color(0xBE, 0x8E, 0x00), null));
			xssf.setFillPattern(FillPatternType.SOLID_FOREGROUND);
		}

		return titleStyle;
	}

	/**
	 * 헤더 셀 스타일을 생성합니다.
	 *
	 * @return 헤더용 CellStyle (굵은 폰트, 가운데 정렬)
	 */
	private CellStyle createHeaderCellStyle() {
		CellStyle headerStyle = createCellStyle(workbook, true);
		headerStyle.setAlignment(HorizontalAlignment.CENTER);

		// 중요 로직(한글): 헤더 배경색 지정 - 요구사항에 따라 #fde598 색상을 적용
		if (headerStyle instanceof org.apache.poi.xssf.usermodel.XSSFCellStyle) {
			org.apache.poi.xssf.usermodel.XSSFCellStyle xssf = (org.apache.poi.xssf.usermodel.XSSFCellStyle) headerStyle;
			xssf.setFillForegroundColor(new org.apache.poi.xssf.usermodel.XSSFColor(new java.awt.Color(0xFD, 0xE5, 0x98), null));
			xssf.setFillPattern(FillPatternType.SOLID_FOREGROUND);
		}

		return headerStyle;
	}

	/**
	 * SXSSF autoSizeColumn 사용을 위해 컬럼들을 추적 대상으로 등록합니다.
	 *
	 * @param columnCount 추적할 컬럼 개수
	 */
	private void trackColumnsForAutoSizing(int columnCount) {
		for (int i = 0; i < columnCount; i++) {
			((org.apache.poi.xssf.streaming.SXSSFSheet) sheet)
				.trackColumnForAutoSizing(COLUMN_START_INDEX + i);
		}
	}

	/**
	 * 데이터 행의 각 셀을 렌더링합니다.
	 *
	 * @param row 렌더링할 행
	 * @param record 데이터 객체
	 * @param fields ExcelColumn 어노테이션이 있는 필드 리스트
	 * @param baseStyle 기본 데이터 셀 스타일(보더 등 공통)
	 * @param integerNumberStyle 정수형 숫자 서식 스타일(#,##0)
	 * @param decimalNumberStyle 소수형 숫자 서식 스타일(#,##0.##)
	 * @throws RuntimeException 필드 접근 실패 시
	 */
	private void renderDataRow(
			Row row,
			Object record,
			List<Field> fields,
			CellStyle baseStyle, CellStyle integerNumberStyle, CellStyle decimalNumberStyle,
			CellStyle baseLeft, CellStyle baseCenter, CellStyle baseRight,
			CellStyle intLeft, CellStyle intCenter, CellStyle intRight,
			CellStyle decLeft, CellStyle decCenter, CellStyle decRight) {
		int columnIndex = COLUMN_START_INDEX;
		try {
			for (Field field : fields) {
				field.setAccessible(true);

				Object value = field.get(record);

				// 수식 처리: ExcelColumn에 formula 설정이 있는 경우 수식을 생성하여 설정
				ExcelColumn excelColumn = field.getAnnotation(ExcelColumn.class);
				ExcelColumn.Align align = (excelColumn != null) ? excelColumn.align() : ExcelColumn.Align.AUTO;
				if (excelColumn != null && excelColumn.formula() && !excelColumn.formulaRefField().isEmpty()) {
					String refFieldName = excelColumn.formulaRefField();
					int refColumnIndex = findFieldColumnIndex(fields, refFieldName);
					if (refColumnIndex >= 0) {
						// 한글 중요 주석: 참조 대상(X열) 값이 null 또는 공백("")이면 수식을 적용하지 않고 빈 셀로 처리하여 컬럼 정렬 유지
						Field refField = fields.get(refColumnIndex);
						refField.setAccessible(true);
						Object refValue = refField.get(record);
						boolean isBlankRef = (refValue == null) || (refValue instanceof String && ((String) refValue).trim().isEmpty());
						CellStyle baseAligned = baseStyle;
						if (align == ExcelColumn.Align.LEFT) baseAligned = baseLeft;
						else if (align == ExcelColumn.Align.CENTER) baseAligned = baseCenter;
						else if (align == ExcelColumn.Align.RIGHT) baseAligned = baseRight;
						if (isBlankRef) {
							Cell cell = row.createCell(columnIndex++);
							cell.setCellValue("");
							cell.setCellStyle(baseAligned);
							continue; // 다음 필드 처리
						}

						String refExcelColumn = toExcelColumnName(refColumnIndex + 1); // 1-based for Excel column letters
						int excelRowNumber = row.getRowNum() + 1; // 1-based row number in Excel
						String formula = String.format(excelColumn.formulaPattern(), refExcelColumn, excelRowNumber);

						Cell cell = row.createCell(columnIndex++);
						cell.setCellFormula(formula);
						cell.setCellStyle(baseAligned);
						continue; // 다음 필드 처리
					}
				}

				// 기본 값 처리: 숫자 타입일 경우 천단위 콤마 서식 적용 + 정렬 옵션 반영
				boolean isIntNum = (value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long);
				boolean isDecNum = (value instanceof Float || value instanceof Double || value instanceof BigDecimal);
				CellStyle styleToUse = baseStyle;
				if (isIntNum) {
					styleToUse = integerNumberStyle;
				} else if (isDecNum) {
					styleToUse = decimalNumberStyle;
				}
				if (align != ExcelColumn.Align.AUTO) {
					if (isIntNum) {
						if (align == ExcelColumn.Align.LEFT) styleToUse = intLeft;
						else if (align == ExcelColumn.Align.CENTER) styleToUse = intCenter;
						else if (align == ExcelColumn.Align.RIGHT) styleToUse = intRight;
					} else if (isDecNum) {
						if (align == ExcelColumn.Align.LEFT) styleToUse = decLeft;
						else if (align == ExcelColumn.Align.CENTER) styleToUse = decCenter;
						else if (align == ExcelColumn.Align.RIGHT) styleToUse = decRight;
					} else {
						if (align == ExcelColumn.Align.LEFT) styleToUse = baseLeft;
						else if (align == ExcelColumn.Align.CENTER) styleToUse = baseCenter;
						else if (align == ExcelColumn.Align.RIGHT) styleToUse = baseRight;
					}
				}
				createCell(row, columnIndex++, value, styleToUse);
			}
		} catch (IllegalAccessException e) {
			throw new RuntimeException("데이터 필드 접근 중 오류가 발생했습니다.", e);
		}
	}

	/**
	 * 필드명으로 컬럼 인덱스를 찾습니다.
	 * 한글 중요 주석: ExcelColumn 필드 순서 기준으로 0부터 시작하는 인덱스를 반환.
	 */
	private int findFieldColumnIndex(List<Field> fields, String targetFieldName) {
		for (int i = 0; i < fields.size(); i++) {
			if (fields.get(i).getName().equals(targetFieldName)) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * 1부터 시작하는 컬럼 번호를 엑셀 컬럼명(A, B, ... AA, AB ...)으로 변환합니다.
	 * 한글 중요 주석: 엑셀 수식에서 참조할 컬럼 문자열을 생성하기 위한 유틸.
	 */
	private String toExcelColumnName(int columnNumber) {
		StringBuilder sb = new StringBuilder();
		int num = columnNumber;
		while (num > 0) {
			int rem = (num - 1) % 26;
			sb.insert(0, (char) ('A' + rem));
			num = (num - 1) / 26;
		}
		return sb.toString();
	}

	/**
	 * 각 컬럼의 너비를 조정합니다.
	 *
	 * <p>headerWidth가 지정된 경우 해당 값을 사용하고,
	 * 그렇지 않은 경우 자동 조정 후 한글 폰트 보정을 적용합니다.</p>
	 *
	 * @param fields ExcelColumn 어노테이션이 있는 필드 리스트
	 * @param metadata 엑셀 메타데이터 (헤더 너비 정보 포함)
	 */
	private void adjustColumnWidths(List<Field> fields, ExcelMetadata metadata) {
		for (int i = 0; i < fields.size(); i++) {
			int columnIndex = COLUMN_START_INDEX + i;
			Field field = fields.get(i);
			String fieldName = field.getName();

			int headerWidth = metadata.getHeaderWidth(fieldName);

			if (headerWidth > 0) {
				// headerWidth가 지정된 경우: 지정된 값 사용
				setColumnWidthInCharacters(columnIndex, headerWidth);
			} else {
				// headerWidth가 0인 경우: 자동 조정 + 한글 보정
				autoSizeColumnWithKoreanFontCorrection(columnIndex);
			}
		}
	}

	/**
	 * 컬럼 너비를 문자 단위로 설정합니다.
	 *
	 * @param columnIndex 컬럼 인덱스
	 * @param widthInCharacters 문자 단위 너비
	 */
	private void setColumnWidthInCharacters(int columnIndex, int widthInCharacters) {
		sheet.setColumnWidth(columnIndex, widthInCharacters * POI_WIDTH_UNIT);
	}

	/**
	 * 컬럼 너비를 자동 조정하고 한글 폰트 보정을 적용합니다.
	 *
	 * <p>POI의 autoSizeColumn은 한글 폰트 크기를 정확하게 계산하지 못하므로,
	 * 자동 조정 후 1.3배 증가시키고 최소/최대 범위를 적용합니다.</p>
	 *
	 * @param columnIndex 컬럼 인덱스
	 */
	private void autoSizeColumnWithKoreanFontCorrection(int columnIndex) {
		sheet.autoSizeColumn(columnIndex);

		int currentWidth = sheet.getColumnWidth(columnIndex);
		int adjustedWidth = (int) (currentWidth * AUTO_SIZE_MULTIPLIER);

		// 최소/최대 범위 적용
		int finalWidth = Math.max(MIN_COLUMN_WIDTH, Math.min(adjustedWidth, MAX_COLUMN_WIDTH));
		sheet.setColumnWidth(columnIndex, finalWidth);
	}

	// ==================== ExcelFile 인터페이스 구현 ====================

	/**
	 * 워크북을 출력 스트림에 씁니다.
	 *
	 * @param stream 출력 스트림
	 * @throws RuntimeException IO 오류 발생 시
	 */
	@Override
	public void write(OutputStream stream) {
		try {
			workbook.write(stream);
		} catch (IOException e) {
			throw new RuntimeException("엑셀 파일 쓰기 중 오류가 발생했습니다.", e);
		}
	}

	/**
	 * 워크북을 암호화하여 출력 스트림에 씁니다.
	 *
	 * <p>AES Agile 암호화 방식을 사용합니다.
	 * password가 null인 경우 암호화 없이 일반 파일로 작성됩니다.</p>
	 *
	 * @param stream 출력 스트림
	 * @param password 암호화 비밀번호 (null이면 암호화하지 않음)
	 * @throws RuntimeException IO 오류 또는 암호화 오류 발생 시
	 */
	@Override
	public void writeWithEncryption(OutputStream stream, String password) {
		try {
			if (password == null) {
				write(stream);
			} else {
				encryptAndWrite(stream, password);
			}
			workbook.close();
			stream.close();
		} catch (IOException e) {
			throw new RuntimeException("암호화된 엑셀 파일 쓰기 중 오류가 발생했습니다.", e);
		}
	}

	/**
	 * 워크북을 암호화하여 출력 스트림에 씁니다.
	 *
	 * @param stream 출력 스트림
	 * @param password 암호화 비밀번호
	 * @throws IOException IO 오류 발생 시
	 */
	private void encryptAndWrite(OutputStream stream, String password) throws IOException {
		POIFSFileSystem fileSystem = new POIFSFileSystem();
		OutputStream encryptorStream = getEncryptorStream(fileSystem, password);

		workbook.write(encryptorStream);
		encryptorStream.close();

		fileSystem.writeFilesystem(stream);
		fileSystem.close();
	}

	/**
	 * POIFSFileSystem에 대한 암호화 스트림을 생성합니다.
	 *
	 * @param fileSystem POI 파일 시스템
	 * @param password 암호화 비밀번호
	 * @return 암호화된 출력 스트림
	 * @throws RuntimeException 암호화 초기화 실패 시
	 */
	private OutputStream getEncryptorStream(POIFSFileSystem fileSystem, String password) {
		try {
			Encryptor encryptor = new EncryptionInfo(EncryptionMode.agile).getEncryptor();
			encryptor.confirmPassword(password);
			return encryptor.getDataStream(fileSystem);
		} catch (IOException | GeneralSecurityException e) {
			throw new RuntimeException("POIFSFileSystem에서 암호화 스트림 생성에 실패했습니다.", e);
		}
	}
}
