package com.erp.service;

import com.erp.common.BaseService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;

@Service
@Slf4j
public class ApprovalService extends BaseService {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private AuditLogService auditLogService;

    /**
     * IN (:effective_user_ids) 쿼리용 fallback.
     * SubstituteContextFilter 가 attribute 를 못 채운 경로(통합 테스트/배치 등) 에서도
     * 빈 IN () SQL 에러를 막기 위해 항상 최소 [user_id] 가 들어가도록 한다.
     */
    @SuppressWarnings("unchecked")
    private void ensureEffectiveUserIds(Map<String, Object> params) {
        Object v = params.get("effective_user_ids");
        boolean empty = v == null || (v instanceof Collection<?> && ((Collection<?>) v).isEmpty());
        if (empty) {
            Object userId = params.get("user_id");
            if (userId != null) {
                params.put("effective_user_ids", List.of(userId));
            }
        }
    }

    // ═══════════════════════════════════════════════════════════════
    // approval_definitions
    // ═══════════════════════════════════════════════════════════════

    public List<Map<String, Object>> getDefinitions(Map<String, Object> params) {
        Object search = params.get("search");
        if (search != null && !search.toString().isBlank()) {
            params.put("search_like", "%" + search + "%");
        }
        return sqlSession.selectList("approval.selectDefinitions", params);
    }

    public Map<String, Object> getDefinition(Map<String, Object> params) {
        Map<String, Object> result = sqlSession.selectOne("approval.selectDefinitionById", params);
        if (result == null) throw new IllegalArgumentException("결재 유형을 찾을 수 없습니다.");
        return result;
    }

    @Transactional
    public Map<String, Object> createDefinition(Map<String, Object> params) {
        sqlSession.insert("approval.insertDefinition", params);
        Map<String, Object> fetch = new HashMap<>();
        fetch.put("definition_id", params.get("definition_id"));
        fetch.put("company_code", "*");
        return sqlSession.selectOne("approval.selectDefinitionById", fetch);
    }

    @Transactional
    public Map<String, Object> updateDefinition(Map<String, Object> params) {
        Map<String, Object> checkP = new HashMap<>();
        checkP.put("definition_id", params.get("definition_id"));
        checkP.put("company_code",  params.get("company_code"));
        if (sqlSession.selectOne("approval.selectDefinitionById", checkP) == null)
            throw new IllegalArgumentException("결재 유형을 찾을 수 없습니다.");
        sqlSession.update("approval.updateDefinition", params);
        checkP.put("company_code", "*");
        return sqlSession.selectOne("approval.selectDefinitionById", checkP);
    }

    @Transactional
    public void deleteDefinition(Map<String, Object> params) {
        Map<String, Object> checkP = new HashMap<>();
        checkP.put("definition_id", params.get("definition_id"));
        checkP.put("company_code",  params.get("company_code"));
        if (sqlSession.selectOne("approval.selectDefinitionById", checkP) == null)
            throw new IllegalArgumentException("결재 유형을 찾을 수 없습니다.");
        sqlSession.delete("approval.deleteDefinition", params);
    }

    // ═══════════════════════════════════════════════════════════════
    // approval_line_templates
    // ═══════════════════════════════════════════════════════════════

    public List<Map<String, Object>> getTemplates(Map<String, Object> params) {
        return sqlSession.selectList("approval.selectTemplates", params);
    }

    public Map<String, Object> getTemplate(Map<String, Object> params) {
        Map<String, Object> template = sqlSession.selectOne("approval.selectTemplateById", params);
        if (template == null) throw new IllegalArgumentException("결재선 템플릿을 찾을 수 없습니다.");

        Map<String, Object> stepP = new HashMap<>();
        stepP.put("template_id",  template.get("template_id"));
        stepP.put("company_code", params.get("company_code"));
        template.put("steps", sqlSession.selectList("approval.selectTemplateSteps", stepP));
        return template;
    }

    @Transactional
    public Map<String, Object> createTemplate(Map<String, Object> params) {
        sqlSession.insert("approval.insertTemplate", params);
        long templateId = toLong(params.get("template_id"));
        List<Map<String, Object>> steps = castList(params.get("steps"));
        if (steps != null) insertTemplateSteps(templateId, steps, (String) params.get("company_code"));

        Map<String, Object> fetch = new HashMap<>();
        fetch.put("template_id",  templateId);
        fetch.put("company_code", "*");
        return sqlSession.selectOne("approval.selectTemplateById", fetch);
    }

