package com.zcloud.personnel.positioning.command;

import com.alibaba.cola.exception.BizException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zcloud.personnel.positioning.persistence.dataobject.FenceDO;
import com.zcloud.personnel.positioning.persistence.dataobject.FencePointDO;
import com.zcloud.personnel.positioning.persistence.repository.FencePointRepository;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;

@Component
@AllArgsConstructor
public class FencePointPersistenceService {
    private final FencePointRepository fencePointRepository;
    private final ObjectMapper objectMapper;

    public void rebuild(FenceDO fence) {
        if (fence == null || fence.getId() == null) {
            throw new BizException("Fence id is required");
        }
        List<RingPoint> points = parsePolygon(fence.getPolygonJson());
        QueryWrapper<FencePointDO> removeWrapper = new QueryWrapper<>();
        removeWrapper.eq("fence_id", fence.getId());
        fencePointRepository.remove(removeWrapper);
        for (RingPoint point : points) {
            FencePointDO pointDO = new FencePointDO();
            pointDO.setFenceId(fence.getId());
            pointDO.setRingIndex(point.ringIndex);
            pointDO.setPointIndex(point.pointIndex);
            pointDO.setLon(point.lon);
            pointDO.setLat(point.lat);
            pointDO.setAlt(point.alt);
            pointDO.setCorpinfoId(fence.getCorpinfoId());
            pointDO.setCorpinfoName(fence.getCorpinfoName());
            fencePointRepository.save(pointDO);
        }
    }

    public void removeByFenceId(Long fenceId) {
        if (fenceId == null) {
            return;
        }
        QueryWrapper<FencePointDO> removeWrapper = new QueryWrapper<>();
        removeWrapper.eq("fence_id", fenceId);
        fencePointRepository.remove(removeWrapper);
    }

    public void fillCenter(FenceDO fence) {
        if (fence == null || (fence.getCenterLon() != null && fence.getCenterLat() != null)) {
            return;
        }
        List<RingPoint> points = parsePolygon(fence.getPolygonJson());
        if (points.isEmpty()) {
            return;
        }
        BigDecimal lonSum = BigDecimal.ZERO;
        BigDecimal latSum = BigDecimal.ZERO;
        for (RingPoint point : points) {
            lonSum = lonSum.add(point.lon);
            latSum = latSum.add(point.lat);
        }
        BigDecimal count = BigDecimal.valueOf(points.size());
        fence.setCenterLon(lonSum.divide(count, 10, RoundingMode.HALF_UP));
        fence.setCenterLat(latSum.divide(count, 10, RoundingMode.HALF_UP));
    }

    public Object parsePolygonObject(String polygonJson) {
        if (!StringUtils.hasText(polygonJson)) {
            throw new BizException("Fence polygonJson is required");
        }
        try {
            return objectMapper.readValue(polygonJson, Object.class);
        } catch (IOException e) {
            throw new BizException("Fence polygonJson is invalid");
        }
    }

    private List<RingPoint> parsePolygon(String polygonJson) {
        if (!StringUtils.hasText(polygonJson)) {
            throw new BizException("Fence polygonJson is required");
        }
        try {
            JsonNode root = objectMapper.readTree(polygonJson);
            List<RingPoint> points = new ArrayList<>();
            int[] ringIndex = new int[]{0};
            collectRings(root, ringIndex, points);
            if (points.size() < 3) {
                throw new BizException("Fence polygonJson must contain at least three points");
            }
            return points;
        } catch (IOException e) {
            throw new BizException("Fence polygonJson is invalid");
        }
    }

    private void collectRings(JsonNode node, int[] ringIndex, List<RingPoint> points) {
        if (isEmpty(node)) {
            return;
        }
        if (isCoordinate(node)) {
            RingPoint point = toPoint(node, ringIndex[0], points.size());
            points.add(point);
            return;
        }
        if (node.isObject()) {
            node.elements().forEachRemaining(child -> collectRings(child, ringIndex, points));
            return;
        }
        if (!node.isArray()) {
            return;
        }
        if (containsCoordinate(node)) {
            collectRing(node, ringIndex[0]++, points);
            return;
        }
        for (JsonNode child : node) {
            collectRings(child, ringIndex, points);
        }
    }

    private void collectRing(JsonNode ringNode, int ringIndex, List<RingPoint> points) {
        int pointIndex = 0;
        for (JsonNode child : ringNode) {
            if (isCoordinate(child)) {
                points.add(toPoint(child, ringIndex, pointIndex++));
            }
        }
    }

    private boolean containsCoordinate(JsonNode node) {
        if (node == null || !node.isArray()) {
            return false;
        }
        for (JsonNode child : node) {
            if (isCoordinate(child)) {
                return true;
            }
        }
        return false;
    }

    private boolean isCoordinate(JsonNode node) {
        return node != null
                && node.isArray()
                && node.size() >= 2
                && node.get(0).isNumber()
                && node.get(1).isNumber();
    }

    private RingPoint toPoint(JsonNode node, int ringIndex, int pointIndex) {
        RingPoint point = new RingPoint();
        point.ringIndex = ringIndex;
        point.pointIndex = pointIndex;
        point.lon = node.get(0).decimalValue();
        point.lat = node.get(1).decimalValue();
        point.alt = node.size() > 2 && node.get(2).isNumber() ? node.get(2).decimalValue() : null;
        return point;
    }

    private boolean isEmpty(JsonNode node) {
        return node == null || node.isMissingNode() || node.isNull();
    }

    private static class RingPoint {
        private Integer ringIndex;
        private Integer pointIndex;
        private BigDecimal lon;
        private BigDecimal lat;
        private BigDecimal alt;
    }
}
