package incheon.uis.uld.service.impl;

import incheon.com.security.vo.LoginVO;
import incheon.uis.uld.mapper.LayerSyncMapper;
import incheon.uis.uld.service.LayerSyncException;
import incheon.uis.uld.service.LayerSyncService;
import incheon.uis.ums.shp.ShpColumnMapping;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.egovframe.rte.fdl.security.userdetails.util.EgovUserDetailsHelper;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * 레이어 동기화 Service 구현체
 * INSERT/UPDATE/DELETE 로직:
 * - 순수 INSERT/UPDATE/DELETE 실행
 * - 에러 유효성 검사 (에러 발생 시 전체 rollback)
 * - 이력 테이블에 원본 데이터 직접 INSERT
 *
 * @author 호지원
 * @since 2025.01.30
 */
@Service("layerSyncService")
@RequiredArgsConstructor
@Slf4j
public class LayerSyncServiceImpl extends EgovAbstractServiceImpl implements LayerSyncService {

    private final LayerSyncMapper layerSyncMapper;

    @Override
    public Map<String, Object> insertSync(Map<String, Object> requestBody) {
        // 기본 파라미터 추출
        String schemaName = (String) requestBody.get("schemaName");
        String tableName = (String) requestBody.get("tableName");
        Integer srid = requestBody.get("srid") != null
                ? ((Number) requestBody.get("srid")).intValue()
                : 5186;
        Integer sourceSrid = parseSourceSrid(requestBody.get("sourceSrid"));

        @SuppressWarnings("unchecked")
        List<Map<String, Object>> features = (List<Map<String, Object>>) requestBody.get("features");

        // 기본 유효성 검사
        validateBasicParams(schemaName, tableName, features);

        // SHP 약자명 → 원본 컬럼명 역변환 (ShpColumnMapping 등록된 테이블만 적용)
        normalizePropertyKeys(tableName, features);

        // 테이블 컬럼 메타데이터 조회
        List<Map<String, Object>> columns = layerSyncMapper.selectTableColumns(schemaName, tableName);
        if (columns == null || columns.isEmpty()) {
            throw new LayerSyncException("테이블을 찾을 수 없습니다: " + schemaName + "." + tableName);
        }

        // DB에서 PK 컬럼 조회
        List<String> pkColumns = layerSyncMapper.selectPrimaryKeyColumns(schemaName, tableName);
        if (pkColumns == null || pkColumns.isEmpty()) {
            throw new LayerSyncException("PK 컬럼을 찾을 수 없습니다: " + schemaName + "." + tableName);
        }
        String pkColumn = String.join(",", pkColumns);

        log.info("[LayerSync] DB에서 조회한 PK 컬럼: {}", pkColumn);

        // 에러 목록
        List<String> errors = new ArrayList<>();

        // 에러A 체크: 코드성 PK 컬럼(ftc, ftr_cde) 값 필수 확인
        // ID성 PK 컬럼(idn, ftr_idn)은 DB 시퀀스로 자동 생성되므로 null 허용
        for (Map<String, Object> feature : features) {
            @SuppressWarnings("unchecked")
            Map<String, Object> props = (Map<String, Object>) feature.get("properties");
            if (props == null) {
                errors.add("속성 데이터가 없습니다.");
                break;
            }
            for (String pkCol : pkColumns) {
                // ID성 PK(idn, ftr_idn)는 null 허용
                if (isIdTypePk(pkCol)) {
                    continue;
                }
                // 코드성 PK(ftc, ftr_cde 등)는 필수
                Object pkValue = props.get(pkCol);
                if (pkValue == null || pkValue.toString().isBlank()) {
                    errors.add(pkCol + " 값이 없는 데이터가 있습니다.");
                    break;
                }
            }
            if (!errors.isEmpty()) break;
        }

        // 에러가 있으면 예외 발생 (전체 rollback)
        if (!errors.isEmpty()) {
            throw new LayerSyncException(errors);
        }

        // 기본 파라미터 구성
        Map<String, Object> param = buildBaseParam(schemaName, tableName, pkColumn, srid, sourceSrid,
                columns, features, getSessionUserId());

        // 참고: PK 중복은 DB에서 DuplicateKeyException으로 처리 (idn이 null인 경우 시퀀스 자동 생성)

        log.info("[LayerSync] INSERT 시작: schema={}, table={}, pk={}, sourceSrid={}, features={}건",
                schemaName, tableName, pkColumn, sourceSrid, features.size());

        int insertedCount = 0;
        int historyCount = 0;

        // INSERT 실행 (RETURNING gid로 생성된 gid를 받아서 이력 테이블에 INSERT)
        for (Map<String, Object> feature : features) {
            Map<String, Object> insertParam = new HashMap<>(param);
            insertParam.put("feature", feature);

            try {
                // INSERT 후 생성된 gid 반환
                Long gid = layerSyncMapper.insertNewFeatureReturning(insertParam);
                insertedCount++;

                // 이력 INSERT (stts_cd = 'C') - gid로 DB에서 SELECT하여 저장
                if (gid != null) {
                    insertParam.put("sttsCd", "C");
                    insertParam.put("gid", gid);
                    try {
                        int histCnt = layerSyncMapper.insertHistoryByGid(insertParam);
                        historyCount += histCnt;
                    } catch (Exception e) {
                        log.warn("[LayerSync] 신규 이력 INSERT 실패: {}", e.getMessage());
                    }
                }
            } catch (DuplicateKeyException e) {
                log.error("[LayerSync] PK 중복 오류: {}", e.getMessage());
                throw new LayerSyncException("pk값이 중복입니다.");
            } catch (DataIntegrityViolationException e) {
                log.error("[LayerSync] 데이터 무결성 오류: {}", e.getMessage());
                throw new LayerSyncException("데이터 무결성 오류가 발생했습니다.");
            }
        }

        log.info("[LayerSync] INSERT 완료: {}건, 이력: {}건", insertedCount, historyCount);

        Map<String, Object> result = new HashMap<>();
        result.put("insertedCount", insertedCount);
        result.put("historyCount", historyCount);
        result.put("totalCount", features.size());
        return result;
    }