    @Transactional
    public Map<String, Object> updateTemplate(Map<String, Object> params) {
        Map<String, Object> checkP = new HashMap<>();
        checkP.put("template_id",  params.get("template_id"));
        checkP.put("company_code", params.get("company_code"));
        if (sqlSession.selectOne("approval.selectTemplateById", checkP) == null)
            throw new IllegalArgumentException("결재선 템플릿을 찾을 수 없습니다.");

        sqlSession.update("approval.updateTemplate", params);

        List<Map<String, Object>> steps = castList(params.get("steps"));
        if (steps != null) {
            String cc       = (String) params.get("company_code");
            long   templateId = toLong(params.get("template_id"));
            Map<String, Object> delP = new HashMap<>();
            delP.put("template_id",  templateId);
            delP.put("company_code", cc);
            sqlSession.delete("approval.deleteTemplateStepsByTemplateId", delP);
            insertTemplateSteps(templateId, steps, cc);
        }

        Map<String, Object> fetch = new HashMap<>();
        fetch.put("template_id",  params.get("template_id"));
        fetch.put("company_code", "*");
        return sqlSession.selectOne("approval.selectTemplateById", fetch);
    }

    @Transactional
    public void deleteTemplate(Map<String, Object> params) {
        Map<String, Object> checkP = new HashMap<>();
        checkP.put("template_id",  params.get("template_id"));
        checkP.put("company_code", params.get("company_code"));
        if (sqlSession.selectOne("approval.selectTemplateById", checkP) == null)
            throw new IllegalArgumentException("결재선 템플릿을 찾을 수 없습니다.");
        sqlSession.delete("approval.deleteTemplate", params);
    }

    private void insertTemplateSteps(long templateId, List<Map<String, Object>> steps, String companyCode) {
        for (Map<String, Object> step : steps) {
            Map<String, Object> sp = new HashMap<>(step);
            sp.put("template_id",  templateId);
            sp.put("company_code", companyCode);
            sqlSession.insert("approval.insertTemplateStep", sp);
        }
    }

    // ═══════════════════════════════════════════════════════════════
    // approval_requests
    // ═══════════════════════════════════════════════════════════════

    public Map<String, Object> getRequests(Map<String, Object> params) {
        ensureEffectiveUserIds(params);
        int page   = toInt(params.getOrDefault("page",  "1"));
        int limit  = toInt(params.getOrDefault("limit", "20"));
        params.put("page_limit",  limit);
        params.put("page_offset", (page - 1) * limit);

        List<Map<String, Object>> data  = sqlSession.selectList("approval.selectRequests", params);
        Number totalNum = sqlSession.selectOne("approval.countRequests", params);
        int total = totalNum != null ? totalNum.intValue() : 0;

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("data",  data);
        result.put("total", total);
        result.put("page",  page);
        result.put("limit", limit);
        return result;
    }

    public Map<String, Object> getRequest(Map<String, Object> params) {
        Map<String, Object> request = sqlSession.selectOne("approval.selectRequestById", params);
        if (request == null) throw new IllegalArgumentException("결재 요청을 찾을 수 없습니다.");

        Map<String, Object> lineP = new HashMap<>();
        lineP.put("request_id",  request.get("request_id"));
        lineP.put("company_code", params.get("company_code"));
        request.put("lines", sqlSession.selectList("approval.selectLinesByRequestId", lineP));
        return request;
    }

