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.jjb.saas.framework.auth.model.SSOUser;
import com.jjb.saas.framework.auth.utils.AuthContext;
import com.zcloud.personnel.positioning.dto.FenceSyncCmd;
import com.zcloud.personnel.positioning.dto.clientobject.FenceSyncCO;
import com.zcloud.personnel.positioning.integration.finds.FindsOpenApiClient;
import com.zcloud.personnel.positioning.integration.finds.OpenapiConfigCache;
import com.zcloud.personnel.positioning.persistence.dataobject.AreaDO;
import com.zcloud.personnel.positioning.persistence.dataobject.FenceDO;
import com.zcloud.personnel.positioning.persistence.dataobject.OpenapiConfigDO;
import com.zcloud.personnel.positioning.persistence.repository.AreaRepository;
import com.zcloud.personnel.positioning.persistence.repository.FenceRepository;
import com.zcloud.personnel.positioning.persistence.repository.OpenapiConfigRepository;
import lombok.AllArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

@Component
@AllArgsConstructor
public class FenceFindsPullService {
    private static final String LIST_API = "finds.fence.list.v2";
    private static final String STATUS_SUCCESS = "SUCCESS";
    private static final String FINDS_AREA_CODE_PREFIX = "FINDS-AREA-";
    private static final String CLEAR_AREA_LINK_SQL =
            "UPDATE fence SET area_id = NULL, area_name = NULL WHERE area_id = ?";
    private static final int DEFAULT_PAGE_SIZE = 200;
    private static final int DEFAULT_MAX_PAGE_COUNT = 50;

    private final FindsOpenApiClient findsOpenApiClient;
    private final OpenapiConfigCache openapiConfigCache;
    private final OpenapiConfigRepository openapiConfigRepository;
    private final AreaRepository areaRepository;
    private final FenceRepository fenceRepository;
    private final FencePointPersistenceService fencePointPersistenceService;
    private final JdbcTemplate jdbcTemplate;
    private final ObjectMapper objectMapper;
    private final TransactionTemplate transactionTemplate;

    public FenceSyncCO sync(FenceSyncCmd cmd) {
        FenceSyncCmd command = cmd == null ? new FenceSyncCmd() : cmd;
        String companyId = command.getCorpinfoId() == null ? currentCompanyId() : command.getCorpinfoId();
        if (command.getCorpinfoId() == null && companyId != null) {
            command.setCorpinfoId(companyId);
        }
        OpenapiConfigDO config = openapiConfigCache.getEnabledConfig(companyId);
        if (config == null) {
            throw new BizException("No enabled FindS OpenAPI configuration");
        }
        return syncConfig(config, command);
    }

    public FenceSyncCO syncAllConfigured() {
        List<OpenapiConfigDO> configs = listEnabledConfigs();
        if (configs.isEmpty()) {
            OpenapiConfigDO fallback = openapiConfigCache.getEnabledConfig(null);
            configs = fallback == null ? Collections.emptyList() : Collections.singletonList(fallback);
        }
        FenceSyncCO total = newResult();
        Set<String> syncedConfigs = new HashSet<>();
        for (OpenapiConfigDO config : configs) {
            if (!syncedConfigs.add(configIdentity(config))) {
                continue;
            }
            FenceSyncCmd command = new FenceSyncCmd();
            command.setCorpinfoId(config.getCorpinfoId());
            command.setCorpinfoName(config.getCorpinfoName());
            merge(total, syncConfig(config, command));
        }
        total.setSyncTime(LocalDateTime.now());
        return total;
    }