    @Override
    public Map<String, Object> updateSync(Map<String, Object> requestBody) {
        // 기본 파라미터 추출
        String schemaName = (String) requestBody.get("schemaName");
        String tableName = (String) requestBody.get("tableName");
        Integer srid = requestBody.get("srid") != null
                ? ((Number) requestBody.get("srid")).intValue()
                : 5186;
        Integer sourceSrid = parseSourceSrid(requestBody.get("sourceSrid"));

        @SuppressWarnings("unchecked")
        List<Map<String, Object>> features = (List<Map<String, Object>>) requestBody.get("features");

        // 기본 유효성 검사
        validateBasicParams(schemaName, tableName, features);

        // SHP 약자명 → 원본 컬럼명 역변환 (ShpColumnMapping 등록된 테이블만 적용)
        normalizePropertyKeys(tableName, features);

        // 테이블 컬럼 메타데이터 조회
        List<Map<String, Object>> columns = layerSyncMapper.selectTableColumns(schemaName, tableName);
        if (columns == null || columns.isEmpty()) {
            throw new LayerSyncException("테이블을 찾을 수 없습니다: " + schemaName + "." + tableName);
        }

        // DB에서 PK 컬럼 조회
        List<String> pkColumns = layerSyncMapper.selectPrimaryKeyColumns(schemaName, tableName);
        if (pkColumns == null || pkColumns.isEmpty()) {
            throw new LayerSyncException("PK 컬럼을 찾을 수 없습니다: " + schemaName + "." + tableName);
        }
        String pkColumn = String.join(",", pkColumns);

        log.info("[LayerSync] DB에서 조회한 PK 컬럼: {}", pkColumn);

        // 에러 목록
        List<String> errors = new ArrayList<>();

        // 에러C 체크: 모든 PK 컬럼 값 존재 확인
        for (Map<String, Object> feature : features) {
            @SuppressWarnings("unchecked")
            Map<String, Object> props = (Map<String, Object>) feature.get("properties");
            if (props == null) {
                errors.add("pk에 값이 없습니다.");
                break;
            }
            for (String pkCol : pkColumns) {
                Object pkValue = props.get(pkCol);
                if (pkValue == null || pkValue.toString().isBlank()) {
                    errors.add("pk에 값이 없습니다.");
                    break;
                }
            }
            if (!errors.isEmpty()) break;
        }

        // 기본 파라미터 구성
        Map<String, Object> param = buildBaseParam(schemaName, tableName, pkColumn, srid, sourceSrid,
                columns, features, getSessionUserId());

        // 에러D 체크: 수정할 데이터 존재 확인
        if (errors.isEmpty()) {
            List<Map<String, Object>> existingData = layerSyncMapper.selectExistingByPk(param);

            // PK별 use_yn 맵 구성
            Set<String> existingPks = new HashSet<>();
            if (existingData != null) {
                for (Map<String, Object> row : existingData) {
                    String pkVal = row.get("pk_value") != null ? row.get("pk_value").toString() : "";
                    String useYn = row.get("use_yn") != null ? row.get("use_yn").toString() : "Y";
                    if ("Y".equals(useYn)) {
                        existingPks.add(pkVal);
                    }
                }
            }

            // 각 feature의 PK가 DB에 존재하는지 확인
            for (Map<String, Object> feature : features) {
                @SuppressWarnings("unchecked")
                Map<String, Object> props = (Map<String, Object>) feature.get("properties");
                if (props == null) continue;

                String pkValue = buildPkValue(pkColumns, props);
                if (!existingPks.contains(pkValue)) {
                    errors.add("수정할 데이터가 없습니다.");
                    break;
                }
            }
        }

        // 에러가 있으면 예외 발생 (전체 rollback)
        if (!errors.isEmpty()) {
            throw new LayerSyncException(errors);
        }

        log.info("[LayerSync] UPDATE 시작: schema={}, table={}, pk={}, sourceSrid={}, features={}건",
                schemaName, tableName, pkColumn, sourceSrid, features.size());

        // 1. 이력 INSERT (stts_cd = 'U') - UPDATE 전에 기존 DB 데이터를 이력에 저장
        param.put("sttsCd", "U");
        int historyCount = 0;
        try {
            historyCount = layerSyncMapper.insertHistory(param);
            log.info("[LayerSync] UPDATE 이력 INSERT 완료 (변경 전 데이터): {}건", historyCount);
        } catch (Exception e) {
            log.warn("[LayerSync] UPDATE 이력 INSERT 실패: {}", e.getMessage());
        }

        // 2. UPDATE 실행
        int updatedCount;
        try {
            updatedCount = layerSyncMapper.batchUpdateFeatures(param);
            log.info("[LayerSync] UPDATE 완료: {}건", updatedCount);
        } catch (DataIntegrityViolationException e) {
            log.error("[LayerSync] 데이터 무결성 오류: {}", e.getMessage());
            throw new LayerSyncException("데이터 무결성 오류가 발생했습니다.");
        }

        Map<String, Object> result = new HashMap<>();
        result.put("updatedCount", updatedCount);
        result.put("historyCount", historyCount);
        result.put("totalCount", features.size());
        return result;
    }

