package com.bizcare.agent;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 접수관리 — billing_react /order(전체 리스트) 기능을 Care Homes 에 이식.
 */
@RestController
@RequestMapping("/api/agent/homes")
@RequiredArgsConstructor
class CareReceptionController {
    private final EntityManager em;
    private final FileStorageService fileStorage;

    static final List<String> PROGRESS_STATUSES = List.of(
            "1.접수완료", "2.고객통화", "3.서류검토", "4.개통완료", "5.사은품지급", "6.정산완료", "9.취소");
    static final List<String> SETTLE_STATUSES = List.of("정산대기", "정산완료", "회수대기", "회수완료");
    static final List<String> GIFT_STATUSES = List.of("미지급", "지급대기", "지급완료", "반송");
    static final List<String> CARRIERS = List.of("KT", "SKB", "LGU+", "SKT", "기타");
    static final List<String> REGIONS = List.of(
            "서울", "경기", "인천", "강원", "충북", "충남", "대전", "전북", "전남", "광주",
            "경북", "대구", "경남", "울산", "부산", "제주");

    @GetMapping("/receptions")
    ApiResponse list(@AuthenticationPrincipal JwtUser u,
                     @RequestParam Map<String, String> f,
                     @RequestParam(defaultValue = "0") int page,
                     @RequestParam(defaultValue = "50") int size) {
        List<HomesReception> filtered = filterReceptions(u, f);
        Map<String, Long> counts = new LinkedHashMap<>();
        counts.put("ALL", (long) filtered.size());
        for (String st : PROGRESS_STATUSES) {
            counts.put(st, filtered.stream().filter(r -> st.equals(r.getProgressStatus())).count());
        }
        int fromIdx = Math.max(0, page) * Math.max(1, size);
        int limit = Math.min(Math.max(size, 1), 200);
        List<Map<String, Object>> list = filtered.stream().skip(fromIdx).limit(limit)
                .map(this::toMap).toList();
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("total", filtered.size());
        data.put("list", list);
        data.put("counts", counts);
        data.put("progressStatuses", PROGRESS_STATUSES);
        data.put("settleStatuses", SETTLE_STATUSES);
        data.put("giftStatuses", GIFT_STATUSES);
        data.put("carriers", distinct(filtered, HomesReception::getCarrier, CARRIERS));
        data.put("branches", distinct(filtered, HomesReception::getBranch, List.of()));
        data.put("counselors", distinct(filtered, HomesReception::getCounselor, List.of()));
        data.put("processors", distinct(filtered, HomesReception::getProcessor, List.of()));
        data.put("regions", REGIONS);
        data.put("giftCategories", distinct(filtered, HomesReception::getGiftCategory, List.of()));
        data.put("productCategories", distinct(filtered, HomesReception::getProductCategory, List.of()));
        return ApiResponse.ok(data);
    }

    @GetMapping(value = "/receptions/excel", produces = "text/csv; charset=UTF-8")
    org.springframework.http.ResponseEntity<String> excel(@AuthenticationPrincipal JwtUser u,
                                                          @RequestParam Map<String, String> f) {
        List<HomesReception> filtered = filterReceptions(u, f);
        StringBuilder csv = new StringBuilder("\uFEFF");
        csv.append("접수번호,접수일,상담자,지점,통신사,사은품분류,상품분류,상품명,고객명,연락처,지역,진행상황,주문번호,입고정보,처리자,세부진행,지급구분,금액,정산상태,개통일,사은품일,사은품진행,지급처,약정,개통번호,메모\n");
        for (HomesReception r : filtered) {
            csv.append(csvCell(r.getId())).append(',')
                    .append(csvCell(fmtDt(r.getOrderDate()))).append(',')
                    .append(csvCell(r.getCounselor())).append(',')
                    .append(csvCell(r.getBranch())).append(',')
                    .append(csvCell(r.getCarrier())).append(',')
                    .append(csvCell(r.getGiftCategory())).append(',')
                    .append(csvCell(r.getProductCategory())).append(',')
                    .append(csvCell(r.getProductName())).append(',')
                    .append(csvCell(r.getCustomerName())).append(',')
                    .append(csvCell(r.getPhone())).append(',')
                    .append(csvCell(r.getRegion())).append(',')
                    .append(csvCell(r.getProgressStatus())).append(',')
                    .append(csvCell(r.getOrderNumber())).append(',')
                    .append(csvCell(r.getInboundInfo())).append(',')
                    .append(csvCell(r.getProcessor())).append(',')
                    .append(csvCell(r.getDetailProgress())).append(',')
                    .append(csvCell(r.getPaymentType())).append(',')
                    .append(csvCell(r.getAmount())).append(',')
                    .append(csvCell(r.getSettleStatus())).append(',')
                    .append(csvCell(r.getOpenDate())).append(',')
                    .append(csvCell(r.getGiftDate())).append(',')
                    .append(csvCell(r.getGiftStatus())).append(',')
                    .append(csvCell(r.getGiftPlace())).append(',')
                    .append(csvCell(r.getAgreementName())).append(',')
                    .append(csvCell(r.getContractNumber())).append(',')
                    .append(csvCell(r.getMemo())).append('\n');
        }
        return org.springframework.http.ResponseEntity.ok()
                .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=receptions.csv")
                .body(csv.toString());
    }

    @GetMapping("/receptions/{id}")
    ApiResponse get(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) {
        HomesReception r = findOwned(id, u);
        if (r == null) return ApiResponse.fail("접수를 찾을 수 없습니다.");
        return ApiResponse.ok(toProgressDetail(r, u));
    }

