package com.zioinfo.kintex.logistics;

import com.zioinfo.kintex.auth.EventRole;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.logistics.dto.LogisticsExtDtos.*;
import com.zioinfo.kintex.tenant.TenantContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * M8 물류 확장 서비스 — F041 장비 신청 · F042 렌탈 주문 · F043 선입고 트래킹.
 *
 * <p><b>금액 서버 권위</b>: 장비 amount = 카탈로그 단가 × 수량 × 일수, 렌탈 amount = 카탈로그 일단가 × 수량 × 기간(일).
 * 단가는 <b>마스터에서 조회</b>하며 요청 값은 신뢰하지 않는다(신청/주문 시점 스냅샷 기록).
 *
 * <p><b>권한</b>: 신청·주문·선입고 등록은 행사 멤버(참가업체 포함). 승인·주문 확정·선입고 상태 전이는 매니저 이상
 * (주최자/홀매니저). 상태 전이 값은 서버 화이트리스트로 검증.
 */
@Service
public class LogisticsExtService {

    private static final Set<String> EQUIP_STATUSES = Set.of("approved", "rejected", "done");
    private static final Set<String> RENTAL_STATUSES = Set.of("confirmed", "cancelled", "delivered");
    private static final Set<String> INBOUND_STATUSES =
            Set.of("inbound_scheduled", "arrived_warehouse", "delivered_booth");

    private final LogisticsExtMapper mapper;

    public LogisticsExtService(LogisticsExtMapper mapper) {
        this.mapper = mapper;
    }

    // ── 통합 조회(탭 보드) ────────────────────────────────────────────────────
    public LogisticsExtBoard board(String eventId, KintexPrincipal principal) {
        String tenant = TenantContext.currentTenantId();
        boolean canManage = isManager(principal, eventId);
        return new LogisticsExtBoard(
                canManage,
                mapper.findEquipmentCatalog(tenant).stream().map(LogisticsExtService::toEquipmentItem).toList(),
                mapper.findEquipmentRequests(tenant, eventId).stream().map(LogisticsExtService::toEquipmentView).toList(),
                mapper.findRentalCatalog(tenant).stream().map(LogisticsExtService::toRentalItem).toList(),
                mapper.findRentalOrders(tenant, eventId).stream().map(LogisticsExtService::toRentalView).toList(),
                mapper.findInbound(tenant, eventId).stream().map(LogisticsExtService::toInboundView).toList()
        );
    }

    // ── ① 장비 신청 ──────────────────────────────────────────────────────────
    @Transactional
    public EquipmentRequestView createEquipmentRequest(String eventId, EquipmentRequestBody req, KintexPrincipal principal) {
        String tenant = TenantContext.currentTenantId();
        Map<String, Object> item = mapper.findEquipmentById(tenant, req.equipmentId());
        if (item == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "선택한 장비 품목을 찾을 수 없습니다.");
        }
        int qty = Math.max(1, req.quantity());
        int days = Math.max(1, req.days());
        long unitPrice = longVal(item.get("unitPrice"));
        long amount = unitPrice * qty * days;