    @Override
    public Map<String, Object> deleteSync(Map<String, Object> requestBody) {
        // 기본 파라미터 추출
        String schemaName = (String) requestBody.get("schemaName");
        String tableName = (String) requestBody.get("tableName");

        @SuppressWarnings("unchecked")
        List<Map<String, Object>> features = (List<Map<String, Object>>) requestBody.get("features");

        // 기본 유효성 검사
        validateBasicParams(schemaName, tableName, features);

        // 테이블 컬럼 메타데이터 조회
        List<Map<String, Object>> columns = layerSyncMapper.selectTableColumns(schemaName, tableName);
        if (columns == null || columns.isEmpty()) {
            throw new LayerSyncException("테이블을 찾을 수 없습니다: " + schemaName + "." + tableName);
        }

        // DB에서 PK 컬럼 조회
        List<String> pkColumns = layerSyncMapper.selectPrimaryKeyColumns(schemaName, tableName);
        if (pkColumns == null || pkColumns.isEmpty()) {
            throw new LayerSyncException("PK 컬럼을 찾을 수 없습니다: " + schemaName + "." + tableName);
        }
        String pkColumn = String.join(",", pkColumns);

        log.info("[LayerSync] DB에서 조회한 PK 컬럼: {}", pkColumn);

        // 에러 목록
        List<String> errors = new ArrayList<>();

        // 에러E 체크: 모든 PK 컬럼 값 존재 확인
        for (Map<String, Object> feature : features) {
            @SuppressWarnings("unchecked")
            Map<String, Object> props = (Map<String, Object>) feature.get("properties");
            if (props == null) {
                errors.add("pk값이 없습니다.");
                break;
            }
            for (String pkCol : pkColumns) {
                Object pkValue = props.get(pkCol);
                if (pkValue == null || pkValue.toString().isBlank()) {
                    errors.add("pk값이 없습니다.");
                    break;
                }
            }
            if (!errors.isEmpty()) break;
        }

        // 기본 파라미터 구성
        Map<String, Object> param = buildBaseParam(schemaName, tableName, pkColumn, null, null,
                columns, features, getSessionUserId());

        // 에러F 체크: 삭제할 데이터 존재 확인
        if (errors.isEmpty()) {
            List<Map<String, Object>> existingData = layerSyncMapper.selectExistingByPk(param);

            // PK별 use_yn 맵 구성
            Set<String> existingPks = new HashSet<>();
            if (existingData != null) {
                for (Map<String, Object> row : existingData) {
                    String pkVal = row.get("pk_value") != null ? row.get("pk_value").toString() : "";
                    String useYn = row.get("use_yn") != null ? row.get("use_yn").toString() : "Y";
                    if ("Y".equals(useYn)) {
                        existingPks.add(pkVal);
                    }
                }
            }

            // 각 feature의 PK가 DB에 존재하는지 확인
            for (Map<String, Object> feature : features) {
                @SuppressWarnings("unchecked")
                Map<String, Object> props = (Map<String, Object>) feature.get("properties");
                if (props == null) continue;

                String pkValue = buildPkValue(pkColumns, props);
                if (!existingPks.contains(pkValue)) {
                    errors.add("삭제할 데이터가 없습니다.");
                    break;
                }
            }
        }

        // 에러가 있으면 예외 발생 (전체 rollback)
        if (!errors.isEmpty()) {
            throw new LayerSyncException(errors);
        }

        log.info("[LayerSync] DELETE 시작: schema={}, table={}, pk={}, features={}건",
                schemaName, tableName, pkColumn, features.size());

        // 1. 이력 INSERT (stts_cd = 'D') - 삭제 전에 기존 DB 데이터를 이력에 저장
        param.put("sttsCd", "D");
        int historyCount = 0;
        try {
            historyCount = layerSyncMapper.insertHistory(param);
            log.info("[LayerSync] DELETE 이력 INSERT 완료 (삭제 전 데이터): {}건", historyCount);
        } catch (Exception e) {
            log.warn("[LayerSync] DELETE 이력 INSERT 실패: {}", e.getMessage());
        }

        // 2. Soft Delete 실행 (use_yn = 'N')
        int deletedCount;
        try {
            deletedCount = layerSyncMapper.softDeleteFeatures(param);
            log.info("[LayerSync] Soft DELETE 완료: {}건", deletedCount);
        } catch (DataIntegrityViolationException e) {
            log.error("[LayerSync] 데이터 무결성 오류: {}", e.getMessage());
            throw new LayerSyncException("데이터 무결성 오류가 발생했습니다.");
        }

        Map<String, Object> result = new HashMap<>();
        result.put("deletedCount", deletedCount);
        result.put("historyCount", historyCount);
        result.put("totalCount", features.size());
        return result;
    }

