package com.erp.service;

import com.erp.common.BaseService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Service
@Slf4j
public class DepartmentService extends BaseService {

    // ──────────────────────────────────────────────────
    // 부서 CRUD
    // ──────────────────────────────────────────────────

    public List<Map<String, Object>> getDepartments(String companyCode) {
        return getDepartments(companyCode, false, null);
    }

    /** soft-delete 대응 — includeDeleted=true 면 DELETED_AT 부서도 포함 */
    public List<Map<String, Object>> getDepartments(String companyCode, boolean includeDeleted) {
        return getDepartments(companyCode, includeDeleted, null);
    }

    /** 기준일 필터 — baseDate 가 있으면 해당 시점에 active 한 부서만 반환 (start_date ≤ baseDate ≤ end_date OR end_date IS NULL) */
    public List<Map<String, Object>> getDepartments(String companyCode, boolean includeDeleted, String baseDate) {
        Map<String, Object> params = new HashMap<>();
        params.put("company_code", companyCode);
        params.put("include_deleted", includeDeleted);
        params.put("base_date", baseDate);  // null/빈문자면 XML if 가 skip
        List<Map<String, Object>> departments = sqlSession.selectList("department.selectDepartments", params);

        for (Map<String, Object> dept : departments) {
            Object cnt = dept.get("member_count");
            if (cnt != null) {
                dept.put("member_count", ((Number) cnt).intValue());
            } else {
                dept.put("member_count", 0);
            }
            // dept_managers JSON 컬럼들 (String) → List<Map> 으로 파싱
            parseManagersJson(dept, "approval_managers");
            parseManagersJson(dept, "dept_managers");
            parseManagersJson(dept, "org_leaders");
        }
        return departments;
    }

    /** active 부서만 반환. deleted 면 null. 복구 흐름은 getDepartmentIncludingDeleted 사용 */
    public Map<String, Object> getDepartment(String deptCode) {
        Map<String, Object> params = new HashMap<>();
        params.put("dept_code", deptCode);
        Map<String, Object> dept = sqlSession.selectOne("department.selectDepartmentByCode", params);
        if (dept != null) {
            parseManagersJson(dept, "approval_managers");
            parseManagersJson(dept, "dept_managers");
            parseManagersJson(dept, "org_leaders");
        }
        return dept;
    }

    /** deleted 부서까지 포함 — 복구 검증 / 부모 deleted 체크 등 internal 흐름용 */
    public Map<String, Object> getDepartmentIncludingDeleted(String deptCode) {
        Map<String, Object> params = new HashMap<>();
        params.put("dept_code", deptCode);
        Map<String, Object> dept = sqlSession.selectOne("department.selectDepartmentByCodeIncludingDeleted", params);
        if (dept != null) {
            parseManagersJson(dept, "approval_managers");
            parseManagersJson(dept, "dept_managers");
            parseManagersJson(dept, "org_leaders");
        }
        return dept;
    }

    @Transactional
    public Map<String, Object> createDepartment(String companyCode, Map<String, Object> body) {
        // 프론트엔드는 snake_case로 전송 (Node.js 호환)
        String deptName = trimString(bodyParam(body, "dept_name", "dept_name"));
        if (deptName == null || deptName.isEmpty()) {
            throw new IllegalArgumentException("부서명을 입력해주세요.");
        }

        // 중복 부서명 확인
        Map<String, Object> dupParams = new HashMap<>();
        dupParams.put("company_code", companyCode);
        dupParams.put("dept_name", deptName);
        Map<String, Object> duplicate = sqlSession.selectOne("department.selectDuplicateDeptName", dupParams);
        if (duplicate != null) {
            throw new DuplicateDeptNameException("\"" + deptName + "\" 부서가 이미 존재합니다.");
        }

        // 회사명 조회
        Map<String, Object> companyParams = new HashMap<>();
        companyParams.put("company_code", companyCode);
        Map<String, Object> company = sqlSession.selectOne("department.selectCompanyName", companyParams);
        String companyName = (company != null && company.get("company_name") != null)
                ? (String) company.get("company_name")
                : companyCode;

        // parent_dept_code cross-tenant / 존재 / 삭제 검증
        Object parentObj = nullIfBlank(bodyParam(body, "parent_dept_code", "parent_dept_code"));
        String parentCode = parentObj != null ? parentObj.toString() : null;
        validateParent(parentCode, companyCode);

        // 부서 코드 자동 생성 — 사용자 입력 받지 않음 (정책 변경 2026-05-08)
        // 재시도 로직 (race condition 대비, 최대 3회)
        String deptCode = null;
        for (int attempt = 0; attempt < 3; attempt++) {
            Map<String, Object> codeResult = sqlSession.selectOne("department.selectNextDeptNumber", null);
            long nextNumber = codeResult != null && codeResult.get("next_number") != null
                    ? ((Number) codeResult.get("next_number")).longValue()
                    : 1L;
            String candidate = "DEPT_" + nextNumber;
            Map<String, Object> existing = sqlSession.selectOne("department.selectDepartmentByCodeIncludingDeleted",
                    Map.of("dept_code", candidate));
            if (existing == null) {
                deptCode = candidate;
                break;
            }
        }
        if (deptCode == null) {
            throw new IllegalStateException("부서 코드 생성 실패 (동시 생성 충돌). 잠시 후 다시 시도해주세요.");
        }

        // 부서 생성 (전체 필드)
        Map<String, Object> insertParams = new HashMap<>();
        insertParams.put("dept_code", deptCode);
        insertParams.put("dept_name", deptName);
        insertParams.put("company_code", companyCode);
        insertParams.put("company_name", companyName);
        insertParams.put("parent_dept_code", parentCode);
        insertParams.put("short_name", nullIfBlank(bodyParam(body, "short_name", "short_name")));
        insertParams.put("dept_type", bodyParam(body, "dept_type", "dept_type"));
        insertParams.put("org_system", nullIfBlank(bodyParam(body, "org_system", "org_system")));
        insertParams.put("approval_manager", nullIfBlank(bodyParam(body, "approval_manager", "approval_manager")));
        insertParams.put("dept_manager", nullIfBlank(bodyParam(body, "dept_manager", "dept_manager")));
        insertParams.put("zipcode", nullIfBlank(bodyParam(body, "zipcode", "zipcode")));
        insertParams.put("address1", nullIfBlank(bodyParam(body, "address1", "address1")));
        insertParams.put("address2", nullIfBlank(bodyParam(body, "address2", "address2")));
        insertParams.put("start_date", nullIfBlank(bodyParam(body, "start_date", "start_date")));
        insertParams.put("end_date", nullIfBlank(bodyParam(body, "end_date", "end_date")));
        insertParams.put("sort_order", bodyParam(body, "sort_order", "sort_order"));
        insertParams.put("status", bodyParam(body, "status", "status"));
        // dept_info 추가 필드 (location 코드만 유지 — V019 정리 후)
        insertParams.put("location", nullIfBlank(bodyParam(body, "location", "location")));
        sqlSession.insert("department.insertDepartment", insertParams);

        syncManagers(deptCode, companyCode, body, "approval");
        syncManagers(deptCode, companyCode, body, "dept");
        syncManagers(deptCode, companyCode, body, "org_leader");

        log.info("부서 생성 성공: deptCode={}, deptName={}", deptCode, deptName);

        Map<String, Object> findParams = new HashMap<>();
        findParams.put("dept_code", deptCode);
        return getDepartment(deptCode);
    }

