package incheon.product.geoview2d.layer.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import incheon.com.cmm.exception.BusinessException;
import incheon.com.cmm.exception.EntityNotFoundException;
import incheon.product.common.config.GeoViewProperties;
import incheon.product.geoview2d.layer.domain.LayerType;
import incheon.product.geoview2d.layer.mapper.LayerMapper;
import incheon.product.geoview2d.layer.service.LayerEditService;
import incheon.product.geoview2d.layer.vo.FeatureVO;
import incheon.product.geoview2d.layer.vo.LayerEditRequestVO;
import incheon.product.geoview2d.layer.vo.LayerEditResultVO;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
 * 레이어 편집 서비스 구현체.
 * Added/Modified/Deleted 피처를 청크 배치로 처리하고,
 * 이력 테이블({table}_h)에 변경이력을 기록한다.
 */
@Slf4j
@Service("productLayerEditService")
public class LayerEditServiceImpl extends EgovAbstractServiceImpl implements LayerEditService {

    @Resource(name = "productLayerMapper")
    private LayerMapper layerMapper;

    @Resource(name = "geoViewProperties")
    private GeoViewProperties geoViewProperties;

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    @Transactional
    public LayerEditResultVO editLayer(LayerEditRequestVO request, String currentUserId) {
        log.debug("레이어 편집 시작 - layerId: {}, userId: {}", request.getLayerId(), currentUserId);

        Map<String, Object> layerMeta = resolveLayerMetadata(request.getLayerId(), request.getLayerType());
        if (layerMeta == null) {
            throw new EntityNotFoundException("레이어를 찾을 수 없습니다: " + request.getLayerId());
        }

        String lyrPhysNm = (String) layerMeta.get("lyr_phys_nm");
        if (lyrPhysNm == null || !lyrPhysNm.contains(".")) {
            throw new BusinessException("레이어 물리 명은 '스키마.테이블' 형식이어야 합니다: " + lyrPhysNm, HttpStatus.BAD_REQUEST);
        }

        String[] parts = lyrPhysNm.split("\\.");
        String schemaName = parts[0];
        String tableName = parts[1];
        Integer srid = (Integer) layerMeta.get("cntm");

        int chunkSize = geoViewProperties.getLayer().getBatchChunkSize();

        LayerEditResultVO result = new LayerEditResultVO();
        result.setLayerId(request.getLayerId());

        // 1. Added
        List<FeatureVO> addedFeatures = request.getChanges().getAdded();
        if (addedFeatures != null && !addedFeatures.isEmpty()) {
            int count = processAddedFeatures(addedFeatures, schemaName, tableName, srid, chunkSize, currentUserId);
            result.addAddedCount(count);
        }

        // 2. Modified
        List<FeatureVO> modifiedFeatures = request.getChanges().getModified();
        if (modifiedFeatures != null && !modifiedFeatures.isEmpty()) {
            int count = processModifiedFeatures(modifiedFeatures, schemaName, tableName, srid, chunkSize, currentUserId);
            result.addModifiedCount(count);
        }

        // 3. Deleted
        List<Integer> deletedIds = request.getChanges().getDeleted();
        if (deletedIds != null && !deletedIds.isEmpty()) {
            int count = processDeletedFeatures(deletedIds, schemaName, tableName, chunkSize);
            result.addDeletedCount(count);
        }

        log.debug("레이어 편집 완료 - Added: {}, Modified: {}, Deleted: {}",
                result.getProcessed().getAdded(), result.getProcessed().getModified(), result.getProcessed().getDeleted());
        return result;
    }

    private Map<String, Object> resolveLayerMetadata(String layerId, LayerType layerType) {
        if (LayerType.TASK.equals(layerType)) {
            return layerMapper.selectTaskLayerMetadata(layerId);
        } else if (LayerType.USER.equals(layerType)) {
            return layerMapper.selectUserLayerMetadata(layerId);
        }
        return null;
    }

    private int processAddedFeatures(List<FeatureVO> features, String schemaName, String tableName, Integer srid, int chunkSize, String currentUserId) {
        int totalProcessed = 0;
        List<Map<String, Object>> tableColumns = layerMapper.selectTableColumns(schemaName, tableName);

        for (int i = 0; i < features.size(); i += chunkSize) {
            int endIndex = Math.min(i + chunkSize, features.size());
            List<FeatureVO> chunk = features.subList(i, endIndex);
            List<Map<String, Object>> mappedFeatures = convertForInsert(chunk);

            Map<String, Object> params = new HashMap<>();
            params.put("schemaName", schemaName);
            params.put("tableName", tableName);
            params.put("features", mappedFeatures);
            params.put("srid", srid);
            params.put("columns", tableColumns);
            params.put("currentUserId", currentUserId);

            List<Map<String, Object>> insertedRows = layerMapper.batchInsertLayerFeatures(params);
            int insertedCount = insertedRows != null ? insertedRows.size() : 0;
            totalProcessed += insertedCount;

            // RETURNING * 결과를 features에 반영
            if (insertedRows != null && !insertedRows.isEmpty()) {
                List<Map<String, Object>> columnsWithGid = new ArrayList<>(tableColumns);
                columnsWithGid.add(Map.of("column_name", "gid", "data_type", "bigint", "is_nullable", "NO"));

                for (int rowIdx = 0; rowIdx < insertedRows.size() && rowIdx < mappedFeatures.size(); rowIdx++) {
                    Map<String, Object> insertedRow = insertedRows.get(rowIdx);
                    @SuppressWarnings("unchecked")
                    Map<String, Object> properties = (Map<String, Object>) mappedFeatures.get(rowIdx).get("properties");
                    for (Map.Entry<String, Object> entry : insertedRow.entrySet()) {
                        if (!"geom".equals(entry.getKey()) && entry.getValue() != null) {
                            properties.put(entry.getKey(), entry.getValue());
                        }
                    }
                }
                params.put("columns", columnsWithGid);
            }

            insertHistoryAdded(schemaName, tableName, params);
        }
        return totalProcessed;
    }