    @Transactional
    public Map<String, Object> createRequest(Map<String, Object> params) {
        String companyCode  = (String) params.get("company_code");
        String userId       = (String) params.get("user_id");
        String userName     = (String) params.getOrDefault("requester_name", userId);
        String deptName     = (String) params.getOrDefault("requester_dept", "");
        String approvalType = (String) params.getOrDefault("approval_type", "escalation");

        String title       = (String) params.get("title");
        String targetTable = (String) params.get("target_table");
        if (title == null || targetTable == null)
            throw new IllegalArgumentException("제목과 대상 테이블은 필수입니다.");

        String safeRecordId = params.get("target_record_id") != null
                ? params.get("target_record_id").toString() : "0";

        // target_record_data JSON 직렬화
        Map<String, Object> recordData = new HashMap<>();
        Object trd = params.get("target_record_data");
        if (trd instanceof Map) recordData.putAll((Map<String, Object>) trd);
        recordData.put("approval_mode", params.getOrDefault("approval_mode", "sequential"));
        String recordDataJson = toJson(recordData);

        // ── 자기결재(전결) ──────────────────────────────────────────
        if ("self".equals(approvalType)) {
            Object defId = params.get("definition_id");
            if (defId != null) {
                Map<String, Object> defP = new HashMap<>();
                defP.put("definition_id", defId);
                defP.put("company_code",  companyCode);
                Map<String, Object> def = sqlSession.selectOne("approval.selectDefinitionById", defP);
                if (def != null) {
                    Object allow = def.get("allow_self_approval");
                    boolean allowed = allow == null || Boolean.TRUE.equals(allow)
                            || "true".equalsIgnoreCase(allow.toString());
                    if (!allowed)
                        throw new IllegalArgumentException("해당 결재 유형은 자기결재(전결)를 허용하지 않습니다.");
                }
            }
            Map<String, Object> insertP = buildRequestInsertParams(params, "approved", 1, "self",
                    userId, userName, deptName, safeRecordId, recordDataJson, companyCode);
            insertP.put("final_approver_id", userId);
            sqlSession.insert("approval.insertRequest", insertP);
            long requestId = toLong(insertP.get("request_id"));

            Map<String, Object> lineP = buildLineInsertParams(requestId, 1, userId, userName,
                    null, deptName, "자기결재", "approved", "approval", true, companyCode);
            sqlSession.insert("approval.insertLine", lineP);
            syncApprovalStatusToTarget(requestId, "approved", companyCode);
            return fetchRequest(requestId);
        }

        // ── 일반 결재 ─────────────────────────────────────────────
        List<Map<String, Object>> approvers = castList(params.get("approvers"));
        if (approvers == null || approvers.isEmpty())
            throw new IllegalArgumentException("결재자를 1명 이상 지정해야 합니다.");

        boolean hasExplicitStepType = approvers.stream().anyMatch(a -> a.get("step_type") != null);
        List<Map<String, Object>> normalized = normalizeApprovers(approvers, approvalType, hasExplicitStepType);

        // 같은 step_order에 approval 타입 2명 이상 방지
        Map<Integer, List<Map<String, Object>>> groups = new LinkedHashMap<>();
        for (Map<String, Object> a : normalized) {
            int so = toInt(a.get("step_order"));
            groups.computeIfAbsent(so, k -> new ArrayList<>()).add(a);
        }
        for (Map.Entry<Integer, List<Map<String, Object>>> e : groups.entrySet()) {
            List<Map<String, Object>> g = e.getValue();
            if (g.size() > 1 && g.stream().allMatch(a -> "approval".equals(a.get("step_type"))))
                throw new IllegalArgumentException(
                        "step_order " + e.getKey() + "에 approval 타입 결재자가 2명 이상입니다. consensus로 지정해주세요.");
        }

        int totalSteps      = normalized.stream().mapToInt(a -> toInt(a.get("step_order"))).max().orElse(1);
        String storedType   = hasExplicitStepType ? "escalation" : approvalType;
        String initialStatus = "post".equals(approvalType) ? "post_pending" : "requested";

        Map<String, Object> insertP = buildRequestInsertParams(params, initialStatus, totalSteps, storedType,
                userId, userName, deptName, safeRecordId, recordDataJson, companyCode);
        sqlSession.insert("approval.insertRequest", insertP);
        long requestId = toLong(insertP.get("request_id"));

        List<Integer> uniqueSteps = normalized.stream()
                .map(a -> toInt(a.get("step_order"))).distinct().sorted().collect(Collectors.toList());
        int firstStep = uniqueSteps.get(0);

        for (Map<String, Object> approver : normalized) {
            int so     = toInt(approver.get("step_order"));
            String ls  = (so == firstStep) ? "pending" : "waiting";
            Map<String, Object> lp = buildLineInsertParams(requestId, so,
                    (String) approver.get("approver_id"), (String) approver.get("approver_name"),
                    (String) approver.get("approver_position"), (String) approver.get("approver_dept"),
                    (String) approver.get("approver_label"), ls, (String) approver.get("step_type"),
                    false, companyCode);
            sqlSession.insert("approval.insertLine", lp);
        }

        // 첫 step이 notification이면 자동 통과
        String firstStepType = normalized.stream()
                .filter(a -> toInt(a.get("step_order")) == firstStep)
                .map(a -> (String) a.get("step_type"))
                .findFirst().orElse("approval");

        if ("notification".equals(firstStepType)) {
            Map<String, Object> notifP = new HashMap<>();
            notifP.put("request_id",  requestId);
            notifP.put("step_order",  firstStep);
            notifP.put("company_code", companyCode);
            sqlSession.update("approval.approveNotificationLines", notifP);
            activateNextStep(requestId, firstStep, totalSteps, companyCode, userId, null);
        }

        if (!"post".equals(approvalType)) {
            Map<String, Object> statusP = new HashMap<>();
            statusP.put("request_id",  requestId);
            statusP.put("status",     "in_progress");
            statusP.put("company_code", companyCode);
            sqlSession.update("approval.updateRequestStatus", statusP);
            syncApprovalStatusToTarget(requestId, "in_progress", companyCode);
        } else {
            syncApprovalStatusToTarget(requestId, "post_pending", companyCode);
        }

        return fetchRequest(requestId);
    }