    private FenceSyncCO syncConfig(OpenapiConfigDO config, FenceSyncCmd command) {
        int pageSize = normalizePositive(command.getPageSize(), DEFAULT_PAGE_SIZE);
        int maxPageCount = normalizePositive(command.getMaxPageCount(), DEFAULT_MAX_PAGE_COUNT);
        FenceSyncCO result = newResult();
        long total = Long.MAX_VALUE;
        long loaded = 0;
        int pageNo = 1;
        while (loaded < total && pageNo <= maxPageCount) {
            Map<String, Object> request = new LinkedHashMap<>();
            if (StringUtils.hasText(command.getNameLike())) {
                request.put("nameLike", command.getNameLike().trim());
            }
            request.put("pageNo", pageNo);
            request.put("pageSize", pageSize);
            JsonNode root = findsOpenApiClient.post(config, LIST_API, request);
            JsonNode data = assertSuccess(root).path("data");
            JsonNode rows = extractRows(data);
            if (!rows.isArray() || rows.size() == 0) {
                total = loaded;
                break;
            }
            for (JsonNode row : rows) {
                result.setPulledCount(result.getPulledCount() + 1);
                SyncRowResult rowResult = transactionTemplate.execute(status -> upsertFence(row, command, result.getSyncTime()));
                if (rowResult == null || rowResult.skipped) {
                    result.setSkippedCount(result.getSkippedCount() + 1);
                    continue;
                }
                if (rowResult.inserted) {
                    result.setInsertedCount(result.getInsertedCount() + 1);
                } else {
                    result.setUpdatedCount(result.getUpdatedCount() + 1);
                }
                if (rowResult.unmappedCompany) {
                    result.setUnmappedCompanyCount(result.getUnmappedCompanyCount() + 1);
                }
            }
            loaded += rows.size();
            total = totalValue(data, loaded);
            pageNo++;
        }
        if (loaded < total) {
            throw new BizException("FindS fence pull exceeded maxPageCount before all rows were loaded");
        }
        return result;
    }

    private SyncRowResult upsertFence(JsonNode row, FenceSyncCmd command, LocalDateTime syncTime) {
        SyncRowResult result = new SyncRowResult();
        Long findsFenceId = longValue(row.path("id"));
        String fenceName = textValue(row.path("name"));
        JsonNode polygon = row.path("polygon");
        if (findsFenceId == null || !StringUtils.hasText(fenceName) || polygon.isMissingNode()
                || polygon.isNull() || !polygon.isArray()) {
            result.skipped = true;
            return result;
        }

        FenceDO fence = queryExistingFence(findsFenceId, textValue(row.path("code")));
        if (fence != null && isPendingLocalSync(fence.getSyncStatus())) {
            result.skipped = true;
            return result;
        }
        result.inserted = fence == null;
        if (fence == null) {
            fence = new FenceDO();
            fence.setFenceCode("FINDS-" + findsFenceId);
        }

        JsonNode fenceBiz = firstPresent(row.path("fenceBiz"), row.path("biz"));
        String remoteOrgCode = firstText(textValue(row.path("orgCode")), textValue(row.path("creditCode")));
        String remoteOrgName = textValue(row.path("orgName"));
        CorpInfo corpInfo = resolveCorpInfo(remoteOrgCode, remoteOrgName, command);

        fence.setFindsFenceId(findsFenceId);
        fence.setFindsFenceCode(firstText(textValue(row.path("code")), fence.getFindsFenceCode()));
        fence.setFenceName(fenceName);
        fence.setFenceBizName(normalizeBizName(firstText(textValue(fenceBiz.path("name")),
                textValue(row.path("bizName")), fence.getFenceBizName(), FencePersistenceSupport.DEFAULT_BIZ_NAME)));
        if (!FencePersistenceSupport.SCOPE_COMPANY_AREA.equals(fence.getFenceScope())) {
            fence.setFenceScope(inferScope(fence.getFenceBizName(), booleanValue(row.path("silence"))));
        }
        fence.setCorpinfoId(corpInfo == null ? fence.getCorpinfoId() : corpInfo.corpinfoId);
        fence.setCorpinfoName(firstText(corpInfo == null ? null : corpInfo.corpName,
                command.getCorpinfoName(), remoteOrgName, fence.getCorpinfoName()));
        fence.setOrgCode(firstText(remoteOrgCode, corpInfo == null ? null : corpInfo.orgCode, fence.getOrgCode()));
        fence.setOrgName(firstText(remoteOrgName, corpInfo == null ? null : corpInfo.corpName,
                command.getCorpinfoName(), fence.getOrgName()));
        fence.setCoor(firstText(textValue(row.path("coor")), fence.getCoor(), FencePersistenceSupport.DEFAULT_COOR));
        fence.setSpaceType(firstText(textValue(row.path("spaceType")), fence.getSpaceType(),
                FencePersistenceSupport.DEFAULT_SPACE_TYPE));
        fence.setBuildingId(firstLong(longValue(row.path("buildingId")), fence.getBuildingId()));
        fence.setFloor(firstInteger(integerValue(row.path("floor")), fence.getFloor()));
        fence.setPolygonJson(jsonValue(polygon));
        fence.setCenterLon(null);
        fence.setCenterLat(null);
        fence.setSilence(booleanValue(row.path("silence")) ? 1 : 0);
        if (!row.path("effectPeriod").isMissingNode() && !row.path("effectPeriod").isNull()) {
            fence.setEffectPeriodJson(jsonValue(row.path("effectPeriod")));
        }
        if (result.inserted) {
            JsonNode enableNode = fenceBiz.path("enable");
            fence.setStatus((enableNode.isMissingNode() || enableNode.isNull() || booleanValue(enableNode))
                    ? AlarmRuleConfigPersistence.STATUS_ENABLED
                    : AlarmRuleConfigPersistence.STATUS_DISABLED);
        }
        fence.setSyncStatus(STATUS_SUCCESS);
        fence.setLastSyncTime(syncTime);
        fence.setRawJson(row.toString());
        fencePointPersistenceService.fillCenter(fence);
        if (result.inserted) {
            fenceRepository.save(fence);
        } else {
            fenceRepository.updateById(fence);
        }
        fencePointPersistenceService.rebuild(fence);
        syncAreaFromFence(fence);
        result.unmappedCompany = fence.getCorpinfoId() == null;
        return result;
    }