    private int processModifiedFeatures(List<FeatureVO> features, String schemaName, String tableName, Integer srid, int chunkSize, String currentUserId) {
        int totalProcessed = 0;
        List<Map<String, Object>> tableColumns = layerMapper.selectTableColumns(schemaName, tableName);

        for (int i = 0; i < features.size(); i += chunkSize) {
            int endIndex = Math.min(i + chunkSize, features.size());
            List<FeatureVO> chunk = features.subList(i, endIndex);
            List<Map<String, Object>> mappedFeatures = convertForUpdate(chunk);

            Map<String, Object> params = new HashMap<>();
            params.put("schemaName", schemaName);
            params.put("tableName", tableName);
            params.put("features", mappedFeatures);
            params.put("srid", srid);
            params.put("columns", tableColumns);
            params.put("currentUserId", currentUserId);

            int updatedCount = layerMapper.batchUpdateLayerFeatures(params);
            totalProcessed += updatedCount;
            insertHistoryModified(schemaName, tableName, params);
        }
        return totalProcessed;
    }

    private int processDeletedFeatures(List<Integer> gids, String schemaName, String tableName, int chunkSize) {
        int totalProcessed = 0;

        for (int i = 0; i < gids.size(); i += chunkSize) {
            int endIndex = Math.min(i + chunkSize, gids.size());
            List<Integer> chunk = gids.subList(i, endIndex);

            Map<String, Object> params = new HashMap<>();
            params.put("schemaName", schemaName);
            params.put("tableName", tableName);
            params.put("gids", chunk);

            int deletedCount = layerMapper.deleteLayerFeatures(params);
            totalProcessed += deletedCount;
            insertHistoryDeleted(schemaName, tableName, params);
        }
        return totalProcessed;
    }

    private List<Map<String, Object>> convertForInsert(List<FeatureVO> features) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (FeatureVO feature : features) {
            Map<String, Object> mapped = new HashMap<>();
            mapped.put("geometryString", serializeGeometry(feature.getGeometry()));
            mapped.put("properties", feature.getProperties());
            result.add(mapped);
        }
        return result;
    }

    private List<Map<String, Object>> convertForUpdate(List<FeatureVO> features) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (FeatureVO feature : features) {
            Map<String, Object> mapped = new HashMap<>();
            mapped.put("gid", feature.getId());

            Object geometry = null;
            Map<String, Object> properties = null;
            if (feature.getModified() != null) {
                geometry = feature.getModified().getGeometry();
                properties = feature.getModified().getProperties();
            }
            if (geometry == null) geometry = feature.getGeometry();
            if (properties == null) properties = feature.getProperties();

            mapped.put("geometryString", serializeGeometry(geometry));
            mapped.put("properties", properties);
            result.add(mapped);
        }
        return result;
    }

    private String serializeGeometry(Object geometry) {
        try {
            return objectMapper.writeValueAsString(geometry);
        } catch (JsonProcessingException e) {
            throw new BusinessException("지오메트리 변환 실패", e);
        }
    }

    private void insertHistoryAdded(String schemaName, String tableName, Map<String, Object> params) {
        try {
            String historyTable = tableName + "_h";
            if (existsHistoryTable(schemaName, historyTable)) {
                Map<String, Object> historyParams = new HashMap<>(params);
                historyParams.put("tableName", historyTable);
                layerMapper.batchInsertHistoryAdded(historyParams);
            }
        } catch (DataAccessException e) {
            log.warn("Added 이력 저장 실패 (무시) - table: {}.{}_h", schemaName, tableName, e);
        }
    }

    private void insertHistoryModified(String schemaName, String tableName, Map<String, Object> params) {
        try {
            String historyTable = tableName + "_h";
            if (existsHistoryTable(schemaName, historyTable)) {
                Map<String, Object> historyParams = new HashMap<>(params);
                historyParams.put("tableName", historyTable);
                layerMapper.batchInsertHistoryModified(historyParams);
            }
        } catch (DataAccessException e) {
            log.warn("Modified 이력 저장 실패 (무시) - table: {}.{}_h", schemaName, tableName, e);
        }
    }

    private void insertHistoryDeleted(String schemaName, String tableName, Map<String, Object> params) {
        try {
            String historyTable = tableName + "_h";
            if (existsHistoryTable(schemaName, historyTable)) {
                Map<String, Object> historyParams = new HashMap<>();
                historyParams.put("schemaName", schemaName);
                historyParams.put("tableName", historyTable);
                historyParams.put("gids", params.get("gids"));
                layerMapper.batchInsertHistoryDeleted(historyParams);
            }
        } catch (DataAccessException e) {
            log.warn("Deleted 이력 저장 실패 (무시) - table: {}.{}_h", schemaName, tableName, e);
        }
    }

    private boolean existsHistoryTable(String schemaName, String tableName) {
        try {
            Boolean exists = layerMapper.checkHistoryTableExists(schemaName, tableName);
            return exists != null && exists;
        } catch (DataAccessException e) {
            log.warn("이력 테이블 존재 확인 실패 - schema: {}, table: {}", schemaName, tableName, e);
            return false;
        }
    }
}