    @Transactional
    public Map<String, Object> updateDepartment(String deptCode, Map<String, Object> body) {
        String deptName = trimString(bodyParam(body, "dept_name", "dept_name"));
        if (deptName == null || deptName.isEmpty()) {
            throw new IllegalArgumentException("부서명을 입력해주세요.");
        }

        // 본인 dept 의 company_code 조회 (validateParent + 중복명 검증에 사용)
        Map<String, Object> existingDept = sqlSession.selectOne(
                "department.selectDepartmentByCodeIncludingDeleted",
                Map.of("dept_code", deptCode)
        );
        String deptCompanyCode = existingDept != null && existingDept.get("company_code") != null
                ? existingDept.get("company_code").toString()
                : null;

        // 사이클 가드 — 자기 자신/자손을 부모로 지정하려는 시도 차단
        Object newParent = nullIfBlank(bodyParam(body, "parent_dept_code", "parent_dept_code"));
        String newParentCode = newParent != null ? newParent.toString() : null;
        // parent_dept_code cross-tenant / 존재 / 삭제 검증
        if (deptCompanyCode != null) {
            validateParent(newParentCode, deptCompanyCode);
        }
        verifyParentCycle(deptCode, newParentCode);

        // 부서명 중복 검증 — 본인 dept_code 는 제외
        if (deptCompanyCode != null) {
            Map<String, Object> dupParams = new HashMap<>();
            dupParams.put("company_code", deptCompanyCode);
            dupParams.put("dept_name", deptName);
            Map<String, Object> duplicate = sqlSession.selectOne("department.selectDuplicateDeptName", dupParams);
            if (duplicate != null && !deptCode.equals(duplicate.get("dept_code"))) {
                throw new DuplicateDeptNameException("\"" + deptName + "\" 부서가 이미 존재합니다.");
            }
        }

        Map<String, Object> params = new HashMap<>();
        params.put("dept_code", deptCode);
        params.put("dept_name", deptName);
        params.put("parent_dept_code", newParent);
        params.put("short_name", nullIfBlank(bodyParam(body, "short_name", "short_name")));
        params.put("dept_type", bodyParam(body, "dept_type", "dept_type"));
        params.put("org_system", nullIfBlank(bodyParam(body, "org_system", "org_system")));
        params.put("approval_manager", nullIfBlank(bodyParam(body, "approval_manager", "approval_manager")));
        params.put("dept_manager", nullIfBlank(bodyParam(body, "dept_manager", "dept_manager")));
        params.put("zipcode", nullIfBlank(bodyParam(body, "zipcode", "zipcode")));
        params.put("address1", nullIfBlank(bodyParam(body, "address1", "address1")));
        params.put("address2", nullIfBlank(bodyParam(body, "address2", "address2")));
        params.put("start_date", nullIfBlank(bodyParam(body, "start_date", "start_date")));
        params.put("end_date", nullIfBlank(bodyParam(body, "end_date", "end_date")));
        params.put("sort_order", bodyParam(body, "sort_order", "sort_order"));
        params.put("status", bodyParam(body, "status", "status"));
        // dept_info 추가 필드 (location 코드만 유지 — V019 정리 후)
        params.put("location", nullIfBlank(bodyParam(body, "location", "location")));

        int updated = sqlSession.update("department.updateDepartment", params);
        if (updated == 0) {
            return null;
        }

        syncManagers(deptCode, deptCompanyCode, body, "approval");
        syncManagers(deptCode, deptCompanyCode, body, "dept");
        syncManagers(deptCode, deptCompanyCode, body, "org_leader");

        log.info("부서 수정 성공: deptCode={}", deptCode);
        return getDepartment(deptCode);
    }