    void syncAreaFromFence(FenceDO fence) {
        if (fence.getFindsFenceId() == null) {
            return;
        }
        QueryWrapper<AreaDO> query = new QueryWrapper<>();
        query.eq("finds_fence_id", fence.getFindsFenceId());
        query.and(wrapper -> wrapper.isNull("delete_enum").or().eq("delete_enum", "FALSE"));
        query.last("LIMIT 1");
        AreaDO area = areaRepository.getOne(query, false);
        if (area != null && FencePersistenceSupport.SCOPE_COMPANY_AREA.equals(area.getAreaType())) {
            linkFenceToArea(fence, area);
            return;
        }
        if (!isFindsArea(fence.getFenceBizName())) {
            removeGeneratedArea(area, fence);
            return;
        }

        boolean create = area == null;
        if (create) {
            area = new AreaDO();
            area.setAreaCode(FINDS_AREA_CODE_PREFIX + fence.getFindsFenceId());
            area.setAreaLevel(1);
            area.setSortNo(0);
        }
        area.setAreaName(fence.getFenceName());
        area.setAreaType(FencePersistenceSupport.SCOPE_SILENCE.equals(fence.getFenceScope())
                ? "BUFFER" : "WORK_AREA");
        area.setOrgCode(fence.getOrgCode());
        area.setOrgName(fence.getOrgName());
        area.setCorpinfoId(fence.getCorpinfoId());
        area.setCorpinfoName(fence.getCorpinfoName());
        area.setCenterLon(fence.getCenterLon());
        area.setCenterLat(fence.getCenterLat());
        area.setPolygonJson(fence.getPolygonJson());
        area.setStatus(fence.getStatus());
        area.setBindFenceId(fence.getId());
        area.setFindsFenceId(fence.getFindsFenceId());
        boolean saved = create ? areaRepository.save(area) : areaRepository.updateById(area);
        if (!saved) {
            throw new BizException("FindS fence area save failed");
        }
        linkFenceToArea(fence, area);
    }

    private void linkFenceToArea(FenceDO fence, AreaDO area) {
        if (Objects.equals(fence.getAreaId(), area.getId())
                && Objects.equals(fence.getAreaName(), area.getAreaName())) {
            return;
        }
        fence.setAreaId(area.getId());
        fence.setAreaName(area.getAreaName());
        if (!fenceRepository.updateById(fence)) {
            throw new BizException("FindS fence area link update failed");
        }
    }

    private void removeGeneratedArea(AreaDO area, FenceDO fence) {
        if (area == null || !isGeneratedArea(area, fence.getFindsFenceId())) {
            return;
        }
        jdbcTemplate.update(CLEAR_AREA_LINK_SQL, area.getId());
        if (!areaRepository.removeById(area.getId())) {
            throw new BizException("Invalid FindS fence area cleanup failed");
        }
        if (Objects.equals(fence.getAreaId(), area.getId())) {
            fence.setAreaId(null);
            fence.setAreaName(null);
        }
    }

    private boolean isGeneratedArea(AreaDO area, Long findsFenceId) {
        return findsFenceId != null
                && (FINDS_AREA_CODE_PREFIX + findsFenceId).equals(area.getAreaCode())
                && !FencePersistenceSupport.SCOPE_COMPANY_AREA.equals(area.getAreaType());
    }