    // ======== Helper Methods ========

    private void validateBasicParams(String schemaName, String tableName,
                                     List<Map<String, Object>> features) {
        List<String> errors = new ArrayList<>();

        if (schemaName == null || schemaName.isBlank()) {
            errors.add("schemaName은 필수입니다.");
        }
        if (tableName == null || tableName.isBlank()) {
            errors.add("tableName은 필수입니다.");
        }
        if (features == null || features.isEmpty()) {
            errors.add("features가 비어있습니다.");
        }

        if (!errors.isEmpty()) {
            throw new LayerSyncException(errors);
        }
    }

    private Integer parseSourceSrid(Object sourceSridObj) {
        Integer sourceSrid = 5186;
        if (sourceSridObj != null) {
            if (sourceSridObj instanceof Number) {
                sourceSrid = ((Number) sourceSridObj).intValue();
            } else if (sourceSridObj instanceof String && !((String) sourceSridObj).isBlank()) {
                try {
                    sourceSrid = Integer.parseInt((String) sourceSridObj);
                } catch (NumberFormatException ignored) {}
            }
        }
        return sourceSrid;
    }

    private List<String> parsePkColumns(String pkColumn) {
        return Arrays.stream(pkColumn.split(","))
                .map(String::trim)
                .filter(s -> !s.isEmpty())
                .toList();
    }