    /**
     * 부서 soft-delete (V1 slim scope).
     * - hard delete 가 아니라 DELETED_AT = NOW() 로 마킹
     * - USER_DEPT 행은 보존 → 복구 시 멤버 그대로 살아남
     * - 활성 자식 부서가 있으면 차단 (deleted 자식은 무시)
     * - 반환: 0 = soft-delete 성공 (보존된 부서원 수는 복구 시점에 재조회)
     *         -1 = not found / already deleted
     */
    @Transactional
    public int deleteDepartment(String deptCode) {
        // 활성 하위 부서 확인 (deleted 자식은 자식 카운트에서 제외)
        Map<String, Object> childParams = new HashMap<>();
        childParams.put("dept_code", deptCode);
        childParams.put("include_deleted", false);
        Number childCountNum = sqlSession.selectOne("department.selectChildDeptCount", childParams);
        int childCount = childCountNum != null ? childCountNum.intValue() : 0;
        if (childCount > 0) {
            throw new IllegalStateException("하위 부서가 있는 부서는 삭제할 수 없습니다. 먼저 하위 부서를 삭제해주세요.");
        }

        // soft-delete: DELETED_AT = NOW(). USER_DEPT 보존
        Map<String, Object> deptParams = new HashMap<>();
        deptParams.put("dept_code", deptCode);
        int updated = sqlSession.update("department.deleteDepartment", deptParams);
        if (updated == 0) {
            return -1; // not found 또는 이미 deleted
        }

        log.info("부서 soft-delete 성공: deptCode={} (USER_DEPT 행 보존)", deptCode);
        return 0;
    }

    /**
     * 부서 복구 (V1 slim scope).
     * - DELETED_AT = NULL 로 되돌림
     * - 부모가 있고 부모도 deleted 상태면 차단 (orphan 방지)
     * - USER_DEPT 행은 soft-delete 시점부터 보존되어왔으므로 자동 복원됨
     */
    @Transactional
    public RestoreResult restoreDepartment(String deptCode) {
        Map<String, Object> dept = getDepartmentIncludingDeleted(deptCode);
        if (dept == null) {
            return RestoreResult.NOT_FOUND;
        }
        if (dept.get("deleted_at") == null) {
            return RestoreResult.NOT_DELETED;
        }

        // 부모 deleted 검증
        Object parentObj = dept.get("parent_dept_code");
        if (parentObj != null && !parentObj.toString().isBlank()) {
            String parentCode = parentObj.toString();
            Map<String, Object> parent = getDepartmentIncludingDeleted(parentCode);
            if (parent != null && parent.get("deleted_at") != null) {
                return RestoreResult.PARENT_DELETED;
            }
        }

        // 동일 이름의 active 부서 중복 검증 (복구 시점)
        Object companyCodeObj = dept.get("company_code");
        Object deptNameObj = dept.get("dept_name");
        if (companyCodeObj != null && deptNameObj != null) {
            Map<String, Object> dupParams = new HashMap<>();
            dupParams.put("company_code", companyCodeObj.toString());
            dupParams.put("dept_name", deptNameObj.toString());
            Map<String, Object> duplicate = sqlSession.selectOne("department.selectDuplicateDeptName", dupParams);
            if (duplicate != null && !deptCode.equals(duplicate.get("dept_code"))) {
                throw new IllegalArgumentException("동일한 이름의 활성 부서가 이미 존재합니다.");
            }
        }

        Map<String, Object> params = new HashMap<>();
        params.put("dept_code", deptCode);
        int restored = sqlSession.update("department.restoreDepartment", params);
        if (restored == 0) {
            return RestoreResult.NOT_DELETED;  // race: 동시 복구
        }

        log.info("부서 복구 성공: deptCode={}", deptCode);
        return RestoreResult.OK;
    }

    public enum RestoreResult {
        OK, NOT_FOUND, NOT_DELETED, PARENT_DELETED
    }

    /**
     * parent_dept_code 가 (a) 존재하고 (b) 같은 회사이며 (c) deleted 가 아닌지 검증.
     * null/blank 면 검증 스킵 (최상위 부서).
     */
    private void validateParent(String parentCode, String companyCode) {
        if (parentCode == null || parentCode.isBlank()) return;
        Map<String, Object> parent = sqlSession.selectOne(
            "department.selectDepartmentByCodeIncludingDeleted",
            Map.of("dept_code", parentCode)
        );
        if (parent == null) {
            throw new IllegalArgumentException("상위 부서를 찾을 수 없습니다: " + parentCode);
        }
        if (parent.get("deleted_at") != null) {
            throw new IllegalArgumentException("삭제된 부서를 상위로 지정할 수 없습니다: " + parentCode);
        }
        Object parentCompany = parent.get("company_code");
        if (parentCompany == null || (!companyCode.equals(parentCompany.toString()) && !"*".equals(parentCompany.toString()))) {
            throw new IllegalArgumentException("다른 회사의 부서를 상위로 지정할 수 없습니다.");
        }
    }