    private boolean isFindsArea(String bizName) {
        return FencePersistenceSupport.DEFAULT_BIZ_NAME.equals(normalizeBizName(bizName));
    }

    private String normalizeBizName(String bizName) {
        return StringUtils.hasText(bizName) ? bizName.trim() : bizName;
    }

    private FenceDO queryExistingFence(Long findsFenceId, String findsFenceCode) {
        QueryWrapper<FenceDO> byId = activeFenceQuery();
        byId.eq("finds_fence_id", findsFenceId).last("LIMIT 1");
        FenceDO fence = fenceRepository.getOne(byId, false);
        if (fence != null || !StringUtils.hasText(findsFenceCode)) {
            return fence;
        }
        QueryWrapper<FenceDO> byCode = activeFenceQuery();
        byCode.eq("finds_fence_code", findsFenceCode).last("LIMIT 1");
        return fenceRepository.getOne(byCode, false);
    }

    private QueryWrapper<FenceDO> activeFenceQuery() {
        QueryWrapper<FenceDO> query = new QueryWrapper<>();
        query.and(wrapper -> wrapper.isNull("delete_enum").or().eq("delete_enum", "FALSE"));
        return query;
    }

    private CorpInfo resolveCorpInfo(String orgCode, String orgName, FenceSyncCmd command) {
        CorpInfo corpInfo = null;
        if (StringUtils.hasText(orgCode)) {
            corpInfo = queryCorpInfo("code", orgCode.trim());
        }
        if (corpInfo == null && StringUtils.hasText(orgName)) {
            corpInfo = queryCorpInfo("corp_name", orgName.trim());
        }
        if (corpInfo == null && command.getCorpinfoId() != null) {
            corpInfo = queryCorpInfo("corpinfo_id", command.getCorpinfoId());
        }
        if (corpInfo == null && StringUtils.hasText(command.getCorpinfoName())) {
            corpInfo = queryCorpInfo("corp_name", command.getCorpinfoName().trim());
        }
        return corpInfo;
    }

    private CorpInfo queryCorpInfo(String column, Object value) {
        String sql = "SELECT corpinfo_id, corp_name, code FROM corp_info "
                + "WHERE (delete_enum IS NULL OR delete_enum = 'FALSE') AND " + column + " = ? LIMIT 1";
        List<CorpInfo> rows = jdbcTemplate.query(sql, new Object[]{value}, this::mapCorpInfo);
        return rows.isEmpty() ? null : rows.get(0);
    }

    private CorpInfo mapCorpInfo(ResultSet rs, int rowNum) throws SQLException {
        CorpInfo corpInfo = new CorpInfo();
        String id = rs.getString("corpinfo_id");
        corpInfo.corpinfoId = rs.wasNull() ? null : id;
        corpInfo.corpName = rs.getString("corp_name");
        corpInfo.orgCode = rs.getString("code");
        return corpInfo;
    }

    private List<OpenapiConfigDO> listEnabledConfigs() {
        QueryWrapper<OpenapiConfigDO> query = new QueryWrapper<>();
        query.eq("enabled", 1);
        query.and(wrapper -> wrapper.isNull("delete_enum").or().eq("delete_enum", "FALSE"));
        query.orderByDesc("update_time");
        List<OpenapiConfigDO> configs = openapiConfigRepository.list(query);
        return configs == null ? Collections.emptyList() : configs;
    }

    private String configIdentity(OpenapiConfigDO config) {
        return firstText(config.getHostUrl(), "") + "|" + firstText(config.getSik(), "");
    }

    private JsonNode assertSuccess(JsonNode root) {
        String code = root == null ? null : root.path("code").asText();
        if (!"200".equals(code) && !"0".equals(code)) {
            String msg = root == null ? "" : root.path("msg").asText();
            String detail = root == null ? "" : root.path("detail").asText();
            throw new BizException("FindS fence list failed: " + msg + " " + detail);
        }
        return root;
    }

    private JsonNode extractRows(JsonNode data) {
        if (data == null || data.isMissingNode() || data.isNull()) {
            return objectMapper.createArrayNode();
        }
        if (data.isArray()) {
            return data;
        }
        for (String field : new String[]{"data", "rows", "records", "list"}) {
            JsonNode rows = data.path(field);
            if (rows.isArray()) {
                return rows;
            }
        }
        return objectMapper.createArrayNode();
    }