    /** 업무 진행내역 저장 — billing_react POST /order/{id}/progress */
    @PostMapping("/receptions/{id}/progress")
    @Transactional
    ApiResponse progress(@PathVariable Long id, @RequestBody Map<String, Object> body, @AuthenticationPrincipal JwtUser u) {
        HomesReception r = findOwned(id, u);
        if (r == null) return ApiResponse.fail("접수를 찾을 수 없습니다.");

        if (body.containsKey("postIt")) r.setPostIt(str(body.get("postIt")));
        if (body.containsKey("postAdd")) r.setPostAdd(str(body.get("postAdd")));
        if (body.containsKey("contractNumber")) r.setContractNumber(str(body.get("contractNumber")));
        if (body.containsKey("installSchedule")) r.setInstallSchedule(str(body.get("installSchedule")));
        if (body.containsKey("customerLoginId") || body.containsKey("customerid")) {
            Object v = body.containsKey("customerLoginId") ? body.get("customerLoginId") : body.get("customerid");
            r.setCustomerLoginId(str(v));
        }
        if (body.containsKey("newTel")) r.setNewTel(str(body.get("newTel")));
        if (body.containsKey("multiLine")) r.setMultiLine(Boolean.TRUE.equals(body.get("multiLine")) || "true".equalsIgnoreCase(String.valueOf(body.get("multiLine"))));
        if (body.containsKey("addPayer")) r.setAddPayer(Boolean.TRUE.equals(body.get("addPayer")) || "true".equalsIgnoreCase(String.valueOf(body.get("addPayer"))));
        if (body.containsKey("record") || body.containsKey("recordFlag")) {
            Object v = body.containsKey("recordFlag") ? body.get("recordFlag") : body.get("record");
            r.setRecordFlag(Boolean.TRUE.equals(v) || "true".equalsIgnoreCase(String.valueOf(v)));
        }
        if (body.containsKey("adminMemo")) r.setAdminMemo(str(body.get("adminMemo")));
        if (body.containsKey("openDate") || body.containsKey("opended")) {
            Object v = body.containsKey("openDate") ? body.get("openDate") : body.get("opended");
            r.setOpenDate(parseDate(str(v)));
        }
        if (body.containsKey("cancelDate") || body.containsKey("canceled")) {
            Object v = body.containsKey("cancelDate") ? body.get("cancelDate") : body.get("canceled");
            r.setCancelDate(parseDate(str(v)));
        }
        if (body.containsKey("giftPlace") || body.containsKey("place")) {
            Object v = body.containsKey("giftPlace") ? body.get("giftPlace") : body.get("place");
            r.setGiftPlace(str(v));
        }
        if (body.containsKey("giftRequested") || body.containsKey("requested")) {
            Object v = body.containsKey("giftRequested") ? body.get("giftRequested") : body.get("requested");
            r.setGiftRequested(parseDate(str(v)));
        }
        if (body.containsKey("giftSended") || body.containsKey("sended")) {
            Object v = body.containsKey("giftSended") ? body.get("giftSended") : body.get("sended");
            r.setGiftSended(parseDate(str(v)));
        }
        if (Boolean.TRUE.equals(body.get("changeOrderDate")) && body.get("orderDate") != null) {
            r.setOrderDate(parseDateTime(str(body.get("orderDate"))));
        }

        String newStatus = str(body.get("status"));
        if (blank(newStatus)) newStatus = str(body.get("progressStatus"));
        String progressMemo = str(body.get("memo"));
        if (blank(progressMemo)) progressMemo = str(body.get("progressLog"));
        boolean adminOnly = Boolean.TRUE.equals(body.get("adminOnly")) || "true".equalsIgnoreCase(String.valueOf(body.get("adminOnly")));

        boolean statusChanged = false;
        if (!blank(newStatus) && !Objects.equals(newStatus, r.getProgressStatus())) {
            r.setProgressStatus(newStatus);
            statusChanged = true;
            if ("4.개통완료".equals(newStatus) && blank(r.getGiftStatus())) {
                r.setGiftStatus("지급대기");
            }
            if ("4.개통완료".equals(newStatus) && r.getOpenDate() == null) {
                r.setOpenDate(LocalDate.now(ZoneId.of("Asia/Seoul")));
            }
        }

        String giftStatus = str(body.get("giftStatus"));
        boolean giftChanged = false;
        if (!blank(giftStatus) && !Objects.equals(giftStatus, r.getGiftStatus())) {
            r.setGiftStatus(giftStatus);
            giftChanged = true;
            appendGiftLog(r, giftStatus, str(body.get("giftProgressLog")), u);
            if ("지급완료".equals(giftStatus) && "4.개통완료".equals(r.getProgressStatus())) {
                r.setProgressStatus("6.정산완료");
                r.setSettleStatus("정산완료");
                statusChanged = true;
            }
        } else if (!blank(str(body.get("giftProgressLog")))) {
            appendGiftLog(r, r.getGiftStatus(), str(body.get("giftProgressLog")), u);
        }

        if (statusChanged || !blank(progressMemo)) {
            appendLog(r, blank(newStatus) ? r.getProgressStatus() : newStatus, progressMemo, u, adminOnly);
        }

        if (body.containsKey("processor") && !blank(str(body.get("processor")))) {
            r.setProcessor(str(body.get("processor")));
        } else if (statusChanged) {
            r.setProcessor(resolveMemberName(u));
        }

        writeLog(u, "RECEPTION", "접수-진행", "접수 #" + r.getId() + " [" + nullToEmpty(r.getCustomerName()) + "] 진행내역 저장");
        return ApiResponse.ok(toProgressDetail(r, u));
    }

    @DeleteMapping("/receptions/progress-log/{logId}")
    @Transactional
    ApiResponse deleteProgressLog(@PathVariable Long logId, @AuthenticationPrincipal JwtUser u) {
        HomesReceptionProgressLog log = em.find(HomesReceptionProgressLog.class, logId);
        if (log == null || !Objects.equals(log.getGroupId(), u.getGroupId())) return ApiResponse.fail("로그를 찾을 수 없습니다.");
        em.remove(log);
        return ApiResponse.ok(null);
    }

    @DeleteMapping("/receptions/gift-progress-log/{logId}")
    @Transactional
    ApiResponse deleteGiftProgressLog(@PathVariable Long logId, @AuthenticationPrincipal JwtUser u) {
        HomesReceptionGiftProgressLog log = em.find(HomesReceptionGiftProgressLog.class, logId);
        if (log == null || !Objects.equals(log.getGroupId(), u.getGroupId())) return ApiResponse.fail("로그를 찾을 수 없습니다.");
        em.remove(log);
        return ApiResponse.ok(null);
    }

    @PostMapping("/receptions")
    @Transactional
    ApiResponse create(@RequestBody Map<String, Object> body, @AuthenticationPrincipal JwtUser u) {
        HomesReception r = new HomesReception();
        r.setGroupId(u.getGroupId());
        apply(r, body);
        if (r.getOrderDate() == null) r.setOrderDate(LocalDateTime.now(ZoneId.of("Asia/Seoul")));
        if (blank(r.getProgressStatus())) r.setProgressStatus("1.접수완료");
        if (blank(r.getSettleStatus())) r.setSettleStatus("정산대기");
        if (blank(r.getGiftStatus())) r.setGiftStatus("미지급");
        if (blank(r.getOrderNumber())) r.setOrderNumber(genOrderNumber());
        em.persist(r);
        em.flush();
        appendLog(r, r.getProgressStatus(), "접수 등록", u);
        writeLog(u, "RECEPTION", "접수-등록", "접수 [" + nullToEmpty(r.getCustomerName()) + "] 등록하였습니다.");
        return ApiResponse.ok(toMap(r));
    }

    @PutMapping("/receptions/{id}")
    @Transactional
    ApiResponse update(@PathVariable Long id, @RequestBody Map<String, Object> body, @AuthenticationPrincipal JwtUser u) {
        HomesReception r = findOwned(id, u);
        if (r == null) return ApiResponse.fail("접수를 찾을 수 없습니다.");
        String prev = r.getProgressStatus();
        apply(r, body);
        if (!Objects.equals(prev, r.getProgressStatus())) {
            appendLog(r, r.getProgressStatus(), str(body.get("progressMemo")), u);
        }
        writeLog(u, "RECEPTION", "접수-수정", "접수 [" + nullToEmpty(r.getCustomerName()) + "] 수정하였습니다.");
        return ApiResponse.ok(toMap(r));
    }

    @DeleteMapping("/receptions/{id}")
    @Transactional
    ApiResponse delete(@PathVariable Long id, @AuthenticationPrincipal JwtUser u) {
        HomesReception r = findOwned(id, u);
        if (r == null) return ApiResponse.fail("접수를 찾을 수 없습니다.");
        writeLog(u, "RECEPTION", "접수-삭제", "접수 [" + nullToEmpty(r.getCustomerName()) + "] 삭제하였습니다.");
        em.remove(r);
        return ApiResponse.ok(null);
    }

    @PostMapping("/receptions/delete")
    @Transactional
    ApiResponse bulkDelete(@RequestBody Map<String, Object> body, @AuthenticationPrincipal JwtUser u) {
        List<Long> ids = toIds(body.get("ids"));
        int n = 0;
        for (Long id : ids) {
            HomesReception r = findOwned(id, u);
            if (r == null) continue;
            writeLog(u, "RECEPTION", "접수-삭제", "접수 [" + nullToEmpty(r.getCustomerName()) + "] 삭제하였습니다.");
            em.remove(r);
            n++;
        }
        return ApiResponse.ok(Map.of("count", n));
    }

