package incheon.ags.aip.util;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.streaming.SXSSFSheet;

public class ExcelResultHandler{

	private final SXSSFSheet sheet;
	private int rownum;

	private final Map<String, String> nCode;
	private final Map<String, String> qCode;
	private final Map<String, String> rCode;
	private final Map<String, String> tCode;

	public ExcelResultHandler(SXSSFSheet sheet, int rownum){
		this.sheet = sheet;
		this.rownum = rownum;
		this.nCode = null;
		this.qCode = null;
		this.rCode = null;
		this.tCode = null;
	}

	public ExcelResultHandler(SXSSFSheet sheet){
		this.sheet = sheet;
		this.nCode = null;
		this.qCode = null;
		this.rCode = null;
		this.tCode = null;
	}

	public ExcelResultHandler(SXSSFSheet sheet, int rownum,
			Map<String, String> nCode,
			Map<String, String> qCode,
			Map<String, String> rCode,
			Map<String, String> tCode){
		this.sheet = sheet;
		this.rownum = rownum;
		this.nCode = nCode;
		this.qCode = qCode;
		this.rCode = rCode;
		this.tCode = tCode;
	}

	public void writeHeader(String[] labels, CellStyle headerStyle){
		Row r = sheet.createRow(rownum++);
		for(int i = 0; i < labels.length; i++){
			Cell c = r.createCell(i);
			c.setCellValue(labels[i]);
			if(headerStyle != null) c.setCellStyle(headerStyle);
		}
	}

	public void writeRowsOrdered(List<LinkedHashMap<String, Object>> rows){
		for(LinkedHashMap<String, Object> row : rows){
			writeRow(row);
		}
	}

	public void writeRow(Map<String, Object> row){
		Row r = sheet.createRow(rownum++);
		int col = 0;
		for(Object v : row.values()){
			Cell c = r.createCell(col++);
			c.setCellValue(v == null ? "" : String.valueOf(v));
		}
	}

	/************************ 판독조서 유효성 검사 ************************/

	public List<ValidationException> validateColErrors(List<Map<String, Object>> rows, int headerRowIndex){
		List<ValidationException> errors = new ArrayList<>();

		for(int i = 0; i < rows.size(); i++){
			Map<String, Object> r = rows.get(i);
			int rowView = headerRowIndex + i;

			addIf(errors, checkAndCanonCore(r, "N", rowView, "조사여부 값이 유효하지 않습니다.", nCode, null));
			addIf(errors, checkAndCanonCore(r, "Q", rowView, "처리구분 값이 유효하지 않습니다.", qCode, null));
			addIf(errors, checkAndCanonCore(r, "R", rowView, "구조 값이 유효하지 않습니다.", rCode, null));
			addIf(errors, checkAndCanonCore(r, "T", rowView, "위반여부 값이 유효하지 않습니다.", tCode, null));

			addIf(errors, validateSurveyDate(r, rowView));

			String tVal = asStr(r.get("T"));
			boolean isYes = Objects.equals(tVal, tCode == null ? null : tCode.get("예"));

			if(isYes){
				addIf(errors, validateRequired(r, "U", rowView, "위반사유를 입력해주세요."));
				addIf(errors, validateNumber(r, "V", rowView, "위반면적을 입력해주세요.", "위반면적은 숫자여야 합니다."));
			}
		}

		return errors;
	}

	private static void addIf(List<ValidationException> errors, ValidationException ve){
		if(ve != null) errors.add(ve);
	}

	private static ValidationException validateSurveyDate(Map<String, Object> r, int rowView){
		String v = asStr(r.get("O"));
		if(isBlank(v)) return null;
		if(!isValidYyyyMmDd(v)) return new ValidationException(rowView, "O", "조사일자는 YYYY-MM-DD 형식이어야 합니다.", v);
		return null;
	}

	private static ValidationException validateRequired(Map<String, Object> r, String col, int rowView, String msg){
		String v = asStr(r.get(col));
		if(isBlank(v)) return new ValidationException(rowView, col, msg, v);
		return null;
	}

	private static ValidationException validateNumber(Map<String, Object> r, String col, int rowView, String emptyMsg, String invalidMsg){
		String v = asStr(r.get(col));
		if(isBlank(v)) return new ValidationException(rowView, col, emptyMsg, v);
		if(!v.matches("\\d+(\\.\\d+)?")) return new ValidationException(rowView, col, invalidMsg, v);
		return null;
	}

	private static final DateTimeFormatter YYYY_MM_DD = DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT);

	static boolean isValidYyyyMmDd(String s){
		if(s == null) return false;
		try{
			LocalDate.parse(s, YYYY_MM_DD);
			return true;
		}catch(DateTimeParseException e){
			return false;
		}
	}

	private static ValidationException checkAndCanonCore(Map<String, Object> r, String col, int row, String msg, Map<String, String> codeMap, Predicate<String> rule){
		String v = asStr(r.get(col));
		if(isBlank(v)) return new ValidationException(row, col, "필수값 누락", v);

		String vTrim = v;

		String[] tokens = Arrays.stream(vTrim.split("[,;/|]")).map(String::trim).filter(s -> !s.isEmpty()).toArray(String[]::new);

		if(tokens.length != 1 || !tokens[0].equals(vTrim)) return new ValidationException(row, col, "하나의 값만 입력하세요.", v);

		if(codeMap != null){
			String canon = codeMap.containsValue(vTrim) ? vTrim : codeMap.get(vTrim);
			if(canon == null) return new ValidationException(row, col, msg, v);
			r.put(col, canon);
			return null;
		}

		if(rule != null && !rule.test(vTrim)){
			return new ValidationException(row, col, msg, v);
		}

		return null;
	}

	private static String asStr(Object o){
		return o == null ? null : String.valueOf(o).trim();
	}

	private static boolean isBlank(String s){
		return s == null || s.isBlank();
	}

	public static class ValidationException extends RuntimeException{
		private final int rowIndex;
		private final String column;
		private final String rawValue;
		private final String userMessage;

		public ValidationException(int rowIndex, String column, String message, String rawValue){
			super(message);
			this.rowIndex = rowIndex;
			this.column = column;
			this.rawValue = rawValue;
			this.userMessage = message;
		}

		public int getRowIndex(){ return rowIndex; }
		public String getColumn(){ return column; }
		public String getRawValue(){ return rawValue; }
		public String getUserMessage(){ return userMessage; }
	}
}