    @Transactional
    public void cancelRequest(Map<String, Object> params) {
        long   requestId   = toLong(params.get("request_id"));
        String companyCode = (String) params.get("company_code");
        String userId      = (String) params.get("user_id");

        Map<String, Object> reqP = new HashMap<>();
        reqP.put("request_id",  requestId);
        reqP.put("company_code", companyCode);
        Map<String, Object> request = sqlSession.selectOne("approval.selectRequestById", reqP);
        if (request == null) throw new IllegalArgumentException("결재 요청을 찾을 수 없습니다.");
        if (!userId.equals(String.valueOf(request.get("requester_id"))))
            throw new IllegalArgumentException("본인이 요청한 건만 회수할 수 있습니다.");
        String status = (String) request.get("status");
        if (!List.of("requested", "in_progress", "post_pending").contains(status))
            throw new IllegalArgumentException("이미 처리된 결재 요청은 회수할 수 없습니다.");

        sqlSession.update("approval.cancelRequest", reqP);
        syncApprovalStatusToTarget(requestId, "cancelled", companyCode);
    }

    @Transactional
    public void postApprove(Map<String, Object> params) {
        long   requestId   = toLong(params.get("request_id"));
        String companyCode = (String) params.get("company_code");
        String userId      = (String) params.get("user_id");
        String comment     = (String) params.get("comment");

        Map<String, Object> reqP = new HashMap<>();
        reqP.put("request_id",  requestId);
        reqP.put("company_code", companyCode);
        Map<String, Object> request = sqlSession.selectOne("approval.selectRequestById", reqP);
        if (request == null) throw new IllegalArgumentException("결재 요청을 찾을 수 없습니다.");
        if (!"post".equals(request.get("approval_type")))
            throw new IllegalArgumentException("후결 유형의 결재 요청만 후결 처리할 수 있습니다.");
        if (Boolean.TRUE.equals(request.get("is_post_approved")))
            throw new IllegalArgumentException("이미 후결 처리된 요청입니다.");

        Number pendingNum = sqlSession.selectOne("approval.countPendingLinesForRequest", reqP);
        int pending = pendingNum != null ? pendingNum.intValue() : 0;
        if (pending > 0) throw new IllegalArgumentException("모든 결재자의 승인이 완료되지 않았습니다.");

        Map<String, Object> p = new HashMap<>();
        p.put("request_id",  requestId);
        p.put("company_code", companyCode);
        p.put("user_id",     userId);
        p.put("comment",    comment);
        sqlSession.update("approval.postApproveRequest", p);
        syncApprovalStatusToTarget(requestId, "approved", companyCode);
    }

    // ═══════════════════════════════════════════════════════════════
    // approval_lines
    // ═══════════════════════════════════════════════════════════════

    public List<Map<String, Object>> getMyPendingLines(Map<String, Object> params) {
        ensureEffectiveUserIds(params);
        return sqlSession.selectList("approval.selectMyPendingLines", params);
    }