    private long totalValue(JsonNode data, long defaultValue) {
        for (String field : new String[]{"total", "totalCount", "count"}) {
            Long value = longValue(data.path(field));
            if (value != null) {
                return value;
            }
        }
        return defaultValue;
    }

    private String inferScope(String bizName, boolean silence) {
        return silence || FencePersistenceSupport.SILENCE_BIZ_NAME.equals(normalizeBizName(bizName))
                ? FencePersistenceSupport.SCOPE_SILENCE
                : FencePersistenceSupport.SCOPE_WORK_AREA;
    }

    private boolean isPendingLocalSync(String status) {
        return FindsPendingSyncService.STATUS_WAIT.equals(status)
                || FindsPendingSyncService.STATUS_FAIL.equals(status)
                || FindsPendingSyncService.STATUS_DELETE_WAIT.equals(status)
                || FindsPendingSyncService.STATUS_DELETE_FAIL.equals(status);
    }

    private FenceSyncCO newResult() {
        FenceSyncCO result = new FenceSyncCO();
        result.setPulledCount(0);
        result.setInsertedCount(0);
        result.setUpdatedCount(0);
        result.setSkippedCount(0);
        result.setUnmappedCompanyCount(0);
        result.setSyncTime(LocalDateTime.now());
        return result;
    }

    private void merge(FenceSyncCO total, FenceSyncCO item) {
        total.setPulledCount(total.getPulledCount() + item.getPulledCount());
        total.setInsertedCount(total.getInsertedCount() + item.getInsertedCount());
        total.setUpdatedCount(total.getUpdatedCount() + item.getUpdatedCount());
        total.setSkippedCount(total.getSkippedCount() + item.getSkippedCount());
        total.setUnmappedCompanyCount(total.getUnmappedCompanyCount() + item.getUnmappedCompanyCount());
    }

    private JsonNode firstPresent(JsonNode... nodes) {
        for (JsonNode node : nodes) {
            if (node != null && !node.isMissingNode() && !node.isNull()) {
                return node;
            }
        }
        return objectMapper.createObjectNode();
    }

    private Long longValue(JsonNode node) {
        if (node == null || node.isMissingNode() || node.isNull()) {
            return null;
        }
        if (node.isNumber()) {
            return node.asLong();
        }
        try {
            return StringUtils.hasText(node.asText()) ? Long.valueOf(node.asText()) : null;
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

    private Integer integerValue(JsonNode node) {
        Long value = longValue(node);
        return value == null ? null : value.intValue();
    }

    private boolean booleanValue(JsonNode node) {
        if (node == null || node.isMissingNode() || node.isNull()) {
            return false;
        }
        if (node.isBoolean()) {
            return node.asBoolean();
        }
        String value = node.asText();
        return "1".equals(value) || "true".equalsIgnoreCase(value) || "ENABLED".equalsIgnoreCase(value);
    }

    private String textValue(JsonNode node) {
        return node != null && !node.isMissingNode() && !node.isNull() && StringUtils.hasText(node.asText())
                ? node.asText() : null;
    }

    private String jsonValue(JsonNode node) {
        try {
            return objectMapper.writeValueAsString(node);
        } catch (IOException e) {
            throw new BizException("FindS fence JSON conversion failed");
        }
    }

    private String firstText(String... values) {
        for (String value : values) {
            if (StringUtils.hasText(value)) {
                return value;
            }
        }
        return null;
    }

    private Long firstLong(Long... values) {
        for (Long value : values) {
            if (value != null) {
                return value;
            }
        }
        return null;
    }

    private Integer firstInteger(Integer... values) {
        for (Integer value : values) {
            if (value != null) {
                return value;
            }
        }
        return null;
    }

    private int normalizePositive(Integer value, int defaultValue) {
        return value == null || value < 1 ? defaultValue : value;
    }

    private String currentCompanyId() {
        SSOUser currentUser = AuthContext.getCurrentUser();
        return currentUser == null || currentUser.getCompanyId() == null
                ? null : String.valueOf(currentUser.getCompanyId());
    }

    private static class CorpInfo {
        private String corpinfoId;
        private String corpName;
        private String orgCode;
    }

    private static class SyncRowResult {
        private boolean inserted;
        private boolean skipped;
        private boolean unmappedCompany;
    }
}
