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

import incheon.com.cmm.exception.BusinessException;
import incheon.com.cmm.exception.EntityNotFoundException;
import incheon.product.common.config.GeoViewProperties;
import incheon.product.common.geo.CoordinateConverter;
import incheon.product.geoview2d.flight.mapper.FlightPhotoMapper;
import incheon.product.geoview2d.flight.service.FlightPhotoService;
import incheon.product.geoview2d.flight.vo.FlightPhotoDownloadRequestVO;
import incheon.product.geoview2d.flight.vo.FlightPhotoLayerVO;
import incheon.product.geoview2d.flight.vo.FlightPhotoSearchVO;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.geotools.api.referencing.FactoryException;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.api.referencing.crs.ProjectedCRS;
import org.geotools.api.referencing.operation.TransformException;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.MapContent;
import org.geotools.ows.ServiceException;
import org.geotools.ows.wmts.WebMapTileServer;
import org.geotools.ows.wmts.map.WMTSMapLayer;
import org.geotools.ows.wmts.model.WMTSLayer;
import org.geotools.referencing.CRS;
import org.geotools.renderer.lite.StreamingRenderer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 항공 사진 레이어 서비스 구현체.
 * WMTS를 통한 항공 사진 렌더링 및 ZIP 다운로드를 제공한다.
 */
@Slf4j
@Service("productFlightPhotoService")
public class FlightPhotoServiceImpl extends EgovAbstractServiceImpl implements FlightPhotoService {

    @Resource(name = "productFlightPhotoMapper")
    private FlightPhotoMapper flightPhotoMapper;

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

    @Override
    public List<FlightPhotoLayerVO> getFlightPhotoLayerList(FlightPhotoSearchVO searchVO, Boolean hasPermission) {
        return flightPhotoMapper.selectFlightPhotoLayerList(searchVO, hasPermission);
    }

    @Override
    public long getFlightPhotoLayerListTotCnt(FlightPhotoSearchVO searchVO, Boolean hasPermission) {
        return flightPhotoMapper.selectFlightPhotoLayerListTotCnt(searchVO, hasPermission);
    }

    @Override
    public FlightPhotoLayerVO getFlightPhotoLayerById(int flightPhotoLyrId) {
        return flightPhotoMapper.selectFlightPhotoLayerById(flightPhotoLyrId);
    }

    @Override
    public void validateDownloadRequest(FlightPhotoDownloadRequestVO request) {
        int maxSize = geoViewProperties.getLayer().getImageMaxSize();
        int defaultSize = geoViewProperties.getLayer().getImageDefaultSize();

        double bboxW = request.getMaxx() - request.getMinx();
        double bboxH = request.getMaxy() - request.getMiny();
        if (bboxW <= 0 || bboxH <= 0) {
            throw new BusinessException("잘못된 범위: min/max 값이 유효하지 않습니다.", HttpStatus.BAD_REQUEST);
        }

        double ratio = bboxW / bboxH;
        int width = resolveWidth(request.getWidth(), request.getHeight(), ratio, defaultSize);
        int height = resolveHeight(request.getWidth(), request.getHeight(), ratio, defaultSize);

        if (width < 256 || height < 256) {
            throw new BusinessException("width/height 값이 유효하지 않습니다.", HttpStatus.BAD_REQUEST);
        }
        if (width > maxSize || height > maxSize) {
            throw new BusinessException("width/height 값은 " + maxSize + "을 넘을 수 없습니다.", HttpStatus.BAD_REQUEST);
        }

        List<FlightPhotoLayerVO> layers = flightPhotoMapper.selectFlightPhotoLayerInIds(request.getLayerIds());
        Set<Integer> foundIds = layers.stream().map(FlightPhotoLayerVO::getFlightPhotoLyrId).collect(Collectors.toSet());
        for (Integer id : request.getLayerIds()) {
            if (!foundIds.contains(id)) {
                throw new EntityNotFoundException("존재하지 않는 항공 사진 레이어 ID: " + id);
            }
        }
    }

    @Override
    public WebMapTileServer createWebMapTileServer() throws ServiceException, IOException {
        String baseUrl = geoViewProperties.getGisServer().getUrl();
        if (baseUrl == null || baseUrl.isBlank()) {
            throw new BusinessException("GIS 서버 URL이 설정되지 않았습니다.", HttpStatus.INTERNAL_SERVER_ERROR);
        }
        URI capabilitiesUri = URI.create(baseUrl.replaceAll("/+$", "") + "/map/wmts?service=WMTS&request=GetCapabilities&version=1.0.0");
        return new WebMapTileServer(capabilitiesUri.toURL());
    }

    @Override
    public void writeFlightPhotoZip(OutputStream out, FlightPhotoDownloadRequestVO request,
                                    Boolean hasPermission, WebMapTileServer wmts) throws IOException, ServiceException {
        int defaultSize = geoViewProperties.getLayer().getImageDefaultSize();
        String prefix = geoViewProperties.getLayer().getServicePrefix();

        double bboxW = request.getMaxx() - request.getMinx();
        double bboxH = request.getMaxy() - request.getMiny();
        double ratio = bboxW / bboxH;
        int width = resolveWidth(request.getWidth(), request.getHeight(), ratio, defaultSize);
        int height = resolveHeight(request.getWidth(), request.getHeight(), ratio, defaultSize);

        List<FlightPhotoLayerVO> layers = flightPhotoMapper.selectFlightPhotoLayerInIds(request.getLayerIds());
        CoordinateReferenceSystem crs = decodeCrs(request.getSrid());
        Rectangle paintArea = new Rectangle(0, 0, width, height);

        ZipOutputStream zipOut = new ZipOutputStream(out);
        try {
            ReferencedEnvelope envelope = buildEnvelope(request, crs);

            for (FlightPhotoLayerVO layer : layers) {
                renderLayerToZip(zipOut, layer, wmts, prefix, crs, envelope, paintArea, width, height, hasPermission);
            }
        } finally {
            zipOut.finish();
            zipOut.flush();
        }
    }