    /**
     * parent_dept_code 변경 시 사이클 검증.
     * deptCode 의 새 부모로 newParent 를 지정하려고 할 때, newParent 또는 그 ancestor
     * 체인에 deptCode 자체가 들어있다면 사이클이 생기므로 차단.
     * (newParent == null 은 최상위로 만들기 — 항상 안전)
     */
    private void verifyParentCycle(String deptCode, String newParent) {
        if (newParent == null) return;
        if (newParent.equals(deptCode)) {
            throw new IllegalArgumentException("자기 자신을 상위 부서로 지정할 수 없습니다.");
        }
        Set<String> visited = new HashSet<>();
        String cur = newParent;
        while (cur != null && !visited.contains(cur)) {
            if (deptCode.equals(cur)) {
                throw new IllegalArgumentException("선택한 부서는 현재 부서의 하위 부서이므로 상위 부서로 지정할 수 없습니다.");
            }
            visited.add(cur);
            Map<String, Object> p = sqlSession.selectOne(
                "department.selectDepartmentByCodeIncludingDeleted",
                Map.of("dept_code", cur)
            );
            if (p == null) break;
            Object parent = p.get("parent_dept_code");
            cur = parent != null ? parent.toString() : null;
        }
    }

    // ──────────────────────────────────────────────────
    // 일괄등록 / 일괄업데이트 (Bulk)
    // ──────────────────────────────────────────────────

    private static final int BULK_MAX_ROWS = 1000;

    /**
     * 일괄등록 — preview (read-only validation). DB 쓰기 없음.
     * batch 내 dept_name 중복 + DB active 중복 + parent/날짜/매니저 검증.
     * 각 row 에 row_index / result(ok|error) / error_detail 채워서 반환.
     */
    public List<Map<String, Object>> bulkPreviewCreate(String companyCode, List<Map<String, Object>> rows) {
        List<Map<String, Object>> results = new ArrayList<>();
        if (rows == null || rows.isEmpty()) return results;
        if (rows.size() > BULK_MAX_ROWS) {
            throw new IllegalArgumentException("한 번에 최대 " + BULK_MAX_ROWS + "건까지 처리 가능합니다.");
        }
        Set<String> existingNames = collectActiveDeptNames(companyCode);
        Set<String> batchNames = new HashSet<>();

        for (int i = 0; i < rows.size(); i++) {
            Map<String, Object> input = rows.get(i);
            Map<String, Object> out = new HashMap<>(input);
            out.put("row_index", i);
            String error = validateBulkCreateRow(input, companyCode, existingNames, batchNames);
            if (error == null) {
                out.put("result", "ok");
                out.put("error_detail", null);
                String dn = trimString(input.get("dept_name"));
                if (dn != null) batchNames.add(dn.toLowerCase());
            } else {
                out.put("result", "error");
                out.put("error_detail", error);
            }
            results.add(out);
        }
        return results;
    }

    /**
     * 일괄업데이트 — preview (read-only). mode = department | manager.
     */
    public List<Map<String, Object>> bulkPreviewUpdate(String companyCode, String mode, List<Map<String, Object>> rows) {
        List<Map<String, Object>> results = new ArrayList<>();
        if (rows == null || rows.isEmpty()) return results;
        if (rows.size() > BULK_MAX_ROWS) {
            throw new IllegalArgumentException("한 번에 최대 " + BULK_MAX_ROWS + "건까지 처리 가능합니다.");
        }
        if (!"department".equals(mode) && !"manager".equals(mode)) {
            throw new IllegalArgumentException("mode 는 department | manager 여야 합니다.");
        }
        for (int i = 0; i < rows.size(); i++) {
            Map<String, Object> input = rows.get(i);
            Map<String, Object> out = new HashMap<>(input);
            out.put("row_index", i);
            String error = validateBulkUpdateRow(input, companyCode, mode);
            if (error == null) {
                out.put("result", "ok");
                out.put("error_detail", null);
            } else {
                out.put("result", "error");
                out.put("error_detail", error);
            }
            results.add(out);
        }
        return results;
    }

    /**
     * 일괄등록 — 실제 저장 (@Transactional, all-or-nothing).
     * 각 row 를 createDepartment 로 위임 — 검증 + manager sync 까지 동일 흐름.
     * 중간 실패 시 IllegalArgumentException 으로 행번호+사유 합쳐서 던짐 → 전체 롤백.
     */
    @Transactional
    public int bulkSaveCreate(String companyCode, List<Map<String, Object>> rows) {
        if (rows == null || rows.isEmpty()) return 0;
        if (rows.size() > BULK_MAX_ROWS) {
            throw new IllegalArgumentException("한 번에 최대 " + BULK_MAX_ROWS + "건까지 등록 가능합니다.");
        }
        int inserted = 0;
        for (int i = 0; i < rows.size(); i++) {
            Map<String, Object> row = rows.get(i);
            String label = trimString(row.get("dept_name"));
            try {
                createDepartment(companyCode, row);
                inserted++;
            } catch (DuplicateDeptNameException | IllegalArgumentException | IllegalStateException e) {
                throw new IllegalArgumentException("행 " + (i + 1) + " (" + (label != null ? label : "?") + "): " + e.getMessage());
            }
        }
        log.info("부서 일괄등록 성공: company={}, inserted={}", companyCode, inserted);
        return inserted;
    }