    @Transactional
    public void processApproval(Map<String, Object> params) {
        String companyCode  = (String) params.get("company_code");
        String userId       = (String) params.get("user_id");
        long   lineId       = toLong(params.get("line_id"));
        String action       = (String) params.get("action");
        String comment      = (String) params.get("comment");
        String proxyReason  = (String) params.get("proxy_reason");

        Map<String, Object> lockP = new HashMap<>();
        lockP.put("line_id",     lineId);
        lockP.put("company_code", companyCode);
        Map<String, Object> line = sqlSession.selectOne("approval.selectLineByIdForUpdate", lockP);
        if (line == null)           throw new IllegalArgumentException("결재 라인을 찾을 수 없습니다.");
        if (!"pending".equals(line.get("status"))) throw new IllegalArgumentException("대기 중인 결재만 처리할 수 있습니다.");

        String lineCC     = (String) line.get("company_code");
        String approverId = (String) line.get("approver_id");
        long   requestId  = toLong(line.get("request_id"));
        int    stepOrder  = toInt(line.get("step_order"));

        String proxyFor    = null;
        String proxyReasonVal = null;
        if (!userId.equals(approverId)) {
            if ("*".equals(companyCode)) {
                proxyFor      = approverId;
                proxyReasonVal = proxyReason != null ? proxyReason : "최고관리자 대리 처리";
            } else {
                Map<String, Object> proxyP = new HashMap<>();
                proxyP.put("original_user_id", approverId);
                proxyP.put("proxy_user_id",    userId);
                proxyP.put("company_code",    lineCC);
                Map<String, Object> proxy = sqlSession.selectOne("approval.selectActiveProxyForLine", proxyP);
                if (proxy == null) throw new IllegalArgumentException("본인이 결재자로 지정된 건만 처리할 수 있습니다.");
                proxyFor      = approverId;
                proxyReasonVal = proxyReason != null ? proxyReason
                        : (String) proxy.getOrDefault("reason", "대결 처리");
            }
        }

        // 현재 라인 업데이트
        Map<String, Object> updLineP = new HashMap<>();
        updLineP.put("line_id",      lineId);
        updLineP.put("status",      action);
        updLineP.put("comment",     comment);
        updLineP.put("proxy_for",    proxyFor);
        updLineP.put("proxy_reason", proxyReasonVal);
        updLineP.put("company_code", lineCC);
        sqlSession.update("approval.updateLine", updLineP);

        Map<String, Object> reqForUpdate = sqlSession.selectOne("approval.selectRequestByIdForUpdate",
                Map.of("request_id", requestId, "company_code", lineCC));
        if (reqForUpdate == null) return;

        int    totalSteps = toInt(reqForUpdate.get("total_steps"));
        String stepType   = (String) line.getOrDefault("step_type", "approval");
        boolean isLegacy  = isLegacyParallel(reqForUpdate.get("target_record_data"), stepType);

        if ("rejected".equals(action)) {
            Map<String, Object> rejP = new HashMap<>();
            rejP.put("request_id",  requestId);
            rejP.put("company_code", lineCC);
            rejP.put("user_id",     userId);
            rejP.put("comment",    comment);
            sqlSession.update("approval.rejectRequest", rejP);
            Map<String, Object> skipP = new HashMap<>();
            skipP.put("request_id",  requestId);
            skipP.put("line_id",     lineId);
            skipP.put("company_code", lineCC);
            sqlSession.update("approval.skipRemainingLines", skipP);
            syncApprovalStatusToTarget(requestId, "rejected", lineCC);
        } else {
            if (isLegacy) {
                Number remainingNum = sqlSession.selectOne("approval.countRemainingLinesInStep",
                        Map.of("request_id", requestId, "step_order", stepOrder, "line_id", lineId, "company_code", lineCC));
                int remaining = remainingNum != null ? remainingNum.intValue() : 0;
                if (remaining == 0) {
                    Map<String, Object> complP = new HashMap<>();
                    complP.put("request_id",  requestId);
                    complP.put("company_code", lineCC);
                    complP.put("user_id",     userId);
                    complP.put("comment",    comment);
                    sqlSession.update("approval.completeRequest", complP);
                    syncApprovalStatusToTarget(requestId, "approved", lineCC);
                }
            } else if ("consensus".equals(stepType)) {
                Number remainingNum = sqlSession.selectOne("approval.countRemainingLinesInStep",
                        Map.of("request_id", requestId, "step_order", stepOrder, "line_id", lineId, "company_code", lineCC));
                int remaining = remainingNum != null ? remainingNum.intValue() : 0;
                if (remaining == 0) activateNextStep(requestId, stepOrder, totalSteps, lineCC, userId, comment);
            } else {
                activateNextStep(requestId, stepOrder, totalSteps, lineCC, userId, comment);
            }
        }

        // 결재 처리 audit log — 대무 시 user_id(A)와 processor_id(B) 분리 기록.
        // 실패는 본 처리를 막지 않음 (가용성 우선).
        try {
            Map<String, Object> auditP = new HashMap<>();
            auditP.put("company_code", lineCC);
            auditP.put("user_id",      approverId);              // 위임자 A
            auditP.put("user_name",    line.get("approver_name"));
            auditP.put("processor_id", userId);                  // 실제 처리자 B
            // processor_name 은 AuditLogService 가 USER_INFO 에서 lookup (T14)
            auditP.put("action",        "approval." + action);
            auditP.put("resource_type", "approval_line");
            auditP.put("resource_id",   String.valueOf(lineId));
            auditP.put("summary",       "결재 " + action + (proxyFor != null ? " (대무)" : ""));
            auditLogService.insertAuditLog(auditP);
        } catch (Exception e) {
            log.warn("결재 audit log 기록 실패 (line={}): {}", lineId, e.getMessage());
        }
    }

