package com.zcloud.personnel.positioning.command.query;

import com.alibaba.cola.exception.BizException;
import com.fasterxml.jackson.databind.JsonNode;
import com.zcloud.personnel.positioning.integration.finds.FindsOpenApiClient;
import com.zcloud.personnel.positioning.persistence.repository.OrgMatchRepository;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Refreshes the FindS-to-local organization/department mapping cache.
 *
 * <p>FindS exposes no bulk org-list or org-tree API (only a single-code lookup), so this sync
 * reconstructs org structure from the {@code orgNamePath} field (root-to-leaf company/department
 * names) embedded in every {@code finds.staff.list} row, rather than depending on the much larger
 * and slower personnel mapping sync.</p>
 *
 * <p>Since FindS and the local platform do not necessarily share the same hierarchy depth, each
 * distinct path is resolved by first finding the deepest path segment that matches a local
 * {@code corp_info} entry, then descending level-by-level from there through {@code department}
 * for as many remaining segments as still resolve.</p>
 */
@Component
@RequiredArgsConstructor
public class OrgMatchSyncService {
    private static final Logger LOGGER = LoggerFactory.getLogger(OrgMatchSyncService.class);
    private static final String SOURCE_CONFIG_CODE = "FINDS";
    private static final String STAFF_LIST_API = "finds.staff.list";
    private static final int PAGE_SIZE = 2000;
    private static final int MAX_PAGE_COUNT = 50;
    private static final int BATCH_CONCURRENCY = 4;

    private final FindsOpenApiClient findsOpenApiClient;
    private final OrgLocalLookupService orgLocalLookupService;
    private final OrgMatchRepository orgMatchRepository;

    public int syncAll() {
        // The schema uses second precision; keep the same precision for the stale-row cutoff.
        LocalDateTime syncTime = LocalDateTime.now().withNano(0);
        List<List<String>> orgPaths = loadDistinctOrgPaths();

        Set<String> allSegments = new LinkedHashSet<>();
        for (List<String> path : orgPaths) {
            allSegments.addAll(path);
        }
        Map<String, List<OrgLocalLookupService.LocalCorp>> corpCandidates =
                orgLocalLookupService.findCorpsByNames(allSegments);

        int matched = 0;
        for (List<String> path : orgPaths) {
            MatchResult result = resolve(path, corpCandidates);
            upsert(path, result, syncTime);
            if ("MATCHED".equals(result.matchStatus)) {
                matched++;
            }
        }
        disableMissingRows(syncTime);
        LOGGER.info("Org mapping synchronization completed, total={}, matched={}", orgPaths.size(), matched);
        return orgPaths.size();
    }

    private List<List<String>> loadDistinctOrgPaths() {
        Set<List<String>> distinctPaths = new LinkedHashSet<>();
        JsonNode firstData = successData(
                findsOpenApiClient.postFailFast(STAFF_LIST_API, pageRequest(1)));
        appendPaths(distinctPaths, extractRows(firstData));
        long total = requiredTotal(firstData);
        int pageCount = (int) ((total + PAGE_SIZE - 1) / PAGE_SIZE);
        if (pageCount > MAX_PAGE_COUNT) {
            throw new BizException("FindS staff pages exceed the configured safety limit: " + pageCount);
        }
        if (pageCount <= 1) {
            return new ArrayList<>(distinctPaths);
        }
        List<Map<String, Object>> requests = new ArrayList<>(pageCount - 1);
        for (int pageNo = 2; pageNo <= pageCount; pageNo++) {
            requests.add(pageRequest(pageNo));
        }
        for (JsonNode root : findsOpenApiClient.postFailFastBatch(
                STAFF_LIST_API, requests, BATCH_CONCURRENCY)) {
            appendPaths(distinctPaths, extractRows(successData(root)));
        }
        return new ArrayList<>(distinctPaths);
    }

    private Map<String, Object> pageRequest(int pageNo) {
        Map<String, Object> request = new LinkedHashMap<>();
        request.put("pageNo", pageNo);
        request.put("pageSize", PAGE_SIZE);
        return request;
    }

    private int appendPaths(Set<List<String>> result, JsonNode rows) {
        if (rows == null || !rows.isArray()) {
            return 0;
        }
        int added = 0;
        for (JsonNode row : rows) {
            List<String> path = orgNamePath(row);
            if (path != null && !path.isEmpty() && result.add(path)) {
                added++;
            }
        }
        return added;
    }

    private List<String> orgNamePath(JsonNode row) {
        JsonNode pathNode = row == null ? null : row.path("orgNamePath");
        if (pathNode == null || !pathNode.isArray() || pathNode.size() == 0) {
            return null;
        }
        List<String> path = new ArrayList<>(pathNode.size());
        for (JsonNode segment : pathNode) {
            String text = segment == null || segment.isNull() ? null : segment.asText();
            if (!StringUtils.hasText(text)) {
                return null;
            }
            path.add(text.trim());
        }
        return path;
    }