    /**
     * 일괄업데이트 — 실제 적용 (@Transactional). mode = department | manager.
     * department: 부서 정보 부분 업데이트 (row 의 null/미지정 필드는 기존값 보존).
     * manager: row 에 명시된 매니저 role 만 sync (delete-all + insert-all).
     */
    @Transactional
    public int bulkUpdate(String companyCode, String mode, List<Map<String, Object>> rows) {
        if (rows == null || rows.isEmpty()) return 0;
        if (rows.size() > BULK_MAX_ROWS) {
            throw new IllegalArgumentException("한 번에 최대 " + BULK_MAX_ROWS + "건까지 수정 가능합니다.");
        }
        if (!"department".equals(mode) && !"manager".equals(mode)) {
            throw new IllegalArgumentException("mode 는 department | manager 여야 합니다.");
        }
        int updated = 0;
        for (int i = 0; i < rows.size(); i++) {
            Map<String, Object> row = rows.get(i);
            String deptCode = trimString(row.get("dept_code"));
            if (deptCode == null) {
                throw new IllegalArgumentException("행 " + (i + 1) + ": 부서코드(dept_code) 필수.");
            }
            Map<String, Object> existing = getDepartment(deptCode);
            if (existing == null) {
                throw new IllegalArgumentException("행 " + (i + 1) + ": 부서를 찾을 수 없습니다: " + deptCode);
            }
            String deptCompanyCode = existing.get("company_code") != null
                ? existing.get("company_code").toString() : null;
            if (!companyCode.equals(deptCompanyCode) && !"*".equals(deptCompanyCode)) {
                throw new IllegalArgumentException("행 " + (i + 1) + ": 다른 회사의 부서입니다: " + deptCode);
            }
            try {
                if ("department".equals(mode)) {
                    Map<String, Object> merged = buildMergedDeptBody(existing, row);
                    Map<String, Object> result = updateDepartment(deptCode, merged);
                    if (result == null) {
                        throw new IllegalStateException("수정 실패: " + deptCode);
                    }
                } else {
                    // manager mode — row 에 명시된 role 만 sync
                    if (row.containsKey("approval_managers")) {
                        syncManagers(deptCode, companyCode, row, "approval");
                    }
                    if (row.containsKey("dept_managers")) {
                        syncManagers(deptCode, companyCode, row, "dept");
                    }
                    if (row.containsKey("org_leaders")) {
                        syncManagers(deptCode, companyCode, row, "org_leader");
                    }
                }
                updated++;
            } catch (DuplicateDeptNameException | IllegalArgumentException | IllegalStateException e) {
                throw new IllegalArgumentException("행 " + (i + 1) + " (" + deptCode + "): " + e.getMessage());
            }
        }
        log.info("부서 일괄수정 성공: company={}, mode={}, updated={}", companyCode, mode, updated);
        return updated;
    }

    /** company 의 active 부서명 lowercase set — 일괄등록 중복검증용 */
    private Set<String> collectActiveDeptNames(String companyCode) {
        Set<String> names = new HashSet<>();
        for (Map<String, Object> d : getDepartments(companyCode, false, null)) {
            Object name = d.get("dept_name");
            if (name != null) names.add(name.toString().trim().toLowerCase());
        }
        return names;
    }

    /**
     * 일괄등록 row 검증. null = ok. 에러 메시지 반환 시 해당 row 는 error.
     */
    private String validateBulkCreateRow(Map<String, Object> row, String companyCode,
                                          Set<String> existingNames, Set<String> batchNames) {
        String deptName = trimString(row.get("dept_name"));
        if (deptName == null || deptName.isEmpty()) return "부서명은 필수입니다.";
        String lower = deptName.toLowerCase();
        if (batchNames.contains(lower)) return "동일 일괄 내 부서명 중복: " + deptName;
        if (existingNames.contains(lower)) return "이미 존재하는 부서명: " + deptName;

        String dt = trimString(row.get("dept_type"));
        if (dt != null && !"dept".equals(dt) && !"team".equals(dt) && !"temp".equals(dt)) {
            return "부서유형은 dept|team|temp 중 하나: " + dt;
        }
        String parent = trimString(row.get("parent_dept_code"));
        String parentErr = validateParentForBulk(parent, companyCode);
        if (parentErr != null) return parentErr;

        String dateErr = validateDateRange(row);
        if (dateErr != null) return dateErr;

        String mgrErr = validateManagerIds(row, companyCode);
        if (mgrErr != null) return mgrErr;
        return null;
    }