    // ═══════════════════════════════════════════════════════════════
    // approval_proxy_settings
    // ═══════════════════════════════════════════════════════════════

    public List<Map<String, Object>> getProxySettings(Map<String, Object> params) {
        return sqlSession.selectList("approval.selectProxySettings", params);
    }

    @Transactional
    public Map<String, Object> createProxySetting(Map<String, Object> params) {
        String origUser  = (String) params.get("original_user_id");
        String proxyUser = (String) params.get("proxy_user_id");
        if (origUser == null || proxyUser == null)
            throw new IllegalArgumentException("위임자와 대결자는 필수입니다.");
        if (params.get("start_date") == null || params.get("end_date") == null)
            throw new IllegalArgumentException("시작일과 종료일은 필수입니다.");
        if (origUser.equals(proxyUser))
            throw new IllegalArgumentException("위임자와 대결자가 동일할 수 없습니다.");

        Number overlapNum = sqlSession.selectOne("approval.countProxyOverlap", params);
        int overlap = overlapNum != null ? overlapNum.intValue() : 0;
        if (overlap > 0) throw new IllegalArgumentException("해당 기간에 이미 대결 설정이 존재합니다.");

        sqlSession.insert("approval.insertProxySetting", params);
        Map<String, Object> fetch = new HashMap<>();
        fetch.put("id",          params.get("id"));
        fetch.put("company_code", params.get("company_code"));
        return sqlSession.selectOne("approval.selectProxyById", fetch);
    }

    @Transactional
    public Map<String, Object> updateProxySetting(Map<String, Object> params) {
        Map<String, Object> checkP = new HashMap<>();
        checkP.put("id",          params.get("id"));
        checkP.put("company_code", params.get("company_code"));
        if (sqlSession.selectOne("approval.selectProxyById", checkP) == null)
            throw new IllegalArgumentException("대결 위임 설정을 찾을 수 없습니다.");
        sqlSession.update("approval.updateProxySetting", params);
        return sqlSession.selectOne("approval.selectProxyById", checkP);
    }

    @Transactional
    public void deleteProxySetting(Map<String, Object> params) {
        int deleted = sqlSession.delete("approval.deleteProxySetting", params);
        if (deleted == 0) throw new IllegalArgumentException("대결 위임 설정을 찾을 수 없습니다.");
    }

    public List<Map<String, Object>> checkActiveProxy(Map<String, Object> params) {
        return sqlSession.selectList("approval.selectActiveProxy", params);
    }

    // ═══════════════════════════════════════════════════════════════
    // private helpers
    // ═══════════════════════════════════════════════════════════════