    @PostMapping("/receptions/bulk-progress")
    @Transactional
    ApiResponse bulkProgress(@RequestBody Map<String, Object> body, @AuthenticationPrincipal JwtUser u) {
        List<Long> ids = toIds(body.get("ids"));
        if (ids.isEmpty()) return ApiResponse.fail("리스트를 선택하세요.");
        String status = str(body.get("status"));
        if (blank(status)) return ApiResponse.fail("진행상황을 선택하세요.");
        String memo = "[일괄처리] " + nullToEmpty(str(body.get("memo")));
        boolean adminOnly = Boolean.TRUE.equals(body.get("adminOnly"));
        String processor = firstNonBlank(str(body.get("centerId")), str(body.get("processor")), str(body.get("centerName")));
        LocalDate opened = parseDate(str(body.containsKey("opended") ? body.get("opended")
                : body.containsKey("opened") ? body.get("opened") : body.get("openDate")));
        LocalDate canceled = parseDate(str(body.containsKey("canceled") ? body.get("canceled") : body.get("cancelDate")));

        List<HomesReception> rows = new ArrayList<>();
        String baseStatus = null;
        for (Long id : ids) {
            HomesReception r = findOwned(id, u);
            if (r == null) return ApiResponse.fail("접수 리스트가 올바르지 않습니다.");
            if (baseStatus != null && !Objects.equals(baseStatus, r.getProgressStatus())) {
                return ApiResponse.fail("진행상황이 다른 접수가 포함되어 있습니다.");
            }
            baseStatus = r.getProgressStatus();
            rows.add(r);
        }

        int n = 0;
        for (HomesReception r : rows) {
            r.setProgressStatus(status);
            if (!blank(processor)) r.setProcessor(processor);
            if (opened != null) r.setOpenDate(opened);
            if (canceled != null) r.setCancelDate(canceled);
            if ("4.개통완료".equals(status) && blank(r.getGiftStatus())) r.setGiftStatus("지급대기");
            appendLog(r, status, memo, u, adminOnly);
            n++;
        }
        writeLog(u, "RECEPTION", "접수-일괄진행", "접수 " + n + "건 진행상황을 [" + status + "]로 변경하였습니다.");
        return ApiResponse.ok(Map.of("count", n));
    }

    @PostMapping("/receptions/bulk-gift")
    @Transactional
    ApiResponse bulkGift(@RequestBody Map<String, Object> body, @AuthenticationPrincipal JwtUser u) {
        List<Long> ids = toIds(body.get("ids"));
        if (ids.isEmpty()) return ApiResponse.fail("리스트를 선택하세요.");
        String status = str(body.get("status"));
        if (blank(status)) return ApiResponse.fail("사은품진행상황을 선택하세요.");
        String place = str(body.get("place"));
        String giftMemo = "[일괄처리] " + nullToEmpty(str(
                body.containsKey("giftProgressLog") ? body.get("giftProgressLog") : body.get("memo")));
        String deliveryCompany = str(body.get("deliveryCompany"));
        String trackingNumber = str(body.get("trackingNumber"));
        LocalDate requested = parseDate(str(body.containsKey("requested") ? body.get("requested") : body.get("giftRequested")));
        LocalDate sended = parseDate(str(body.containsKey("sended") ? body.get("sended")
                : body.containsKey("giftSended") ? body.get("giftSended") : body.get("giftDate")));

        List<HomesReception> rows = new ArrayList<>();
        String basePlace = null;
        for (Long id : ids) {
            HomesReception r = findOwned(id, u);
            if (r == null) return ApiResponse.fail("선택한 접수가 올바르지 않습니다.");
            String p = r.getGiftPlace();
            if (basePlace == null) {
                basePlace = p;
            } else {
                boolean baseSelf = "b02".equals(basePlace) || "영업점".equals(basePlace);
                boolean curSelf = "b02".equals(p) || "영업점".equals(p);
                if (baseSelf != curSelf) return ApiResponse.fail("사은품 지급처가 다른 접수가 포함되어 있습니다.");
            }
            rows.add(r);
        }

        int n = 0;
        for (HomesReception r : rows) {
            r.setGiftStatus(status);
            if (!blank(place)) r.setGiftPlace(place);
            if (!blank(deliveryCompany)) r.setDeliveryCompany(deliveryCompany);
            if (!blank(trackingNumber)) r.setTrackingNumber(trackingNumber);
            if (requested != null) r.setGiftRequested(requested);
            if (sended != null) {
                r.setGiftSended(sended);
                r.setGiftDate(sended);
            }
            appendGiftLog(r, status, giftMemo, u);
            n++;
        }
        writeLog(u, "RECEPTION", "사은품-일괄", "접수 " + n + "건 사은품진행을 [" + status + "]로 변경하였습니다.");
        return ApiResponse.ok(Map.of("count", n));
    }

    @PostMapping("/receptions/bulk-center-progress")
    @Transactional
    ApiResponse bulkCenterProgress(@RequestBody Map<String, Object> body, @AuthenticationPrincipal JwtUser u) {
        List<Long> ids = toIds(body.get("ids"));
        if (ids.isEmpty()) return ApiResponse.fail("리스트를 선택하세요.");

        List<HomesReception> rows = new ArrayList<>();
        for (Long id : ids) {
            HomesReception r = findOwned(id, u);
            if (r == null) return ApiResponse.fail("접수 리스트가 올바르지 않습니다.");
            // 입력처 정산이 비어있거나 미지정인 건만 대상
            if (!blank(r.getSettleStatus()) && !"정산대기".equals(r.getSettleStatus())
                    && !"미지정".equals(r.getSettleStatus())) {
                return ApiResponse.fail("접수 리스트가 올바르지 않습니다.");
            }
            rows.add(r);
        }

        int n = 0;
        for (HomesReception r : rows) {
            r.setSettleStatus("정산대기");
            appendLog(r, r.getProgressStatus(), "[일괄변경] 입력처 정산대기", u, false);
            n++;
        }
        writeLog(u, "RECEPTION", "입력처-일괄", "접수 " + n + "건을 입력처 정산대기로 변경하였습니다.");
        return ApiResponse.ok(Map.of("count", n));
    }

    /** 신규신청 통신사 목록 (billing_react GET /isp?onlyOn=true) */
    @GetMapping("/reception-isps")
    ApiResponse isps(@AuthenticationPrincipal JwtUser u) {
        List<HomesReceptionIsp> list = em.createQuery(
                        "select e from HomesReceptionIsp e where e.groupId=:g and (e.active is null or e.active=true) order by e.seq asc, e.id asc",
                        HomesReceptionIsp.class)
                .setParameter("g", u.getGroupId()).getResultList();
        return ApiResponse.ok(list.stream().map(i -> {
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("id", i.getId());
            m.put("name", i.getName());
            m.put("telecom", i.getTelecom());
            m.put("seq", i.getSeq());
            m.put("isSktb", Boolean.TRUE.equals(i.getIsSktb()));
            return m;
        }).toList());
    }

    @GetMapping("/reception-isps/{ispId}/products")
    ApiResponse ispProducts(@PathVariable Long ispId, @AuthenticationPrincipal JwtUser u) {
        HomesReceptionIsp isp = findIsp(ispId, u);
        if (isp == null) return ApiResponse.fail("통신사를 찾을 수 없습니다.");
        List<HomesReceptionIspProduct> list = em.createQuery(
                        "select e from HomesReceptionIspProduct e where e.groupId=:g and e.ispId=:isp and (e.active is null or e.active=true) order by e.id",
                        HomesReceptionIspProduct.class)
                .setParameter("g", u.getGroupId()).setParameter("isp", ispId).getResultList();
        return ApiResponse.ok(list.stream().map(p -> Map.of(
                "id", p.getId(),
                "type", nullToEmpty(p.getProductType()),
                "name", nullToEmpty(p.getName())
        )).toList());
    }

    @GetMapping("/reception-isps/{ispId}/agreements")
    ApiResponse ispAgreements(@PathVariable Long ispId, @AuthenticationPrincipal JwtUser u) {
        HomesReceptionIsp isp = findIsp(ispId, u);
        if (isp == null) return ApiResponse.fail("통신사를 찾을 수 없습니다.");
        List<HomesReceptionIspAgreement> list = em.createQuery(
                        "select e from HomesReceptionIspAgreement e where e.groupId=:g and e.ispId=:isp and (e.active is null or e.active=true) order by e.id",
                        HomesReceptionIspAgreement.class)
                .setParameter("g", u.getGroupId()).setParameter("isp", ispId).getResultList();
        return ApiResponse.ok(list.stream().map(a -> Map.of(
                "id", a.getId(),
                "name", nullToEmpty(a.getName())
        )).toList());
    }