    /**
     * 일괄업데이트 row 검증. dept_code 필수 + 회사 격리 + (department mode 한정) 부서명/유형/날짜/부모 검증.
     */
    private String validateBulkUpdateRow(Map<String, Object> row, String companyCode, String mode) {
        String deptCode = trimString(row.get("dept_code"));
        if (deptCode == null) return "부서코드(dept_code) 필수.";
        Map<String, Object> existing = getDepartment(deptCode);
        if (existing == null) return "부서를 찾을 수 없습니다: " + deptCode;
        String deptCompanyCode = existing.get("company_code") != null
            ? existing.get("company_code").toString() : null;
        if (!companyCode.equals(deptCompanyCode) && !"*".equals(deptCompanyCode)) {
            return "다른 회사의 부서: " + deptCode;
        }
        if ("department".equals(mode)) {
            String newName = trimString(row.get("dept_name"));
            if (newName != null && !newName.isEmpty()) {
                String existingName = existing.get("dept_name") != null
                    ? existing.get("dept_name").toString().trim() : "";
                if (!newName.equalsIgnoreCase(existingName)) {
                    Map<String, Object> dupParams = new HashMap<>();
                    dupParams.put("company_code", companyCode);
                    dupParams.put("dept_name", newName);
                    Map<String, Object> dup = sqlSession.selectOne("department.selectDuplicateDeptName", dupParams);
                    if (dup != null && !deptCode.equals(dup.get("dept_code"))) {
                        return "이미 존재하는 부서명: " + newName;
                    }
                }
            }
            String dt = trimString(row.get("dept_type"));
            if (dt != null && !"dept".equals(dt) && !"team".equals(dt) && !"temp".equals(dt)) {
                return "부서유형은 dept|team|temp 중 하나: " + dt;
            }
            String dateErr = validateDateRange(row);
            if (dateErr != null) return dateErr;
            String parent = trimString(row.get("parent_dept_code"));
            String parentErr = validateParentForBulk(parent, companyCode);
            if (parentErr != null) return parentErr;
        }
        return validateManagerIds(row, companyCode);
    }

    private String validateParentForBulk(String parent, String companyCode) {
        if (parent == null) return null;
        Map<String, Object> p = sqlSession.selectOne(
            "department.selectDepartmentByCodeIncludingDeleted", Map.of("dept_code", parent));
        if (p == null) return "상위 부서를 찾을 수 없습니다: " + parent;
        if (p.get("deleted_at") != null) return "삭제된 부서를 상위로 지정할 수 없음: " + parent;
        Object pc = p.get("company_code");
        if (pc == null || (!companyCode.equals(pc.toString()) && !"*".equals(pc.toString()))) {
            return "다른 회사의 부서를 상위로 지정 불가: " + parent;
        }
        return null;
    }

    private String validateDateRange(Map<String, Object> row) {
        String sd = trimString(row.get("start_date"));
        String ed = trimString(row.get("end_date"));
        if (sd != null && !sd.matches("\\d{4}-\\d{2}-\\d{2}")) return "시작일 형식 오류 (YYYY-MM-DD): " + sd;
        if (ed != null && !ed.matches("\\d{4}-\\d{2}-\\d{2}")) return "종료일 형식 오류 (YYYY-MM-DD): " + ed;
        if (sd != null && ed != null && sd.compareTo(ed) > 0) return "시작일이 종료일보다 늦을 수 없음.";
        return null;
    }

    private String validateManagerIds(Map<String, Object> row, String companyCode) {
        for (String key : new String[]{"approval_managers", "dept_managers", "org_leaders"}) {
            Object raw = row.get(key);
            if (raw instanceof List<?> list && list.size() > 10) {
                return key + " 는 최대 10명까지 등록 가능합니다.";
            }
        }
        List<String> ids = collectManagerUserIds(row);
        if (ids.isEmpty()) return null;
        Map<String, Object> vParams = new HashMap<>();
        vParams.put("user_ids", ids);
        vParams.put("company_code", companyCode);
        List<String> valid = sqlSession.selectList("department.selectValidUserIds", vParams);
        if (valid == null || valid.size() != ids.size()) {
            Set<String> invalid = new HashSet<>(ids);
            if (valid != null) invalid.removeAll(valid);
            return "유효하지 않은 사용자 ID: " + invalid;
        }
        return null;
    }

    private List<String> collectManagerUserIds(Map<String, Object> row) {
        List<String> ids = new ArrayList<>();
        for (String key : new String[]{"approval_managers", "dept_managers", "org_leaders"}) {
            Object raw = row.get(key);
            if (raw instanceof List<?> list) {
                for (Object item : list) {
                    String uid = null;
                    if (item instanceof Map<?, ?> m) {
                        Object v = m.get("user_id");
                        if (v != null) uid = v.toString().trim();
                    } else if (item != null) {
                        uid = item.toString().trim();
                    }
                    if (uid != null && !uid.isEmpty() && !ids.contains(uid)) ids.add(uid);
                }
            }
        }
        return ids;
    }

    /**
     * 일괄업데이트 department mode — 기존값 + row override 머지.
     * row 값이 null/미지정이면 기존값 보존 (PATCH semantic).
     * 매니저 매핑 키는 항상 제거 (department mode 에서는 안 다룸).
     */
    private Map<String, Object> buildMergedDeptBody(Map<String, Object> existing, Map<String, Object> row) {
        Map<String, Object> merged = new HashMap<>();
        String[] textKeys = {
            "dept_name", "parent_dept_code", "short_name", "dept_type", "org_system",
            "approval_manager", "dept_manager", "zipcode", "address1", "address2",
            "sort_order", "status", "location"
        };
        for (String k : textKeys) merged.put(k, existing.get(k));
        merged.put("start_date", stringifyDate(existing.get("start_date")));
        merged.put("end_date", stringifyDate(existing.get("end_date")));
        for (Map.Entry<String, Object> e : row.entrySet()) {
            String k = e.getKey();
            if ("dept_code".equals(k)) continue;
            if (e.getValue() == null) continue;
            if ("approval_managers".equals(k) || "dept_managers".equals(k) || "org_leaders".equals(k)) continue;
            merged.put(k, e.getValue());
        }
        return merged;
    }