    private void activateNextStep(long requestId, int currentStep, int totalSteps,
                                  String companyCode, String userId, String comment) {
        int nextStep = currentStep + 1;
        Map<String, Object> complP = new HashMap<>();
        complP.put("request_id",  requestId);
        complP.put("company_code", companyCode);
        complP.put("user_id",     userId);
        complP.put("comment",    comment);

        if (nextStep > totalSteps) {
            sqlSession.update("approval.completeRequest", complP);
            syncApprovalStatusToTarget(requestId, "approved", companyCode);
            return;
        }

        List<Map<String, Object>> nextLines = sqlSession.selectList("approval.selectLinesForStep",
                Map.of("request_id", requestId, "step_order", nextStep, "company_code", companyCode));

        if (nextLines.isEmpty()) {
            sqlSession.update("approval.completeRequest", complP);
            syncApprovalStatusToTarget(requestId, "approved", companyCode);
            return;
        }

        String nextStepType = (String) nextLines.get(0).getOrDefault("step_type", "approval");

        if ("notification".equals(nextStepType)) {
            Map<String, Object> notifP = new HashMap<>();
            notifP.put("request_id",  requestId);
            notifP.put("step_order",  nextStep);
            notifP.put("company_code", companyCode);
            sqlSession.update("approval.approveNotificationLines", notifP);
            Map<String, Object> stepP = new HashMap<>();
            stepP.put("request_id",  requestId);
            stepP.put("next_step",   nextStep);
            stepP.put("company_code", companyCode);
            sqlSession.update("approval.updateRequestCurrentStep", stepP);
            activateNextStep(requestId, nextStep, totalSteps, companyCode, userId, comment);
        } else {
            Map<String, Object> actP = new HashMap<>();
            actP.put("request_id",  requestId);
            actP.put("next_step",   nextStep);
            actP.put("company_code", companyCode);
            sqlSession.update("approval.activateNextStepLines", actP);
            sqlSession.update("approval.updateRequestCurrentStep", actP);
        }
    }

    private void syncApprovalStatusToTarget(long requestId, String newStatus, String companyCode) {
        try {
            Map<String, Object> syncP = new HashMap<>();
            syncP.put("request_id",  requestId);
            syncP.put("company_code", companyCode);
            Map<String, Object> req = sqlSession.selectOne("approval.selectRequestForSync", syncP);
            if (req == null) return;

            String targetTable    = (String) req.get("target_table");
            Object targetRecordId = req.get("target_record_id");
            if (targetTable == null || targetRecordId == null
                    || "0".equals(targetRecordId.toString())
                    || targetRecordId.toString().isBlank()) return;

            String safeTable = targetTable.replaceAll("[^a-zA-Z0-9_]", "");
            if (safeTable.isEmpty()) return;

            Number hasColNum = sqlSession.selectOne("approval.checkTargetTableHasApprovalStatusColumn",
                    Map.of("table_name", safeTable));
            int hasCol = hasColNum != null ? hasColNum.intValue() : 0;
            if (hasCol == 0) return;

            Map<String, String> statusMap = new HashMap<>();
            statusMap.put("in_progress",  "결재중");
            statusMap.put("approved",     "결재완료");
            statusMap.put("rejected",     "반려");
            statusMap.put("cancelled",    "작성중");
            statusMap.put("draft",        "작성중");
            statusMap.put("post_pending", "후결대기");
            String businessStatus = statusMap.getOrDefault(newStatus, newStatus);

            Map<String, Object> updP = new HashMap<>();
            updP.put("table_name",      safeTable);
            updP.put("approval_status", businessStatus);
            updP.put("target_record_id", targetRecordId);
            updP.put("company_code",    companyCode);
            sqlSession.update("approval.updateTargetTableApprovalStatus", updP);

            if ("approved".equals(newStatus))
                log.info("[NodeFlowExecutionService] 결재 완료 후 플로우 실행 미구현. requestId={}", requestId);
        } catch (Exception e) {
            log.error("[syncApprovalStatusToTarget] 동기화 실패: requestId={}", requestId, e);
        }
    }

