package com.zioinfo.cms.uiws.system.service;

import com.zioinfo.cms.uiws.common.UiwsApiException;
import com.zioinfo.cms.uiws.common.UiwsErrorCode;
import com.zioinfo.cms.uiws.system.dto.DeptDto;
import com.zioinfo.cms.uiws.system.dto.DeptSaveDto;
import com.zioinfo.cms.uiws.system.dto.DeptTreeDto;
import com.zioinfo.cms.uiws.system.dto.PageResponse;
import com.zioinfo.cms.uiws.system.mapper.DeptMapper;
import com.zioinfo.cms.uiws.system.mapper.SysUserMapper;
import com.zioinfo.cms.uiws.system.model.SysDept;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/** 2.3 부서(depts) CRUD + 검색팝업 + 계층 트리. */
@Service
@RequiredArgsConstructor
public class DeptService {

    private final DeptMapper deptMapper;
    private final SysUserMapper userMapper;

    @Transactional(readOnly = true)
    public PageResponse<DeptDto> list(String keyword, int page, int size) {
        List<DeptDto> content = deptMapper.search(keyword, page * size, size).stream().map(this::toDto).toList();
        return PageResponse.of(content, deptMapper.countSearch(keyword), page, size);
    }

    @Transactional(readOnly = true)
    public List<DeptDto> searchPopup(String keyword) {
        return deptMapper.searchActive(keyword).stream().map(this::toDto).toList();
    }

    /** 부서 계층 트리(루트부터 중첩). sortOrd→deptId 순. */
    @Transactional(readOnly = true)
    public List<DeptTreeDto> tree() {
        List<SysDept> all = deptMapper.findAll();
        Set<String> ids = all.stream().map(SysDept::getDeptId).collect(Collectors.toSet());

        Map<String, List<SysDept>> childrenOf = new HashMap<>();
        for (SysDept d : all) {
            if (d.getParentDeptId() != null && ids.contains(d.getParentDeptId())) {
                childrenOf.computeIfAbsent(d.getParentDeptId(), k -> new ArrayList<>()).add(d);
            }
        }
        Comparator<SysDept> order = Comparator
                .comparing((SysDept d) -> d.getSortOrd() == null ? 0 : d.getSortOrd())
                .thenComparing(SysDept::getDeptId);

        List<SysDept> roots = all.stream()
                .filter(d -> d.getParentDeptId() == null || !ids.contains(d.getParentDeptId()))
                .sorted(order)
                .toList();
        return roots.stream().map(r -> toNode(r, childrenOf, order, new HashSet<>())).toList();
    }

    private DeptTreeDto toNode(SysDept d, Map<String, List<SysDept>> childrenOf,
                               Comparator<SysDept> order, Set<String> visited) {
        if (!visited.add(d.getDeptId())) {
            return new DeptTreeDto(d.getDeptId(), d.getDeptNm(), d.getParentDeptId(),
                    d.getSortOrd() == null ? 0 : d.getSortOrd(), d.getUseYn(), List.of());
        }
        List<DeptTreeDto> children = childrenOf.getOrDefault(d.getDeptId(), List.of()).stream()
                .sorted(order)
                .map(c -> toNode(c, childrenOf, order, visited))
                .toList();
        return new DeptTreeDto(d.getDeptId(), d.getDeptNm(), d.getParentDeptId(),
                d.getSortOrd() == null ? 0 : d.getSortOrd(), d.getUseYn(), children);
    }

    @Transactional(readOnly = true)
    public DeptDto get(String deptId) {
        return toDto(find(deptId));
    }

    @Transactional
    public DeptDto create(DeptSaveDto dto) {
        if (dto.deptId() == null || dto.deptId().isBlank()) {
            throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "deptId는 등록 시 필수입니다.");
        }
        if (deptMapper.existsById(dto.deptId())) {
            throw new UiwsApiException(UiwsErrorCode.DUPLICATE_KEY, "이미 존재하는 부서 ID입니다.");
        }
        validateParent(dto.deptId(), dto.parentDeptId());
        SysDept d = new SysDept();
        d.setDeptId(dto.deptId());
        d.setDeptNm(dto.deptNm());
        d.setParentDeptId(dto.parentDeptId());
        d.setSortOrd(dto.sortOrd() == null ? 0 : dto.sortOrd());
        d.setUseYn(dto.useYn());
        d.setCreatedBy(SysActor.id());
        deptMapper.insert(d);
        return toDto(find(dto.deptId()));
    }

    @Transactional
    public DeptDto update(String deptId, DeptSaveDto dto) {
        SysDept d = find(deptId);
        validateParent(deptId, dto.parentDeptId());
        d.setDeptNm(dto.deptNm());
        d.setParentDeptId(dto.parentDeptId());
        d.setSortOrd(dto.sortOrd() == null ? 0 : dto.sortOrd());
        d.setUseYn(dto.useYn());
        d.setUpdatedBy(SysActor.id());
        deptMapper.update(d);
        return toDto(find(deptId));
    }

    @Transactional
    public void delete(String deptId) {
        find(deptId);
        if (deptMapper.existsByParentDeptId(deptId)) {
            throw new UiwsApiException(UiwsErrorCode.RESOURCE_IN_USE, "하위 부서가 존재하여 삭제할 수 없습니다.");
        }
        if (!userMapper.findByDeptId(deptId).isEmpty()) {
            throw new UiwsApiException(UiwsErrorCode.RESOURCE_IN_USE, "소속 사용자가 존재하여 삭제할 수 없습니다.");
        }
        deptMapper.deleteById(deptId);
    }

    /** 상위부서 무결성: 자기참조 금지·존재 확인·순환(자기 하위부서를 상위로) 금지. */
    private void validateParent(String deptId, String parentId) {
        if (parentId == null || parentId.isBlank()) {
            return;
        }
        if (parentId.equals(deptId)) {
            throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "자기 자신을 상위부서로 지정할 수 없습니다.");
        }
        if (!deptMapper.existsById(parentId)) {
            throw new UiwsApiException(UiwsErrorCode.DEPT_NOT_FOUND, "상위부서가 존재하지 않습니다: " + parentId);
        }
        if (deptId != null && selfAndDescendants(deptId).contains(parentId)) {
            throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "하위부서를 상위부서로 지정할 수 없습니다(순환 구조).");
        }
    }

    /** deptId 자신 + 모든 하위부서 ID 집합(순환 방지용). */
    private Set<String> selfAndDescendants(String deptId) {
        List<SysDept> all = deptMapper.findAll();
        Map<String, List<String>> childrenOf = new HashMap<>();
        for (SysDept d : all) {
            if (d.getParentDeptId() != null) {
                childrenOf.computeIfAbsent(d.getParentDeptId(), k -> new ArrayList<>()).add(d.getDeptId());
            }
        }
        Set<String> result = new HashSet<>();
        ArrayList<String> stack = new ArrayList<>();
        stack.add(deptId);
        while (!stack.isEmpty()) {
            String cur = stack.remove(stack.size() - 1);
            if (!result.add(cur)) {
                continue;
            }
            stack.addAll(childrenOf.getOrDefault(cur, List.of()));
        }
        return result;
    }

    private DeptDto toDto(SysDept d) {
        return new DeptDto(d.getDeptId(), d.getDeptNm(), d.getParentDeptId(),
                d.getSortOrd() == null ? 0 : d.getSortOrd(), d.getUseYn());
    }

    private SysDept find(String deptId) {
        SysDept d = deptMapper.findById(deptId);
        if (d == null) {
            throw new UiwsApiException(UiwsErrorCode.DEPT_NOT_FOUND);
        }
        return d;
    }
}