    /**
     * 신규신청 등록 — billing_react POST /order 를 Care Homes reception 으로 매핑.
     * body: { ispId, counselor, branch, customer:{type,name,phone,region,address,ssid,postIt}, payment:{method,name,...},
     *         product:{productType,productId,agreementId,price,installDate,discount}, gift:{type,place,cashAmount,goods,giftDate}, memo }
     */
    @PostMapping("/receptions/apply")
    @Transactional
    ApiResponse apply(@RequestBody Map<String, Object> body, @AuthenticationPrincipal JwtUser u) {
        Long ispId = toLong(body.get("ispId"));
        HomesReceptionIsp isp = ispId == null ? null : findIsp(ispId, u);
        if (isp == null) return ApiResponse.fail("통신사를 선택하세요.");

        @SuppressWarnings("unchecked")
        Map<String, Object> customer = body.get("customer") instanceof Map<?, ?> m ? (Map<String, Object>) m : Map.of();
        @SuppressWarnings("unchecked")
        Map<String, Object> payment = body.get("payment") instanceof Map<?, ?> m ? (Map<String, Object>) m : Map.of();
        @SuppressWarnings("unchecked")
        Map<String, Object> product = body.get("product") instanceof Map<?, ?> m ? (Map<String, Object>) m : Map.of();
        @SuppressWarnings("unchecked")
        Map<String, Object> gift = body.get("gift") instanceof Map<?, ?> m ? (Map<String, Object>) m : Map.of();

        String customerName = str(customer.get("name"));
        if (blank(customerName)) return ApiResponse.fail("고객명을 입력하세요.");
        Long productId = toLong(product.get("productId"));
        if (productId == null) return ApiResponse.fail("상품을 선택하세요.");

        HomesReceptionIspProduct prod = em.find(HomesReceptionIspProduct.class, productId);
        if (prod == null || !Objects.equals(prod.getGroupId(), u.getGroupId()) || !Objects.equals(prod.getIspId(), ispId)) {
            return ApiResponse.fail("상품이 올바르지 않습니다.");
        }
        String agreementName = null;
        Long agreementId = toLong(product.get("agreementId"));
        if (agreementId != null) {
            HomesReceptionIspAgreement ag = em.find(HomesReceptionIspAgreement.class, agreementId);
            if (ag != null && Objects.equals(ag.getGroupId(), u.getGroupId())) agreementName = ag.getName();
        }

        String productTypeLabel = switch (nullToEmpty(prod.getProductType())) {
            case "e02" -> "전화";
            case "e03" -> "TV";
            case "e04" -> "렌탈";
            default -> "인터넷";
        };
        String giftType = str(gift.get("type"));
        String giftCategory = switch (nullToEmpty(giftType)) {
            case "a02" -> "현금";
            case "a03" -> "상품";
            case "a04" -> "현금+상품";
            case "a01" -> "없음";
            default -> blank(giftType) ? "상품권" : giftType;
        };
        String giftPlaceCode = str(gift.get("place"));
        String payMethod = str(payment.get("method"));
        String paymentType = switch (nullToEmpty(payMethod)) {
            case "card" -> "카드";
            case "paper" -> "지로";
            case "telephone" -> "전화납부";
            default -> "계좌";
        };

        HomesReception r = new HomesReception();
        r.setGroupId(u.getGroupId());
        r.setOrderDate(LocalDateTime.now(ZoneId.of("Asia/Seoul")));
        r.setIspId(isp.getId());
        r.setIspName(isp.getName());
        r.setCarrier(isp.getTelecom());
        r.setCounselor(blank(str(body.get("counselor"))) ? resolveMemberName(u) : str(body.get("counselor")));
        r.setBranch(str(body.get("branch")));
        r.setBranchGroup(str(body.get("branchGroup")));
        r.setCustomerType(blank(str(customer.get("type"))) ? "public" : str(customer.get("type")));
        r.setCustomerName(customerName);
        r.setPhone(str(customer.get("phone")));
        r.setTel(str(customer.get("tel")));
        r.setEmail(str(customer.get("email")));
        r.setZipcode(str(customer.get("zipcode")));
        r.setAddress(str(customer.get("address")));
        r.setAddressDetail(str(customer.get("addressDetail")));
        r.setRegion(str(customer.get("region")));
        r.setIdentifyNumber(str(customer.get("ssid")));
        r.setPostIt(str(customer.get("postIt")));
        r.setPostAdd(str(customer.get("postAdd")));
        r.setProductType(prod.getProductType());
        r.setProductCategory(productTypeLabel);
        r.setProductName(prod.getName());
        r.setAgreementName(agreementName);
        r.setAmount(toDecimal(product.get("price")));
        r.setOpenDate(parseDate(str(product.get("installDate"))));
        r.setGiftCategory(giftCategory);
        r.setGiftPlace(giftPlaceCode);
        r.setGiftDate(parseDate(str(gift.get("giftDate"))));
        r.setGiftStatus(blank(giftType) || "a01".equals(giftType) ? "미지급" : "지급대기");
        r.setGiftCashAmount(toDecimal(gift.get("cashAmount")));
        r.setGiftGoods(str(gift.get("goods")));
        r.setGiftGoodsPrice(toDecimal(gift.get("goodsPrice")));
        r.setGiftBank(str(gift.get("accountBank")));
        r.setGiftAccountNumber(str(gift.get("accountNumber")));
        r.setGiftAccountName(str(gift.get("accountName")));
        r.setGiftSsid(str(gift.get("ssid")));
        r.setPaymentMethod(blank(payMethod) ? "transfer" : payMethod);
        r.setPaymentType(paymentType);
        r.setPaymentName(str(payment.get("name")));
        r.setPaymentSsid(str(payment.get("ssid")));
        r.setPaymentTel(str(payment.get("tel")));
        r.setPaymentBank(str(payment.get("bank")));
        r.setPaymentAccountNumber(str(payment.get("accountNumber")));
        r.setPaymentCardCompany(str(payment.get("cardCompany")));
        r.setPaymentCardYear(str(payment.get("cardYear")));
        r.setPaymentCardMonth(str(payment.get("cardMonth")));
        r.setPaymentCardNumber(str(payment.get("cardNumber")));
        r.setProgressStatus("1.접수완료");
        r.setSettleStatus("정산대기");
        r.setOrderNumber(genOrderNumber());
        r.setProcessor(r.getCounselor());
        r.setDetailProgress("신규신청");
        r.setMemo(str(body.get("memo")));
        em.persist(r);
        em.flush();
        appendLog(r, r.getProgressStatus(), "신규신청 [" + isp.getName() + "]", u, false);
        writeLog(u, "RECEPTION", "신규신청", "신규신청 [" + isp.getName() + "] " + customerName + " 등록하였습니다.");
        return ApiResponse.ok(toProgressDetail(r, u));
    }

    // ---- 첨부파일 (일반 file1~5) ----
    @PostMapping("/receptions/{id}/file/{slot}")
    @Transactional
    ApiResponse uploadFile(@PathVariable Long id, @PathVariable int slot,
                           @RequestParam("file") MultipartFile file,
                           @AuthenticationPrincipal JwtUser u) {
        return uploadAttachment(id, slot, false, file, u);
    }

    @GetMapping("/receptions/{id}/file/{slot}")
    ResponseEntity<Resource> downloadFile(@PathVariable Long id, @PathVariable int slot,
                                          @AuthenticationPrincipal JwtUser u) {
        return downloadAttachment(id, slot, false, u);
    }

    @DeleteMapping("/receptions/{id}/file/{slot}")
    @Transactional
    ApiResponse deleteFile(@PathVariable Long id, @PathVariable int slot,
                           @AuthenticationPrincipal JwtUser u) {
        return deleteAttachment(id, slot, false, u);
    }

    // ---- 관리자 첨부파일 (admin_file1~3) ----
    @PostMapping("/receptions/{id}/admin-file/{slot}")
    @Transactional
    ApiResponse uploadAdminFile(@PathVariable Long id, @PathVariable int slot,
                                @RequestParam("file") MultipartFile file,
                                @AuthenticationPrincipal JwtUser u) {
        return uploadAttachment(id, slot, true, file, u);
    }

    @GetMapping("/receptions/{id}/admin-file/{slot}")
    ResponseEntity<Resource> downloadAdminFile(@PathVariable Long id, @PathVariable int slot,
                                               @AuthenticationPrincipal JwtUser u) {
        return downloadAttachment(id, slot, true, u);
    }

    @DeleteMapping("/receptions/{id}/admin-file/{slot}")
    @Transactional
    ApiResponse deleteAdminFile(@PathVariable Long id, @PathVariable int slot,
                                @AuthenticationPrincipal JwtUser u) {
        return deleteAttachment(id, slot, true, u);
    }