    private String stringifyDate(Object date) {
        if (date == null) return null;
        String s = date.toString();
        return s.length() >= 10 ? s.substring(0, 10) : null;
    }

    // ──────────────────────────────────────────────────
    // 부서원 관리
    // ──────────────────────────────────────────────────

    public List<Map<String, Object>> getDeptMembers(String deptCode) {
        Map<String, Object> params = new HashMap<>();
        params.put("dept_code", deptCode);
        return sqlSession.selectList("department.selectDeptMembers", params);
    }

    public List<Map<String, Object>> searchUsers(String companyCode, String search) {
        Map<String, Object> params = new HashMap<>();
        params.put("company_code", companyCode);
        params.put("search", "%" + search + "%");
        return sqlSession.selectList("department.searchUsers", params);
    }

    @Transactional
    public void addDeptMember(String deptCode, String userId) {
        // 사용자 존재 확인
        Map<String, Object> userParams = new HashMap<>();
        userParams.put("user_id", userId);
        Map<String, Object> user = sqlSession.selectOne("department.selectUserById", userParams);
        if (user == null) {
            throw new IllegalArgumentException("사용자를 찾을 수 없습니다.");
        }

        // 이미 부서원인지 확인
        Map<String, Object> existParams = new HashMap<>();
        existParams.put("user_id", userId);
        existParams.put("dept_code", deptCode);
        Map<String, Object> existing = sqlSession.selectOne("department.selectExistingMember", existParams);
        if (existing != null) {
            throw new DuplicateMemberException("이미 해당 부서의 부서원입니다.");
        }

        // 주 부서가 있는지 확인
        Map<String, Object> primaryParams = new HashMap<>();
        primaryParams.put("user_id", userId);
        Map<String, Object> hasPrimary = sqlSession.selectOne("department.selectUserPrimaryDept", primaryParams);

        // 부서원 추가
        Map<String, Object> insertParams = new HashMap<>();
        insertParams.put("user_id", userId);
        insertParams.put("dept_code", deptCode);
        insertParams.put("is_primary", hasPrimary == null);
        sqlSession.insert("department.insertDeptMember", insertParams);

        log.info("부서원 추가 성공: userId={}, deptCode={}", userId, deptCode);
    }

    @Transactional
    public boolean removeDeptMember(String deptCode, String userId) {
        // 1. 제거 전 — 이 row 가 primary 였는지 확인
        Map<String, Object> existParams = new HashMap<>();
        existParams.put("user_id", userId);
        existParams.put("dept_code", deptCode);
        Map<String, Object> existing = sqlSession.selectOne("department.selectExistingMember", existParams);
        boolean wasPrimary = existing != null && Boolean.TRUE.equals(existing.get("is_primary"));

        // 2. 제거
        int deleted = sqlSession.delete("department.deleteDeptMember", existParams);
        if (deleted == 0) {
            return false;
        }

        // 3. primary 였으면 다른 USER_DEPT row 중 하나 promote
        if (wasPrimary) {
            Map<String, Object> remaining = sqlSession.selectOne("department.selectFirstUserDept",
                    Map.of("user_id", userId));
            if (remaining != null && remaining.get("dept_code") != null) {
                Map<String, Object> promote = new HashMap<>();
                promote.put("user_id", userId);
                promote.put("dept_code", remaining.get("dept_code").toString());
                sqlSession.update("department.setUserPrimaryDept", promote);
                log.info("주 부서 자동 승격: userId={}, newPrimaryDept={}", userId, remaining.get("dept_code"));
            }
        }

        log.info("부서원 제거 성공: userId={}, deptCode={}, wasPrimary={}", userId, deptCode, wasPrimary);
        return true;
    }

    @Transactional
    public void setPrimaryDept(String deptCode, String userId) {
        // 멤버십 검증 — 미소속 부서로 호출 시 데이터 손상 방지
        Map<String, Object> existParams = new HashMap<>();
        existParams.put("user_id", userId);
        existParams.put("dept_code", deptCode);
        Map<String, Object> existing = sqlSession.selectOne("department.selectExistingMember", existParams);
        if (existing == null) {
            throw new IllegalArgumentException("해당 부서의 부서원이 아닙니다. 먼저 부서원으로 추가해주세요.");
        }

        // 다른 부서의 주 부서 해제
        Map<String, Object> clearParams = new HashMap<>();
        clearParams.put("user_id", userId);
        sqlSession.update("department.clearUserPrimaryDept", clearParams);

        // 해당 부서를 주 부서로 설정
        Map<String, Object> setParams = new HashMap<>();
        setParams.put("user_id", userId);
        setParams.put("dept_code", deptCode);
        sqlSession.update("department.setUserPrimaryDept", setParams);

        log.info("주 부서 설정 성공: userId={}, deptCode={}", userId, deptCode);
    }

    // ──────────────────────────────────────────────────
    // 내부 유틸
    // ──────────────────────────────────────────────────

    private String trimString(Object value) {
        if (value == null) return null;
        String str = value.toString().trim();
        return str.isEmpty() ? null : str;
    }