        String id = "leqr-" + UUID.randomUUID();
        Map<String, Object> p = new LinkedHashMap<>();
        p.put("id", id);
        p.put("tenantId", tenant);
        p.put("eventId", eventId);
        p.put("equipmentId", req.equipmentId());
        p.put("equipmentName", str(item.get("name")));
        p.put("unitPrice", unitPrice);
        p.put("boothNo", blankToNull(req.boothNo()));
        p.put("quantity", qty);
        p.put("useDate", parseDateOrNull(req.useDate()));
        p.put("days", days);
        p.put("amount", amount);
        p.put("note", blankToNull(req.note()));
        p.put("userId", principal == null ? null : principal.userId());
        mapper.insertEquipmentRequest(p);
        return findEquipmentView(tenant, eventId, id);
    }

    @Transactional
    public EquipmentRequestView decideEquipmentRequest(String eventId, String id, StatusDecisionBody req, KintexPrincipal principal) {
        requireManager(principal, eventId, "장비 신청 승인/처리는 매니저 이상만 수행할 수 있습니다.");
        String status = normalize(req.status());
        if (!EQUIP_STATUSES.contains(status)) {
            throw new ApiException(ErrorCode.VALIDATION, "장비 신청 상태는 approved|rejected|done 이어야 합니다.");
        }
        String tenant = TenantContext.currentTenantId();
        Map<String, Object> p = new LinkedHashMap<>();
        p.put("tenantId", tenant);
        p.put("eventId", eventId);
        p.put("id", id);
        p.put("status", status);
        p.put("reason", "rejected".equals(status) ? blankToNull(req.reason()) : null);
        p.put("userId", principal == null ? null : principal.userId());
        if (mapper.updateEquipmentStatus(p) == 0) {
            throw new ApiException(ErrorCode.NOT_FOUND, "대상 장비 신청을 찾을 수 없습니다.");
        }
        return findEquipmentView(tenant, eventId, id);
    }

    // ── ② 렌탈 주문 ──────────────────────────────────────────────────────────
    @Transactional
    public RentalOrderView createRentalOrder(String eventId, RentalOrderBody req, KintexPrincipal principal) {
        String tenant = TenantContext.currentTenantId();
        Map<String, Object> item = mapper.findRentalItemById(tenant, req.itemId());
        if (item == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "선택한 렌탈 품목을 찾을 수 없습니다.");
        }
        int qty = Math.max(1, req.quantity());
        int days = resolveDays(req.startDate(), req.endDate(), req.days());
        long dailyPrice = longVal(item.get("dailyPrice"));
        long amount = dailyPrice * qty * days;

        String id = "lro-" + UUID.randomUUID();
        Map<String, Object> p = new LinkedHashMap<>();
        p.put("id", id);
        p.put("tenantId", tenant);
        p.put("eventId", eventId);
        p.put("itemId", req.itemId());
        p.put("itemName", str(item.get("name")));
        p.put("dailyPrice", dailyPrice);
        p.put("boothNo", blankToNull(req.boothNo()));
        p.put("quantity", qty);
        p.put("startDate", parseDateOrNull(req.startDate()));
        p.put("endDate", parseDateOrNull(req.endDate()));
        p.put("days", days);
        p.put("amount", amount);
        p.put("note", blankToNull(req.note()));
        p.put("userId", principal == null ? null : principal.userId());
        mapper.insertRentalOrder(p);
        return findRentalView(tenant, eventId, id);
    }

    @Transactional
    public RentalOrderView decideRentalOrder(String eventId, String id, StatusDecisionBody req, KintexPrincipal principal) {
        requireManager(principal, eventId, "렌탈 주문 확정/처리는 매니저 이상만 수행할 수 있습니다.");
        String status = normalize(req.status());
        if (!RENTAL_STATUSES.contains(status)) {
            throw new ApiException(ErrorCode.VALIDATION, "렌탈 주문 상태는 confirmed|cancelled|delivered 이어야 합니다.");
        }
        String tenant = TenantContext.currentTenantId();
        Map<String, Object> p = new LinkedHashMap<>();
        p.put("tenantId", tenant);
        p.put("eventId", eventId);
        p.put("id", id);
        p.put("status", status);
        p.put("userId", principal == null ? null : principal.userId());
        if (mapper.updateRentalStatus(p) == 0) {
            throw new ApiException(ErrorCode.NOT_FOUND, "대상 렌탈 주문을 찾을 수 없습니다.");
        }
        return findRentalView(tenant, eventId, id);
    }

    // ── ③ 선입고 트래킹 ──────────────────────────────────────────────────────
    @Transactional
    public InboundView registerInbound(String eventId, InboundBody req, KintexPrincipal principal) {
        String tenant = TenantContext.currentTenantId();
        String id = "linb-" + UUID.randomUUID();
        Map<String, Object> p = new LinkedHashMap<>();
        p.put("id", id);
        p.put("tenantId", tenant);
        p.put("eventId", eventId);
        p.put("trackingNo", req.trackingNo().trim());
        p.put("carrier", blankToNull(req.carrier()));
        p.put("boothNo", blankToNull(req.boothNo()));
        p.put("itemDesc", blankToNull(req.itemDesc()));
        p.put("quantity", Math.max(1, req.quantity()));
        p.put("etaDate", parseDateOrNull(req.etaDate()));
        p.put("note", blankToNull(req.note()));
        p.put("userId", principal == null ? null : principal.userId());
        mapper.insertInbound(p);
        return findInboundView(tenant, eventId, id);
    }

    @Transactional
    public InboundView advanceInbound(String eventId, String id, InboundStatusBody req, KintexPrincipal principal) {
        requireManager(principal, eventId, "선입고 상태 전이는 매니저 이상만 수행할 수 있습니다.");
        String status = normalize(req.status());
        if (!INBOUND_STATUSES.contains(status)) {
            throw new ApiException(ErrorCode.VALIDATION,
                    "선입고 상태는 inbound_scheduled|arrived_warehouse|delivered_booth 이어야 합니다.");
        }
        String tenant = TenantContext.currentTenantId();
        Map<String, Object> p = new LinkedHashMap<>();
        p.put("tenantId", tenant);
        p.put("eventId", eventId);
        p.put("id", id);
        p.put("status", status);
        p.put("note", blankToNull(req.note()));
        p.put("userId", principal == null ? null : principal.userId());
        if (mapper.updateInboundStatus(p) == 0) {
            throw new ApiException(ErrorCode.NOT_FOUND, "대상 선입고 건을 찾을 수 없습니다.");
        }
        return findInboundView(tenant, eventId, id);
    }

    // ── 조회 헬퍼(단건 재조회) ────────────────────────────────────────────────
    private EquipmentRequestView findEquipmentView(String tenant, String eventId, String id) {
        return mapper.findEquipmentRequests(tenant, eventId).stream()
                .filter(r -> id.equals(str(r.get("id"))))
                .map(LogisticsExtService::toEquipmentView)
                .findFirst()
                .orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "대상 장비 신청을 찾을 수 없습니다."));
    }

    private RentalOrderView findRentalView(String tenant, String eventId, String id) {
        return mapper.findRentalOrders(tenant, eventId).stream()
                .filter(r -> id.equals(str(r.get("id"))))
                .map(LogisticsExtService::toRentalView)
                .findFirst()
                .orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "대상 렌탈 주문을 찾을 수 없습니다."));
    }

    private InboundView findInboundView(String tenant, String eventId, String id) {
        return mapper.findInbound(tenant, eventId).stream()
                .filter(r -> id.equals(str(r.get("id"))))
                .map(LogisticsExtService::toInboundView)
                .findFirst()
                .orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "대상 선입고 건을 찾을 수 없습니다."));
    }

    // ── row → DTO 매핑 ───────────────────────────────────────────────────────
    private static EquipmentItem toEquipmentItem(Map<String, Object> r) {
        return new EquipmentItem(str(r.get("id")), str(r.get("code")), str(r.get("name")),
                str(r.get("category")), str(r.get("unit")), longVal(r.get("unitPrice")), str(r.get("spec")));
    }

    private static EquipmentRequestView toEquipmentView(Map<String, Object> r) {
        return new EquipmentRequestView(str(r.get("id")), str(r.get("equipmentId")), str(r.get("equipmentName")),
                str(r.get("category")), longVal(r.get("unitPrice")), str(r.get("boothNo")), intVal(r.get("quantity")),
                str(r.get("useDate")), intVal(r.get("days")), longVal(r.get("amount")), str(r.get("status")),
                str(r.get("note")), str(r.get("rejectReason")), str(r.get("createdAt")));
    }

    private static RentalItem toRentalItem(Map<String, Object> r) {
        return new RentalItem(str(r.get("id")), str(r.get("code")), str(r.get("name")),
                str(r.get("category")), str(r.get("unit")), longVal(r.get("dailyPrice")), str(r.get("spec")));
    }

    private static RentalOrderView toRentalView(Map<String, Object> r) {
        return new RentalOrderView(str(r.get("id")), str(r.get("itemId")), str(r.get("itemName")),
                str(r.get("category")), longVal(r.get("dailyPrice")), str(r.get("boothNo")), intVal(r.get("quantity")),
                str(r.get("startDate")), str(r.get("endDate")), intVal(r.get("days")), longVal(r.get("amount")),
                str(r.get("status")), str(r.get("note")), str(r.get("createdAt")));
    }

    private static InboundView toInboundView(Map<String, Object> r) {
        return new InboundView(str(r.get("id")), str(r.get("trackingNo")), str(r.get("carrier")),
                str(r.get("boothNo")), str(r.get("itemDesc")), intVal(r.get("quantity")), str(r.get("status")),
                str(r.get("etaDate")), str(r.get("warehouseAt")), str(r.get("deliveredAt")),
                str(r.get("note")), str(r.get("createdAt")));
    }

    // ── 권한 ─────────────────────────────────────────────────────────────────
    private boolean isManager(KintexPrincipal principal, String eventId) {
        if (principal == null) {
            return false;
        }
        if (principal.hallManager()) {
            return true;
        }
        EventRole role = principal.roleFor(eventId);
        return role == EventRole.ORGANIZER || role == EventRole.HALL_MANAGER;
    }

    private void requireManager(KintexPrincipal principal, String eventId, String message) {
        if (!isManager(principal, eventId)) {
            throw new ApiException(ErrorCode.FORBIDDEN, message);
        }
    }

    // ── 값 헬퍼 ──────────────────────────────────────────────────────────────
    /** 기간(일) 산출 — start/end 유효 시 (end-start+1), 아니면 요청 days(최소 1). */
    private static int resolveDays(String start, String end, int fallbackDays) {
        LocalDate s = tryParse(start);
        LocalDate e = tryParse(end);
        if (s != null && e != null && !e.isBefore(s)) {
            return (int) (ChronoUnit.DAYS.between(s, e) + 1);
        }
        return Math.max(1, fallbackDays);
    }

    private static LocalDate tryParse(String s) {
        String n = blankToNull(s);
        if (n == null) {
            return null;
        }
        try {
            return LocalDate.parse(n);
        } catch (DateTimeParseException e) {
            return null;
        }
    }

    private static String parseDateOrNull(String s) {
        LocalDate d = tryParse(s);
        return d == null ? null : d.toString();
    }

    private static String normalize(String s) {
        return s == null ? "" : s.trim().toLowerCase();
    }

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

    private static int intVal(Object o) {
        if (o instanceof Number n) return n.intValue();
        if (o == null) return 0;
        try {
            return Integer.parseInt(String.valueOf(o));
        } catch (NumberFormatException e) {
            return 0;
        }
    }

    private static long longVal(Object o) {
        if (o instanceof Number n) return n.longValue();
        if (o == null) return 0L;
        try {
            return Long.parseLong(String.valueOf(o));
        } catch (NumberFormatException e) {
            return 0L;
        }
    }

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