    /**
     * Walks the path from the deepest segment back toward the root looking for a segment that
     * uniquely matches a local corp_info entry (the deepest match wins, since FindS sometimes
     * prefixes the path with a group/holding name that has no local corp_info counterpart).
     * Once a corp anchor is found, remaining segments are resolved one level at a time via
     * {@code department.parent_id}, stopping at the deepest level that still resolves.
     */
    private MatchResult resolve(List<String> path,
                                 Map<String, List<OrgLocalLookupService.LocalCorp>> corpCandidates) {
        int rootIndex = -1;
        OrgLocalLookupService.LocalCorp rootCorp = null;
        boolean ambiguous = false;
        for (int i = path.size() - 1; i >= 0; i--) {
            List<OrgLocalLookupService.LocalCorp> candidates =
                    corpCandidates.get(path.get(i));
            if (candidates == null || candidates.isEmpty()) {
                continue;
            }
            if (candidates.size() > 1) {
                ambiguous = true;
                continue;
            }
            rootIndex = i;
            rootCorp = candidates.get(0);
            break;
        }
        if (rootCorp == null) {
            return ambiguous ? MatchResult.ambiguous() : MatchResult.unmatched();
        }
        OrgLocalLookupService.LocalDepartment lastDepartment = null;
        String currentParentId = rootCorp.getCorpDepartmentId() == null
                ? null : String.valueOf(rootCorp.getCorpDepartmentId());
        for (int i = rootIndex + 1; currentParentId != null && i < path.size(); i++) {
            OrgLocalLookupService.LocalDepartment department =
                    orgLocalLookupService.findChildDepartment(currentParentId, path.get(i));
            if (department == null) {
                break;
            }
            lastDepartment = department;
            currentParentId = String.valueOf(department.getDepartmentId());
        }
        String matchType = lastDepartment == null ? "CORP_NAME" : "DEPARTMENT_NAME";
        return MatchResult.matched(matchType, rootCorp.getCorpinfoId(), rootCorp.getCorpinfoName(),
                lastDepartment == null ? null : lastDepartment.getDepartmentId(),
                lastDepartment == null ? null : lastDepartment.getDepartmentName());
    }

    private void upsert(List<String> path, MatchResult result, LocalDateTime syncTime) {
        String corpinfoName = path.get(0);
        String departmentName = path.get(path.size() - 1);
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("sourceConfigCode", SOURCE_CONFIG_CODE);
        params.put("findsCorpinfoName", emptyIfBlank(corpinfoName));
        params.put("findsDepartmentName", emptyIfBlank(departmentName));
        params.put("localCorpinfoId", result.localCorpinfoId);
        params.put("localCorpinfoName", result.localCorpinfoName);
        params.put("localDepartmentId", result.localDepartmentId);
        params.put("localDepartmentName", result.localDepartmentName);
        params.put("matchStatus", result.matchStatus);
        params.put("matchType", result.matchType);
        params.put("syncTime", syncTime);
        orgMatchRepository.upsert(params);
    }

    private void disableMissingRows(LocalDateTime syncTime) {
        orgMatchRepository.disableMissing(SOURCE_CONFIG_CODE, syncTime);
    }

    private String emptyIfBlank(String value) {
        return StringUtils.hasText(value) ? value.trim() : "";
    }

    private JsonNode successData(JsonNode root) {
        int code = root == null ? -1 : root.path("code").asInt(-1);
        if (code != 200 && code != 0) {
            throw new BizException("FindS staff list synchronization failed, code=" + code);
        }
        return root.path("data");
    }

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

    private long requiredTotal(JsonNode data) {
        JsonNode totalNode = firstPresent(data, "total", "totalCount", "count", "totalRecords");
        if (totalNode == null) {
            throw new BizException("FindS staff response is missing a valid total");
        }
        try {
            return totalNode.isNumber() ? totalNode.asLong() : Long.parseLong(totalNode.asText());
        } catch (NumberFormatException e) {
            throw new BizException("FindS staff response is missing a valid total");
        }
    }

    private JsonNode firstPresent(JsonNode node, String... fields) {
        if (node == null || node.isNull() || node.isMissingNode()) {
            return null;
        }
        for (String field : fields) {
            JsonNode value = node.get(field);
            if (value != null && !value.isNull() && !value.isMissingNode()) {
                return value;
            }
        }
        return null;
    }

    private static class MatchResult {
        private final String matchStatus;
        private final String matchType;
        private final String localCorpinfoId;
        private final String localCorpinfoName;
        private final Long localDepartmentId;
        private final String localDepartmentName;

        private MatchResult(String matchStatus, String matchType, String localCorpinfoId,
                             String localCorpinfoName, Long localDepartmentId, String localDepartmentName) {
            this.matchStatus = matchStatus;
            this.matchType = matchType;
            this.localCorpinfoId = localCorpinfoId;
            this.localCorpinfoName = localCorpinfoName;
            this.localDepartmentId = localDepartmentId;
            this.localDepartmentName = localDepartmentName;
        }

        private static MatchResult matched(String matchType, String localCorpinfoId, String localCorpinfoName,
                                            Long localDepartmentId, String localDepartmentName) {
            return new MatchResult("MATCHED", matchType, localCorpinfoId, localCorpinfoName,
                    localDepartmentId, localDepartmentName);
        }

        private static MatchResult ambiguous() {
            return new MatchResult("AMBIGUOUS", null, null, null, null, null);
        }

        private static MatchResult unmatched() {
            return new MatchResult("UNMATCHED", null, null, null, null, null);
        }
    }
}