    /** snake_case 우선, camelCase fallback으로 request body 파라미터 추출 */
    private Object bodyParam(Map<String, Object> body, String snakeCase, String camelCase) {
        Object val = body.get(snakeCase);
        return val != null ? val : body.get(camelCase);
    }

    /** 빈 문자열 또는 공백만 있는 문자열을 null 로 치환. 그 외엔 trim 한 값을 반환 */
    private Object nullIfBlank(Object value) {
        if (value == null) return null;
        if (value instanceof String s) {
            String trimmed = s.trim();
            return trimmed.isEmpty() ? null : trimmed;
        }
        return value;
    }

    // ── 관리자 매핑 sync ────────────────────────────────

    private static final com.fasterxml.jackson.databind.ObjectMapper JSON_MAPPER =
        new com.fasterxml.jackson.databind.ObjectMapper();

    private static final int MAX_MANAGERS_JSON_BYTES = 64 * 1024;

    private void parseManagersJson(Map<String, Object> dept, String key) {
        Object raw = dept.get(key);
        if (raw == null) {
            dept.put(key, new java.util.ArrayList<Map<String, Object>>());
            return;
        }
        String s = raw.toString();
        if (s.length() > MAX_MANAGERS_JSON_BYTES) {
            log.warn("parseManagersJson 크기 초과 dept_code={} key={} len={}",
                     dept.get("dept_code"), key, s.length());
            dept.put(key, new java.util.ArrayList<Map<String, Object>>());
            return;
        }
        try {
            @SuppressWarnings("unchecked")
            java.util.List<Map<String, Object>> parsed = JSON_MAPPER.readValue(s,
                new com.fasterxml.jackson.core.type.TypeReference<java.util.List<Map<String, Object>>>() {});
            dept.put(key, parsed);
        } catch (Exception e) {
            log.warn("parseManagersJson 실패 dept_code={} key={} err={}",
                     dept.get("dept_code"), key, e.getMessage());
            dept.put(key, new java.util.ArrayList<Map<String, Object>>());
        }
    }

    /**
     * 부서 관리자 role 단위 sync — 항상 delete-all + insert-all 패턴.
     * body 의 키는 (role 별): "approval_managers" / "dept_managers" / "org_leaders".
     * 각 값은 List&lt;Map&gt; 형태이며 각 element 에서 "user_id" 만 추출.
     * 최대 10명 검증 + 빈 user_id 무시.
     */
    private void syncManagers(String deptCode, String companyCode, Map<String, Object> body, String role) {
        String bodyKey = switch (role) {
            case "approval"   -> "approval_managers";
            case "dept"       -> "dept_managers";
            case "org_leader" -> "org_leaders";
            default -> throw new IllegalArgumentException("Unknown role: " + role);
        };
        // PUT partial update: 키가 명시적으로 존재할 때만 sync.
        // body 에 키 자체가 없으면 기존 매핑 보존 (partial update 의도).
        if (!body.containsKey(bodyKey)) {
            return;
        }
        Object raw = body.get(bodyKey);
        java.util.List<String> userIds = new java.util.ArrayList<>();
        if (raw instanceof java.util.List<?> list) {
            for (Object item : list) {
                String uid = null;
                if (item instanceof Map<?, ?> m) {
                    Object v = m.get("user_id");
                    if (v != null) uid = v.toString().trim();
                } else if (item != null) {
                    uid = item.toString().trim();
                }
                if (uid != null && !uid.isEmpty() && !userIds.contains(uid)) {
                    userIds.add(uid);
                }
            }
        }
        if (userIds.size() > 10) {
            String roleLabel = switch (role) {
                case "approval"   -> "결재 관리자";
                case "dept"       -> "부서 관리자";
                case "org_leader" -> "조직장";
                default -> role;
            };
            throw new IllegalArgumentException(roleLabel + " 는 최대 10명까지 등록 가능합니다.");
        }
        // user_id 가 같은 회사 (or '*') 에 실존하는지 검증 — cross-tenant 차단
        if (!userIds.isEmpty()) {
            Map<String, Object> vParams = new HashMap<>();
            vParams.put("user_ids", userIds);
            vParams.put("company_code", companyCode);
            List<String> validUserIds = sqlSession.selectList("department.selectValidUserIds", vParams);
            if (validUserIds == null || validUserIds.size() != userIds.size()) {
                Set<String> invalid = new HashSet<>(userIds);
                if (validUserIds != null) invalid.removeAll(validUserIds);
                throw new IllegalArgumentException("유효하지 않은 사용자 ID: " + invalid);
            }
        }
        // delete-all
        Map<String, Object> delParams = new HashMap<>();
        delParams.put("dept_code", deptCode);
        delParams.put("role", role);
        sqlSession.delete("department.deleteDeptManagersByDeptAndRole", delParams);
        // insert-all
        if (!userIds.isEmpty()) {
            Map<String, Object> insParams = new HashMap<>();
            insParams.put("dept_code", deptCode);
            insParams.put("role", role);
            insParams.put("user_ids", userIds);
            sqlSession.insert("department.insertDeptManagers", insParams);
        }
    }

    // ── 중복 예외 클래스 ────────────────────────────────

    public static class DuplicateDeptNameException extends RuntimeException {
        public DuplicateDeptNameException(String message) {
            super(message);
        }
    }

    public static class DuplicateMemberException extends RuntimeException {
        public DuplicateMemberException(String message) {
            super(message);
        }
    }
}