    private List<Map<String, Object>> normalizeApprovers(
            List<Map<String, Object>> approvers, String approvalType, boolean hasExplicitStepType) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (int i = 0; i < approvers.size(); i++) {
            Map<String, Object> a    = approvers.get(i);
            Map<String, Object> norm = new HashMap<>();
            norm.put("approver_id",       a.get("approver_id"));
            norm.put("approver_name",     a.get("approver_name"));
            norm.put("approver_position", a.get("approver_position"));
            norm.put("approver_dept",     a.get("approver_dept"));

            if ("consensus".equals(approvalType) && !hasExplicitStepType) {
                norm.put("approver_label", a.getOrDefault("approver_label", "합의 결재"));
                norm.put("step_order", 1);
                norm.put("step_type",  "consensus");
            } else if (hasExplicitStepType) {
                norm.put("approver_label", a.get("approver_label"));
                Object so = a.get("step_order");
                norm.put("step_order", so != null ? toInt(so) : i + 1);
                norm.put("step_type",  a.getOrDefault("step_type", "approval"));
            } else {
                Object label = a.get("approver_label");
                norm.put("approver_label", label != null ? label : (i + 1) + "차 결재");
                Object so = a.get("step_order");
                norm.put("step_order", so != null ? toInt(so) : i + 1);
                norm.put("step_type", "approval");
            }
            result.add(norm);
        }
        return result;
    }

    private boolean isLegacyParallel(Object recordData, String stepType) {
        if (stepType != null && !stepType.isEmpty()) return false;
        if (recordData instanceof Map<?, ?> m) return "parallel".equals(m.get("approval_mode"));
        if (recordData instanceof String s) return s.contains("\"approval_mode\":\"parallel\"");
        return false;
    }

    private Map<String, Object> fetchRequest(long requestId) {
        Map<String, Object> p = new HashMap<>();
        p.put("request_id",  requestId);
        p.put("company_code", "*");
        return sqlSession.selectOne("approval.selectRequestById", p);
    }

    private Map<String, Object> buildRequestInsertParams(
            Map<String, Object> src, String status, int totalSteps, String approvalType,
            String userId, String userName, String deptName,
            String safeRecordId, String recordDataJson, String companyCode) {
        Map<String, Object> p = new HashMap<>();
        p.put("title",             src.get("title"));
        p.put("description",       src.get("description"));
        p.put("definition_id",      src.get("definition_id"));
        p.put("template_id",        src.get("template_id"));
        p.put("target_table",       src.get("target_table"));
        p.put("target_record_id",    safeRecordId);
        p.put("target_record_data",  recordDataJson);
        p.put("status",            status);
        p.put("total_steps",        totalSteps);
        p.put("approval_type",      approvalType);
        p.put("requester_id",       userId);
        p.put("requester_name",     userName);
        p.put("requester_dept",     deptName);
        p.put("screen_id",          src.get("screen_id"));
        p.put("button_component_id", src.get("button_component_id"));
        p.put("company_code",       companyCode);
        return p;
    }

    private Map<String, Object> buildLineInsertParams(
            long requestId, int stepOrder, String approverId, String approverName,
            String approverPosition, String approverDept, String approverLabel,
            String status, String stepType, boolean alreadyProcessed, String companyCode) {
        Map<String, Object> p = new HashMap<>();
        p.put("request_id",        requestId);
        p.put("step_order",        stepOrder);
        p.put("approver_id",       approverId);
        p.put("approver_name",     approverName);
        p.put("approver_position", approverPosition);
        p.put("approver_dept",     approverDept);
        p.put("approver_label",    approverLabel);
        p.put("status",           status);
        p.put("step_type",         stepType);
        p.put("processed_at",      alreadyProcessed ? Boolean.TRUE : null);
        p.put("company_code",      companyCode);
        return p;
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> castList(Object obj) {
        if (obj instanceof List) return (List<Map<String, Object>>) obj;
        return null;
    }

    private long toLong(Object val) {
        if (val == null) return 0L;
        if (val instanceof Number) return ((Number) val).longValue();
        return Long.parseLong(val.toString());
    }

    private int toInt(Object val) {
        if (val == null) return 0;
        if (val instanceof Number) return ((Number) val).intValue();
        try { return Integer.parseInt(val.toString()); }
        catch (NumberFormatException e) { return 0; }
    }

    private String toJson(Object obj) {
        try { return objectMapper.writeValueAsString(obj); }
        catch (Exception e) { return "{}"; }
    }
}