    private String buildPkValue(List<String> pkColumns, Map<String, Object> props) {
        StringBuilder pkValueBuilder = new StringBuilder();
        for (int i = 0; i < pkColumns.size(); i++) {
            if (i > 0) pkValueBuilder.append(",");
            Object val = props.get(pkColumns.get(i));
            pkValueBuilder.append(val != null ? val.toString() : "");
        }
        return pkValueBuilder.toString();
    }

    private Map<String, Object> buildBaseParam(String schemaName, String tableName, String pkColumn,
                                               Integer srid, Integer sourceSrid,
                                               List<Map<String, Object>> columns,
                                               List<Map<String, Object>> features,
                                               String sessionUserId) {
        Map<String, Object> param = new HashMap<>();
        param.put("schemaName", schemaName);
        param.put("tableName", tableName);
        param.put("pkColumn", pkColumn);
        if (srid != null) param.put("srid", srid);
        if (sourceSrid != null) param.put("sourceSrid", sourceSrid);
        param.put("columns", columns);
        param.put("features", features);
        param.put("sessionUserId", sessionUserId);
        return param;
    }

    private String getSessionUserId() {
        String sessionUserId = "SYSTEM";
        LoginVO loginVO = (LoginVO) EgovUserDetailsHelper.getAuthenticatedUser();
        if (loginVO != null && loginVO.getUserId() != null) {
            sessionUserId = loginVO.getUserId();
        }
        return sessionUserId;
    }

    /**
     * SHP 약자명 → 원본 컬럼명 역변환
     * ShpColumnMapping에 등록된 테이블/컬럼만 변환, 나머지는 그대로 유지
     */
    private void normalizePropertyKeys(String tableName, List<Map<String, Object>> features) {
        Map<String, String> shpToFullMap = ShpColumnMapping.getShpToFullMap(tableName);
        if (shpToFullMap.isEmpty()) return;

        for (Map<String, Object> feature : features) {
            @SuppressWarnings("unchecked")
            Map<String, Object> props = (Map<String, Object>) feature.get("properties");
            if (props == null) continue;

            Map<String, Object> normalized = new LinkedHashMap<>();
            for (Map.Entry<String, Object> e : props.entrySet()) {
                String fullName = shpToFullMap.getOrDefault(e.getKey(), e.getKey());
                normalized.put(fullName, e.getValue());
            }
            feature.put("properties", normalized);
        }
    }

    /**
     * ID성 PK 컬럼인지 확인 (idn, ftr_idn)
     * ID성 PK는 DB 시퀀스로 자동 생성되므로 INSERT 시 null 허용
     */
    private boolean isIdTypePk(String pkColumn) {
        if (pkColumn == null) return false;
        String lowerPk = pkColumn.toLowerCase();
        return "idn".equals(lowerPk) || "ftr_idn".equals(lowerPk);
    }

    /**
     * 코드성 PK 컬럼인지 확인 (ftc, ftr_cde)
     * 코드성 PK는 INSERT 시 필수
     */
    private boolean isCodeTypePk(String pkColumn) {
        if (pkColumn == null) return false;
        String lowerPk = pkColumn.toLowerCase();
        return "ftc".equals(lowerPk) || "ftr_cde".equals(lowerPk);
    }
}