    private ApiResponse uploadAttachment(Long id, int slot, boolean admin, MultipartFile file, JwtUser u) {
        HomesReception r = findOwned(id, u);
        int max = admin ? 3 : 5;
        if (slot < 1 || slot > max) return ApiResponse.fail("첨부 슬롯이 올바르지 않습니다.");
        if (file == null || file.isEmpty()) return ApiResponse.fail("업로드할 파일을 선택하세요.");
        String old = getFile(r, slot, admin);
        if (old != null && !old.isBlank()) fileStorage.delete(old);
        String path = fileStorage.store("reception/" + id, file);
        setFile(r, slot, admin, path);
        writeLog(u, "RECEPTION", admin ? "관리자첨부" : "첨부",
                "접수 #" + id + " " + (admin ? "관리자첨부" : "첨부") + slot + " 업로드");
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("id", r.getId());
        data.put("slot", slot);
        data.put("admin", admin);
        data.put("path", path);
        data.put("name", FileStorageService.displayName(path));
        return ApiResponse.ok(data);
    }

    private ResponseEntity<Resource> downloadAttachment(Long id, int slot, boolean admin, JwtUser u) {
        HomesReception r = findOwned(id, u);
        String rel = getFile(r, slot, admin);
        if (rel == null || rel.isBlank()) {
            return ResponseEntity.notFound().build();
        }
        Resource resource = fileStorage.loadAsResource(rel);
        String filename = URLEncoder.encode(FileStorageService.displayName(rel), StandardCharsets.UTF_8)
                .replace("+", "%20");
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + filename)
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(resource);
    }

    private ApiResponse deleteAttachment(Long id, int slot, boolean admin, JwtUser u) {
        HomesReception r = findOwned(id, u);
        int max = admin ? 3 : 5;
        if (slot < 1 || slot > max) return ApiResponse.fail("첨부 슬롯이 올바르지 않습니다.");
        String old = getFile(r, slot, admin);
        if (old != null && !old.isBlank()) fileStorage.delete(old);
        setFile(r, slot, admin, null);
        writeLog(u, "RECEPTION", admin ? "관리자첨부삭제" : "첨부삭제",
                "접수 #" + id + " " + (admin ? "관리자첨부" : "첨부") + slot + " 삭제");
        return ApiResponse.ok(Map.of("id", id, "slot", slot, "admin", admin));
    }

    private static String getFile(HomesReception r, int slot, boolean admin) {
        if (admin) {
            return switch (slot) {
                case 1 -> r.getAdminFile1();
                case 2 -> r.getAdminFile2();
                case 3 -> r.getAdminFile3();
                default -> null;
            };
        }
        return switch (slot) {
            case 1 -> r.getFile1();
            case 2 -> r.getFile2();
            case 3 -> r.getFile3();
            case 4 -> r.getFile4();
            case 5 -> r.getFile5();
            default -> null;
        };
    }

    private static void setFile(HomesReception r, int slot, boolean admin, String path) {
        if (admin) {
            switch (slot) {
                case 1 -> r.setAdminFile1(path);
                case 2 -> r.setAdminFile2(path);
                case 3 -> r.setAdminFile3(path);
                default -> {
                }
            }
            return;
        }
        switch (slot) {
            case 1 -> r.setFile1(path);
            case 2 -> r.setFile2(path);
            case 3 -> r.setFile3(path);
            case 4 -> r.setFile4(path);
            case 5 -> r.setFile5(path);
            default -> {
            }
        }
    }

    private HomesReceptionIsp findIsp(Long id, JwtUser u) {
        HomesReceptionIsp isp = em.find(HomesReceptionIsp.class, id);
        if (isp == null || !Objects.equals(isp.getGroupId(), u.getGroupId())) return null;
        return isp;
    }

    private static Long toLong(Object v) {
        if (v == null || String.valueOf(v).isBlank()) return null;
        try { return Long.valueOf(String.valueOf(v)); } catch (Exception e) { return null; }
    }

    private List<HomesReception> filterReceptions(JwtUser u, Map<String, String> f) {
        List<HomesReception> all = em.createQuery(
                        "select e from HomesReception e where e.groupId=:g order by e.orderDate desc, e.id desc",
                        HomesReception.class)
                .setParameter("g", u.getGroupId()).getResultList();

        LocalDate from = parseDate(first(f, "dateFrom", "orderDateStart"));
        LocalDate to = parseDate(first(f, "dateTo", "orderDateEnd"));
        LocalDate openFrom = parseDate(f.get("openDateFrom"));
        LocalDate openTo = parseDate(f.get("openDateTo"));
        LocalDate opened = parseDate(first(f, "opended", "opened", "openDate"));
        LocalDate giftSended = parseDate(first(f, "giftSended", "giftDateExact"));
        String progress = blankToNull(first(f, "progressStatus", "tab"));
        if ("ALL".equalsIgnoreCase(progress)) progress = null;
        String settle = blankToNull(first(f, "settleStatus", "centerStatus"));
        String giftStatus = blankToNull(f.get("giftStatus"));
        String carrier = blankToNull(f.get("carrier"));
        String branch = blankToNull(f.get("branch"));
        String counselor = blankToNull(f.get("counselor"));
        String processor = blankToNull(f.get("processor"));
        String region = blankToNull(first(f, "region", "customerSido"));
        String giftCategory = blankToNull(first(f, "giftCategory", "giftType"));
        String productCategory = blankToNull(first(f, "productCategory", "productType"));
        String customerName = blankToNull(f.get("customerName"));
        String phone = blankToNull(f.get("customerPhone") != null ? f.get("customerPhone") : f.get("phone"));
        String orderNumber = blankToNull(f.get("orderNumber"));
        String q = blankToNull(first(f, "q", "keyword"));
        String postIt = blankToNull(f.get("postIt"));
        String agreementName = blankToNull(f.get("agreementName"));
        String customerSsid = blankToNull(first(f, "customerSsid", "identifyNumber"));
        String customerEmail = blankToNull(first(f, "customerEmail", "email"));
        String productName = blankToNull(f.get("productName"));
        String ispName = blankToNull(f.get("ispName"));
        String contractNumber = blankToNull(f.get("contractNumber"));
        String giftPlace = blankToNull(f.get("giftPlace"));
        String branchEmployee = blankToNull(first(f, "branchEmployee"));
        String branchName = blankToNull(f.get("branchName"));
        String sorting = blankToNull(f.get("sorting"));

        String progressF = progress;
        List<HomesReception> result = all.stream().filter(r -> {
            LocalDate od = r.getOrderDate() == null ? null : r.getOrderDate().toLocalDate();
            if (from != null && (od == null || od.isBefore(from))) return false;
            if (to != null && (od == null || od.isAfter(to))) return false;
            if (openFrom != null && (r.getOpenDate() == null || r.getOpenDate().isBefore(openFrom))) return false;
            if (openTo != null && (r.getOpenDate() == null || r.getOpenDate().isAfter(openTo))) return false;
            if (opened != null && (r.getOpenDate() == null || !r.getOpenDate().equals(opened))) return false;
            if (giftSended != null) {
                LocalDate gd = r.getGiftSended() != null ? r.getGiftSended() : r.getGiftDate();
                if (gd == null || !gd.equals(giftSended)) return false;
            }
            if (progressF != null && !progressF.equals(r.getProgressStatus())) return false;
            if (settle != null && !settle.equals(r.getSettleStatus())) return false;
            if (giftStatus != null && !giftStatus.equals(r.getGiftStatus())) return false;
            if (carrier != null && !carrier.equals(r.getCarrier()) && !carrier.equals(r.getIspName())) return false;
            if (ispName != null) {
                String hay = (nullToEmpty(r.getIspName()) + " " + nullToEmpty(r.getCarrier())).toLowerCase();
                if (!hay.contains(ispName.toLowerCase())) return false;
            }
            if (branch != null && !branch.equals(r.getBranch())) return false;
            if (branchName != null && !nullToEmpty(r.getBranch()).contains(branchName)) return false;
            if (counselor != null && !counselor.equals(r.getCounselor())) return false;
            if (branchEmployee != null && !nullToEmpty(r.getCounselor()).contains(branchEmployee)) return false;
            if (processor != null && !processor.equals(r.getProcessor())) return false;
            if (region != null && !region.equals(r.getRegion())) return false;
            if (giftCategory != null && !nullToEmpty(r.getGiftCategory()).contains(giftCategory)) return false;
            if (productCategory != null
                    && !productCategory.equals(r.getProductCategory())
                    && !productCategory.equals(r.getProductType())) return false;
            if (giftPlace != null && !giftPlace.equals(r.getGiftPlace())) return false;
            if (customerName != null && !nullToEmpty(r.getCustomerName()).contains(customerName)) return false;
            if (phone != null) {
                String digits = nullToEmpty(r.getPhone()).replaceAll("\\D", "");
                if (!digits.contains(phone.replaceAll("\\D", "")) && !nullToEmpty(r.getPhone()).contains(phone)) return false;
            }
            if (orderNumber != null && !nullToEmpty(r.getOrderNumber()).contains(orderNumber)) return false;
            if (postIt != null && !nullToEmpty(r.getPostIt()).contains(postIt)
                    && !nullToEmpty(r.getPostAdd()).contains(postIt)) return false;
            if (agreementName != null && !nullToEmpty(r.getAgreementName()).contains(agreementName)) return false;
            if (customerSsid != null && !nullToEmpty(r.getIdentifyNumber()).contains(customerSsid)) return false;
            if (customerEmail != null && !nullToEmpty(r.getEmail()).contains(customerEmail)) return false;
            if (productName != null && !nullToEmpty(r.getProductName()).contains(productName)) return false;
            if (contractNumber != null && !nullToEmpty(r.getContractNumber()).contains(contractNumber)) return false;
            if (q != null) {
                String hay = (nullToEmpty(r.getCustomerName()) + " " + nullToEmpty(r.getPhone()) + " "
                        + nullToEmpty(r.getProductName()) + " " + nullToEmpty(r.getOrderNumber()) + " "
                        + nullToEmpty(r.getCounselor()) + " " + nullToEmpty(r.getBranch()) + " "
                        + nullToEmpty(r.getMemo()) + " " + nullToEmpty(r.getPostIt()) + " "
                        + nullToEmpty(r.getIdentifyNumber())).toLowerCase();
                if (!hay.contains(q.toLowerCase())) return false;
            }
            return true;
        }).collect(Collectors.toCollection(ArrayList::new));

        if ("opended".equals(sorting) || "opened".equals(sorting)) {
            result.sort(Comparator
                    .comparing(HomesReception::getOpenDate, Comparator.nullsLast(Comparator.naturalOrder()))
                    .reversed());
        }
        return result;
    }

    private void apply(HomesReception r, Map<String, Object> d) {
        if (d.containsKey("orderDate")) r.setOrderDate(parseDateTime(str(d.get("orderDate"))));
        if (d.containsKey("counselor")) r.setCounselor(str(d.get("counselor")));
        if (d.containsKey("branch")) r.setBranch(str(d.get("branch")));
        if (d.containsKey("branchGroup")) r.setBranchGroup(str(d.get("branchGroup")));
        if (d.containsKey("carrier")) r.setCarrier(str(d.get("carrier")));
        if (d.containsKey("giftCategory")) r.setGiftCategory(str(d.get("giftCategory")));
        if (d.containsKey("productCategory")) r.setProductCategory(str(d.get("productCategory")));
        if (d.containsKey("productName")) r.setProductName(str(d.get("productName")));
        if (d.containsKey("customerName")) r.setCustomerName(str(d.get("customerName")));
        if (d.containsKey("phone")) r.setPhone(str(d.get("phone")));
        if (d.containsKey("region")) r.setRegion(str(d.get("region")));
        if (d.containsKey("progressStatus")) r.setProgressStatus(str(d.get("progressStatus")));
        if (d.containsKey("orderNumber")) r.setOrderNumber(str(d.get("orderNumber")));
        if (d.containsKey("ispId")) r.setIspId(toLong(d.get("ispId")));
        if (d.containsKey("ispName")) r.setIspName(str(d.get("ispName")));
        if (d.containsKey("inboundInfo")) r.setInboundInfo(str(d.get("inboundInfo")));
        if (d.containsKey("processor")) r.setProcessor(str(d.get("processor")));
        if (d.containsKey("detailProgress")) r.setDetailProgress(str(d.get("detailProgress")));
        if (d.containsKey("paymentType")) r.setPaymentType(str(d.get("paymentType")));
        if (d.containsKey("amount")) r.setAmount(toDecimal(d.get("amount")));
        if (d.containsKey("settleStatus")) r.setSettleStatus(str(d.get("settleStatus")));
        if (d.containsKey("openDate")) r.setOpenDate(parseDate(str(d.get("openDate"))));
        if (d.containsKey("giftDate")) r.setGiftDate(parseDate(str(d.get("giftDate"))));
        if (d.containsKey("giftStatus")) r.setGiftStatus(str(d.get("giftStatus")));
        if (d.containsKey("giftPlace")) r.setGiftPlace(str(d.get("giftPlace")));
        if (d.containsKey("agreementName")) r.setAgreementName(str(d.get("agreementName")));
        if (d.containsKey("contractNumber")) r.setContractNumber(str(d.get("contractNumber")));
        if (d.containsKey("identifyNumber")) r.setIdentifyNumber(str(d.get("identifyNumber")));
        if (d.containsKey("postIt")) r.setPostIt(str(d.get("postIt")));
        if (d.containsKey("postAdd")) r.setPostAdd(str(d.get("postAdd")));
        if (d.containsKey("memo")) r.setMemo(str(d.get("memo")));
        if (d.containsKey("adminMemo")) r.setAdminMemo(str(d.get("adminMemo")));
        if (d.containsKey("customerType")) r.setCustomerType(str(d.get("customerType")));
        if (d.containsKey("tel")) r.setTel(str(d.get("tel")));
        if (d.containsKey("email")) r.setEmail(str(d.get("email")));
        if (d.containsKey("zipcode")) r.setZipcode(str(d.get("zipcode")));
        if (d.containsKey("address")) r.setAddress(str(d.get("address")));
        if (d.containsKey("addressDetail")) r.setAddressDetail(str(d.get("addressDetail")));
        if (d.containsKey("paymentMethod")) r.setPaymentMethod(str(d.get("paymentMethod")));
        if (d.containsKey("productType")) r.setProductType(str(d.get("productType")));
        if (d.containsKey("installSchedule")) r.setInstallSchedule(str(d.get("installSchedule")));
        if (d.containsKey("customerLoginId")) r.setCustomerLoginId(str(d.get("customerLoginId")));
        if (d.containsKey("newTel")) r.setNewTel(str(d.get("newTel")));
        if (d.containsKey("cancelDate")) r.setCancelDate(parseDate(str(d.get("cancelDate"))));
        if (d.containsKey("branchPhone")) r.setBranchPhone(str(d.get("branchPhone")));
    }

    private Map<String, Object> toProgressDetail(HomesReception r, JwtUser u) {
        Map<String, Object> data = new LinkedHashMap<>();
        Map<String, Object> reception = toMap(r);
        data.put("reception", reception);
        data.put("order", reception); // billing_react 호환 별칭
        data.put("id", r.getId());

        Map<String, Object> customer = new LinkedHashMap<>();
        customer.put("type", blank(r.getCustomerType()) ? "public" : r.getCustomerType());
        customer.put("name", r.getCustomerName());
        customer.put("postIt", r.getPostIt());
        customer.put("postAdd", r.getPostAdd());
        customer.put("phone", r.getPhone());
        customer.put("tel", r.getTel());
        customer.put("email", r.getEmail());
        customer.put("zipcode", r.getZipcode());
        customer.put("address", r.getAddress());
        customer.put("addressDetail", r.getAddressDetail());
        customer.put("sido", r.getRegion());
        customer.put("ssid", r.getIdentifyNumber());
        data.put("customer", customer);
        data.put("customerPublic", Map.of("ssid", nullToEmpty(r.getIdentifyNumber())));

        Map<String, Object> payment = new LinkedHashMap<>();
        payment.put("method", blank(r.getPaymentMethod()) ? paymentMethodFromType(r.getPaymentType()) : r.getPaymentMethod());
        payment.put("name", r.getPaymentName());
        payment.put("ssid", r.getPaymentSsid());
        payment.put("tel", r.getPaymentTel());
        payment.put("bank", r.getPaymentBank());
        payment.put("accountNumber", r.getPaymentAccountNumber());
        payment.put("cardCompany", r.getPaymentCardCompany());
        payment.put("cardYear", r.getPaymentCardYear());
        payment.put("cardMonth", r.getPaymentCardMonth());
        payment.put("cardNumber", r.getPaymentCardNumber());
        data.put("payment", payment);
        if ("card".equals(payment.get("method"))) data.put("paymentCard", payment);
        else if ("transfer".equals(payment.get("method"))) data.put("paymentTransfer", payment);

        Map<String, Object> product = new LinkedHashMap<>();
        product.put("productType", blank(r.getProductType()) ? productTypeFromCategory(r.getProductCategory()) : r.getProductType());
        product.put("name", r.getProductName());
        product.put("productName", r.getProductName());
        product.put("price", r.getAmount() == null ? 0 : r.getAmount());
        data.put("product", product);
        data.put("productName", r.getProductName());
        data.put("agreementName", r.getAgreementName());
        data.put("ispName", blank(r.getIspName()) ? r.getCarrier() : r.getIspName());
        data.put("eventNames", List.of());

        Map<String, Object> gift = new LinkedHashMap<>();
        gift.put("status", r.getGiftStatus());
        gift.put("place", r.getGiftPlace());
        gift.put("giftDate", r.getGiftDate() == null ? null : r.getGiftDate().toString());
        gift.put("requested", r.getGiftRequested() == null ? null : r.getGiftRequested().toString());
        gift.put("sended", r.getGiftSended() == null ? null : r.getGiftSended().toString());
        gift.put("accountBank", r.getGiftBank());
        gift.put("accountNumber", r.getGiftAccountNumber());
        gift.put("accountName", r.getGiftAccountName());
        gift.put("ssid", r.getGiftSsid());
        gift.put("goods", r.getGiftGoods());
        gift.put("cashAmount", r.getGiftCashAmount());
        gift.put("goodsPrice", r.getGiftGoodsPrice());
        gift.put("category", r.getGiftCategory());
        data.put("gift", gift);

        Map<String, Object> etc = new LinkedHashMap<>();
        etc.put("memo", r.getMemo());
        etc.put("adminMemo", r.getAdminMemo());
        etc.put("file1", r.getFile1());
        etc.put("file2", r.getFile2());
        etc.put("file3", r.getFile3());
        etc.put("file4", r.getFile4());
        etc.put("file5", r.getFile5());
        etc.put("adminFile1", r.getAdminFile1());
        etc.put("adminFile2", r.getAdminFile2());
        etc.put("adminFile3", r.getAdminFile3());
        data.put("etc", etc);

        data.put("employeeName", r.getCounselor());
        data.put("branchOfficeName", blank(r.getBranch()) ? "본지점" : r.getBranch());
        data.put("branchPhone", blank(r.getBranchPhone()) ? "" : r.getBranchPhone());
        data.put("branchTel", "");
        data.put("progressLogs", progressLogs(r.getId(), u.getGroupId()));
        data.put("giftProgressLogs", giftProgressLogs(r.getId(), u.getGroupId()));
        data.put("logs", data.get("progressLogs"));
        data.put("progressStatuses", PROGRESS_STATUSES);
        data.put("giftStatuses", GIFT_STATUSES);
        data.put("settleStatuses", SETTLE_STATUSES);
        Map<String, String> statusOptions = new LinkedHashMap<>();
        for (String s : PROGRESS_STATUSES) statusOptions.put(s, s);
        data.put("statusOptions", statusOptions);
        Map<String, String> giftStatusOptions = new LinkedHashMap<>();
        for (String s : GIFT_STATUSES) giftStatusOptions.put(s, s);
        data.put("giftStatusOptions", giftStatusOptions);
        // flat alias for list edit form compatibility
        data.putAll(reception);
        return data;
    }

    private static String paymentMethodFromType(String paymentType) {
        if ("카드".equals(paymentType)) return "card";
        if ("지로".equals(paymentType)) return "paper";
        if ("전화납부".equals(paymentType)) return "telephone";
        return "transfer";
    }

    private static String productTypeFromCategory(String cat) {
        if ("전화".equals(cat)) return "e02";
        if ("TV".equals(cat)) return "e03";
        if ("렌탈".equals(cat)) return "e04";
        return "e01";
    }

    private Map<String, Object> toMap(HomesReception r) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("id", r.getId());
        m.put("orderDate", fmtDt(r.getOrderDate()));
        m.put("today", r.getOrderDate() != null
                && r.getOrderDate().toLocalDate().equals(LocalDate.now(ZoneId.of("Asia/Seoul"))));
        m.put("counselor", r.getCounselor());
        m.put("branch", r.getBranch());
        m.put("branchGroup", r.getBranchGroup());
        m.put("branchPhone", r.getBranchPhone());
        m.put("carrier", r.getCarrier());
        m.put("giftCategory", r.getGiftCategory());
        m.put("productCategory", r.getProductCategory());
        m.put("productType", r.getProductType());
        m.put("productName", r.getProductName());
        m.put("customerType", r.getCustomerType());
        m.put("customerName", r.getCustomerName());
        m.put("phone", r.getPhone());
        m.put("tel", r.getTel());
        m.put("email", r.getEmail());
        m.put("zipcode", r.getZipcode());
        m.put("address", r.getAddress());
        m.put("addressDetail", r.getAddressDetail());
        m.put("region", r.getRegion());
        m.put("progressStatus", r.getProgressStatus());
        m.put("orderNumber", r.getOrderNumber());
        m.put("ispId", r.getIspId());
        m.put("ispName", r.getIspName());
        m.put("inboundInfo", r.getInboundInfo());
        m.put("processor", r.getProcessor());
        m.put("detailProgress", r.getDetailProgress());
        m.put("paymentType", r.getPaymentType());
        m.put("paymentMethod", r.getPaymentMethod());
        m.put("amount", r.getAmount());
        m.put("settleStatus", r.getSettleStatus());
        m.put("openDate", r.getOpenDate() == null ? null : r.getOpenDate().toString());
        m.put("cancelDate", r.getCancelDate() == null ? null : r.getCancelDate().toString());
        m.put("giftDate", r.getGiftDate() == null ? null : r.getGiftDate().toString());
        m.put("giftStatus", r.getGiftStatus());
        m.put("giftPlace", r.getGiftPlace());
        m.put("giftRequested", r.getGiftRequested() == null ? null : r.getGiftRequested().toString());
        m.put("giftSended", r.getGiftSended() == null ? null : r.getGiftSended().toString());
        m.put("agreementName", r.getAgreementName());
        m.put("contractNumber", r.getContractNumber());
        m.put("installSchedule", r.getInstallSchedule());
        m.put("customerLoginId", r.getCustomerLoginId());
        m.put("newTel", r.getNewTel());
        m.put("multiLine", Boolean.TRUE.equals(r.getMultiLine()));
        m.put("addPayer", Boolean.TRUE.equals(r.getAddPayer()));
        m.put("record", Boolean.TRUE.equals(r.getRecordFlag()));
        m.put("identifyNumber", r.getIdentifyNumber());
        m.put("postIt", r.getPostIt());
        m.put("postAdd", r.getPostAdd());
        m.put("memo", r.getMemo());
        m.put("adminMemo", r.getAdminMemo());
        // billing_react OrderListRow 호환 별칭
        m.put("employeeName", r.getCounselor());
        m.put("sido", r.getRegion());
        m.put("opened", r.getOpenDate() == null ? null : r.getOpenDate().toString());
        m.put("canceled", r.getCancelDate() == null ? null : r.getCancelDate().toString());
        m.put("status", r.getProgressStatus());
        m.put("giftType", r.getGiftCategory());
        m.put("giftSended", r.getGiftSended() == null
                ? (r.getGiftDate() == null ? null : r.getGiftDate().toString())
                : r.getGiftSended().toString());
        m.put("branchOfficeName", r.getBranch());
        m.put("branchGroupName", r.getBranchGroup());
        if (blank(r.getIspName())) m.put("ispName", r.getCarrier());
        BigDecimal giftAmt = BigDecimal.ZERO;
        if (r.getGiftCashAmount() != null) giftAmt = giftAmt.add(r.getGiftCashAmount());
        if (r.getGiftGoodsPrice() != null) giftAmt = giftAmt.add(r.getGiftGoodsPrice());
        if (giftAmt.signum() == 0 && r.getAmount() != null
                && r.getGiftStatus() != null && !"미지급".equals(r.getGiftStatus()) && !"없음".equals(r.getGiftCategory())) {
            giftAmt = r.getAmount();
        }
        m.put("giftAmount", giftAmt.signum() == 0 ? null : giftAmt);
        m.put("centerName", blank(r.getProcessor()) ? "본사" : r.getProcessor());
        m.put("centerStatus", r.getSettleStatus());
        return m;
    }

    private List<Map<String, Object>> progressLogs(Long receptionId, String groupId) {
        return em.createQuery(
                        "select l from HomesReceptionProgressLog l where l.groupId=:g and l.receptionId=:rid order by l.loggedAt desc, l.id desc",
                        HomesReceptionProgressLog.class)
                .setParameter("g", groupId).setParameter("rid", receptionId)
                .getResultList().stream().map(l -> {
                    Map<String, Object> m = new LinkedHashMap<>();
                    m.put("id", l.getId());
                    m.put("status", l.getStatus());
                    m.put("memo", l.getMemo());
                    m.put("authorName", l.getAuthorName());
                    m.put("writer", l.getAuthorName());
                    m.put("loggedAt", fmtDt(l.getLoggedAt()));
                    m.put("createdAt", fmtDt(l.getLoggedAt()));
                    m.put("adminOnly", Boolean.TRUE.equals(l.getAdminOnly()));
                    return m;
                }).toList();
    }

    private List<Map<String, Object>> giftProgressLogs(Long receptionId, String groupId) {
        return em.createQuery(
                        "select l from HomesReceptionGiftProgressLog l where l.groupId=:g and l.receptionId=:rid order by l.loggedAt desc, l.id desc",
                        HomesReceptionGiftProgressLog.class)
                .setParameter("g", groupId).setParameter("rid", receptionId)
                .getResultList().stream().map(l -> {
                    Map<String, Object> m = new LinkedHashMap<>();
                    m.put("id", l.getId());
                    m.put("status", l.getStatus());
                    m.put("memo", l.getMemo());
                    m.put("authorName", l.getAuthorName());
                    m.put("writer", l.getAuthorName());
                    m.put("loggedAt", fmtDt(l.getLoggedAt()));
                    m.put("createdAt", fmtDt(l.getLoggedAt()));
                    return m;
                }).toList();
    }

    private void appendLog(HomesReception r, String status, String memo, JwtUser u) {
        appendLog(r, status, memo, u, false);
    }

    private void appendLog(HomesReception r, String status, String memo, JwtUser u, boolean adminOnly) {
        HomesReceptionProgressLog log = new HomesReceptionProgressLog();
        log.setGroupId(u.getGroupId());
        log.setReceptionId(r.getId());
        log.setStatus(status);
        log.setMemo(memo);
        log.setAuthorName(resolveMemberName(u));
        log.setLoggedAt(LocalDateTime.now(ZoneId.of("Asia/Seoul")));
        log.setAdminOnly(adminOnly);
        em.persist(log);
    }

    private void appendGiftLog(HomesReception r, String status, String memo, JwtUser u) {
        HomesReceptionGiftProgressLog log = new HomesReceptionGiftProgressLog();
        log.setGroupId(u.getGroupId());
        log.setReceptionId(r.getId());
        log.setStatus(status);
        log.setMemo(memo);
        log.setAuthorName(resolveMemberName(u));
        log.setLoggedAt(LocalDateTime.now(ZoneId.of("Asia/Seoul")));
        em.persist(log);
    }

    private HomesReception findOwned(Long id, JwtUser u) {
        HomesReception r = em.find(HomesReception.class, id);
        if (r == null || !Objects.equals(r.getGroupId(), u.getGroupId())) return null;
        return r;
    }

    private void writeLog(JwtUser u, String domain, String action, String content) {
        HomesLog log = new HomesLog();
        log.setGroupId(u.getGroupId());
        log.setDomain(domain);
        log.setAction(action);
        log.setContent(content);
        String staff = resolveMemberName(u);
        log.setStaff(staff == null || staff.isBlank() ? u.getUserid() : staff);
        log.setProcessedAt(LocalDateTime.now());
        em.persist(log);
    }

    private String resolveMemberName(JwtUser u) {
        List<Member> rows = em.createQuery("select m from Member m where m.groupId=:g and m.userid=:uid", Member.class)
                .setParameter("g", u.getGroupId()).setParameter("uid", u.getUserid()).setMaxResults(1).getResultList();
        return rows.isEmpty() ? u.getUserid() : rows.getFirst().getName();
    }

    private static List<String> distinct(List<HomesReception> list,
                                         java.util.function.Function<HomesReception, String> getter,
                                         List<String> fallback) {
        List<String> values = list.stream().map(getter).filter(s -> s != null && !s.isBlank())
                .distinct().sorted().collect(Collectors.toCollection(ArrayList::new));
        if (values.isEmpty() && fallback != null) return fallback;
        return values;
    }

    private static String genOrderNumber() {
        return "RCP-" + LocalDateTime.now(ZoneId.of("Asia/Seoul"))
                .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
                + "-" + (1000 + new Random().nextInt(9000));
    }

    private static List<Long> toIds(Object raw) {
        if (!(raw instanceof List<?> list)) return List.of();
        List<Long> ids = new ArrayList<>();
        for (Object o : list) {
            if (o == null) continue;
            try { ids.add(Long.valueOf(String.valueOf(o))); } catch (Exception ignored) { }
        }
        return ids;
    }

    private static BigDecimal toDecimal(Object v) {
        if (v == null || String.valueOf(v).isBlank()) return null;
        try { return new BigDecimal(String.valueOf(v).replace(",", "")); }
        catch (Exception e) { return null; }
    }

    private static LocalDate parseDate(String s) {
        if (s == null || s.isBlank()) return null;
        try { return LocalDate.parse(s.substring(0, Math.min(10, s.length()))); }
        catch (Exception e) { return null; }
    }

    private static LocalDateTime parseDateTime(String s) {
        if (s == null || s.isBlank()) return null;
        try {
            if (s.length() <= 10) return LocalDate.parse(s).atStartOfDay();
            return LocalDateTime.parse(s.replace(' ', 'T').substring(0, Math.min(19, s.length())));
        } catch (Exception e) {
            LocalDate d = parseDate(s);
            return d == null ? null : d.atStartOfDay();
        }
    }

    private static String fmtDt(LocalDateTime dt) {
        if (dt == null) return null;
        return dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }

    private static String first(Map<String, String> f, String... keys) {
        for (String k : keys) {
            String v = f.get(k);
            if (v != null && !v.isBlank()) return v;
        }
        return null;
    }

    private static String blankToNull(String s) {
        return s == null || s.isBlank() ? null : s.trim();
    }

    private static boolean blank(String s) {
        return s == null || s.isBlank();
    }

    private static String nullToEmpty(String s) {
        return s == null ? "" : s;
    }

    private static String str(Object v) {
        return v == null ? null : String.valueOf(v).trim();
    }

    private static String firstNonBlank(String... values) {
        if (values == null) return null;
        for (String v : values) {
            if (v != null && !v.isBlank()) return v.trim();
        }
        return null;
    }

    private static String csvCell(Object value) {
        if (value == null) return "";
        String s = String.valueOf(value).replace("\"", "\"\"");
        if (s.contains(",") || s.contains("\"") || s.contains("\n")) return "\"" + s + "\"";
        return s;
    }
}