    // ========== Private helpers ==========

    private int resolveWidth(Integer reqWidth, Integer reqHeight, double ratio, int defaultSize) {
        if (reqWidth == null && reqHeight == null) return ratio >= 1 ? defaultSize : (int) Math.round(defaultSize * ratio);
        if (reqWidth != null && reqHeight == null) return reqWidth;
        if (reqWidth == null) return (int) Math.round(reqHeight * ratio);
        return reqWidth;
    }

    private int resolveHeight(Integer reqWidth, Integer reqHeight, double ratio, int defaultSize) {
        if (reqWidth == null && reqHeight == null) return ratio >= 1 ? (int) Math.round(defaultSize / ratio) : defaultSize;
        if (reqWidth != null && reqHeight == null) return (int) Math.round(reqWidth / ratio);
        if (reqWidth == null) return reqHeight;
        return reqHeight;
    }

    private CoordinateReferenceSystem decodeCrs(int srid) {
        try {
            CoordinateReferenceSystem crs = CRS.decode("urn:ogc:def:crs:EPSG::" + srid);
            if (crs instanceof ProjectedCRS) {
                crs = CoordinateConverter.createCorrectedCRS((ProjectedCRS) crs);
            }
            return crs;
        } catch (FactoryException e) {
            throw new BusinessException("유효하지 않은 좌표계: " + srid, HttpStatus.BAD_REQUEST, e);
        }
    }

    private ReferencedEnvelope buildEnvelope(FlightPhotoDownloadRequestVO request, CoordinateReferenceSystem crs) {
        if (CRS.getAxisOrder(crs).equals(CRS.AxisOrder.NORTH_EAST)) {
            return new ReferencedEnvelope(request.getMiny(), request.getMaxy(), request.getMinx(), request.getMaxx(), crs);
        }
        return new ReferencedEnvelope(request.getMinx(), request.getMaxx(), request.getMiny(), request.getMaxy(), crs);
    }

    private void renderLayerToZip(ZipOutputStream zipOut, FlightPhotoLayerVO layer, WebMapTileServer wmts,
                                  String prefix, CoordinateReferenceSystem crs, ReferencedEnvelope requestedEnvelope,
                                  Rectangle paintArea, int width, int height, Boolean hasPermission) throws IOException {
        String safeName = sanitize(layer.getFlightPhotoLyrNm());

        if (!hasPermission && !Boolean.TRUE.equals(layer.getOtsdRlsEn())) {
            writeTextEntry(zipOut, safeName + ".txt", "권한이 없어 이 항공사진을 열람할 수 없습니다.");
            return;
        }

        String layerPrefix = (layer.getLyrSrvcPrefix() != null && !layer.getLyrSrvcPrefix().isEmpty()) ? layer.getLyrSrvcPrefix() : prefix;
        MapContent mapContent = null;
        Graphics2D g = null;

        try {
            ReferencedEnvelope layerEnvelope;
            try {
                CoordinateReferenceSystem layerCRS = CRS.decode("urn:ogc:def:crs:EPSG::" + layer.getCntm(), true);
                layerEnvelope = requestedEnvelope.transform(layerCRS, true);
            } catch (FactoryException | TransformException e) {
                writeTextEntry(zipOut, safeName + ".txt", "좌표 변환에 실패했습니다.");
                return;
            }

            WMTSLayer layerInfo = wmts.getCapabilities().getLayer(layerPrefix + ":" + layer.getLyrSrvcNm());
            if (layerInfo == null) {
                writeTextEntry(zipOut, safeName + ".txt", "요청한 레이어를 찾을 수 없습니다.");
                return;
            }

            WMTSMapLayer mapLayer = new WMTSMapLayer(wmts, layerInfo);
            mapContent = new MapContent();
            mapContent.getViewport().setBounds(layerEnvelope);
            mapContent.getViewport().setScreenArea(paintArea);
            mapContent.getViewport().setMatchingAspectRatio(false);
            mapContent.addLayer(mapLayer);

            StreamingRenderer renderer = new StreamingRenderer();
            renderer.setMapContent(mapContent);

            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            g = image.createGraphics();
            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            renderer.paint(g, paintArea, layerEnvelope);

            zipOut.putNextEntry(new ZipEntry(safeName + ".png"));
            try {
                ImageIO.write(image, "png", zipOut);
            } finally {
                zipOut.closeEntry();
            }
            zipOut.flush();

        } catch (IOException e) {
            log.error("항공 사진 렌더링 실패: id={}", layer.getFlightPhotoLyrId(), e);
            writeTextEntry(zipOut, safeName + ".txt", "서버 내부 오류가 발생했습니다.");
        } finally {
            if (g != null) g.dispose();
            if (mapContent != null) mapContent.dispose();
        }
    }

    private void writeTextEntry(ZipOutputStream zipOut, String name, String message) throws IOException {
        zipOut.putNextEntry(new ZipEntry(name));
        try {
            zipOut.write(message.getBytes(StandardCharsets.UTF_8));
        } finally {
            zipOut.closeEntry();
            zipOut.flush();
        }
    }

    private String sanitize(String name) {
        if (name == null) return "layer";
        return name.replaceAll("[\\\\/:*?\"<>|\\s]+", "_");
    }
}
