package com.zioinfo.kintex.boothsales;

import com.zioinfo.kintex.auth.EventRole;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.boothsales.dto.BoothSalesDtos.*;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.settlement.SettlementService;
import com.zioinfo.kintex.settlement.dto.SettlementDtos.GenerateResult;
import com.zioinfo.kintex.tenant.TenantContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * F005/F006 부스 판매 인벤토리·온라인 셀프 판매 서비스.
 *
 * <p><b>판매 상태 오버레이</b>: 기존 M2 booth(좌표, read-only) 위에 판매 상태(available|held|sold|blocked)를
 * 얹는다(booth_sale). M2 스키마/데이터는 불변 — 이 서비스는 booth 좌표를 <b>읽기만</b> 한다.
 *
 * <p><b>가격은 서버 권위</b>: 기본 = 판매면적(ST_Area) × 요율(rates-v1.0 exhibitionHallPerM2). 주최자 override 가능.
 *
 * <p><b>동시성</b>: hold/purchase 는 매퍼의 원자 UPDATE(WHERE status 조건)로 전이한다. 같은 부스 이중 hold 는
 * 409. hold 만료는 조회 시 lazy(SQL CASE)로 available 강등된다.
 *
 * <p><b>권한</b>: 인벤토리 조회·hold·구매는 행사 멤버(참가업체 포함). blocked/가격 관리·정산 연계는 매니저 이상
 * (주최자 또는 홀매니저). 참가업체 시점 인벤토리는 available(+ 본인 hold)만 노출한다.
 *
 * <p><b>M9 정산 연계</b>: 구매 확정은 booth_sale 에 권위 판매가를 기록한다. 청구서 생성은 매니저가
 * {@link #settle}(→ {@link SettlementService#generate})로 행사 참가업체별 집계를 트리거한다 —
 * settlement 공개 서비스 메서드 <b>호출/생성만</b>, 내부 수정 없음.
 */
@Service
public class BoothSalesService {

    /** hold 유지 시간(분) — 서버 고정 TTL. 경과 시 lazy available. */
    private static final int HOLD_TTL_MINUTES = 15;

    private static final String RULESET_VERSION = "rates-v1.0";

    private final BoothSalesMapper mapper;
    private final SettlementService settlementService;

    public BoothSalesService(BoothSalesMapper mapper, SettlementService settlementService) {
        this.mapper = mapper;
        this.settlementService = settlementService;
    }

    // ── 인벤토리 조회 ────────────────────────────────────────────────────────────
    /**
     * 행사(+선택 홀)의 부스 판매 인벤토리. 참가업체 시점은 available(+본인 hold)만, 매니저는 전 상태.
     */
    public InventoryResponse inventory(String eventId, String hallId, KintexPrincipal principal) {
        String tenant = TenantContext.currentTenantId();
        boolean canManage = isManager(principal, eventId);
        String userId = principal == null ? null : principal.userId();
        long rate = ratePerM2();

        List<Map<String, Object>> rows = mapper.findInventory(tenant, eventId, blankToNull(hallId));
        List<BoothSaleUnit> units = new ArrayList<>();
        int available = 0, held = 0, sold = 0, blocked = 0;

        for (Map<String, Object> r : rows) {
            String status = str(r.get("status"));
            boolean heldByMe = userId != null && userId.equals(str(r.get("heldBy")));

            switch (status == null ? "available" : status) {
                case "held" -> held++;
                case "sold" -> sold++;
                case "blocked" -> blocked++;
                default -> available++;
            }

            // 참가업체 시점: available 또는 본인이 hold 중인 부스만 노출(구매 진행 UX).
            if (!canManage) {
                boolean visible = "available".equals(status) || ("held".equals(status) && heldByMe);
                if (!visible) {
                    continue;
                }
            }
            units.add(toUnit(r, rate, heldByMe));
        }

        return new InventoryResponse(eventId, blankToNull(hallId), RULESET_VERSION, rate,
                available, held, sold, blocked, canManage, HOLD_TTL_MINUTES, units);
    }

    // ── hold (참가업체 셀프 선택) ──────────────────────────────────────────────────
    @Transactional
    public BoothSaleResult hold(HoldRequest req, KintexPrincipal principal) {
        requireMember(principal, req.eventId());
        Map<String, Object> booth = requireBooth(req.eventId(), req.boothId());
        String tenant = TenantContext.currentTenantId();

        Map<String, Object> p = base(tenant, req.eventId(), req.boothId(), booth, principal);
        p.put("ttlMinutes", HOLD_TTL_MINUTES);
        int n = mapper.holdBooth(p);
        if (n == 0) {
            // sold/blocked/타인 활성 hold → 이중 hold 충돌.
            throw new ApiException(ErrorCode.CONFLICT, "이미 다른 사용자가 선점했거나 판매/차단된 부스입니다.");
        }
        return result("held", req.eventId(), req.boothId(), principal);
    }

    // ── 구매 확정 (hold→sold) ──────────────────────────────────────────────────────
    @Transactional
    public BoothSaleResult purchase(PurchaseRequest req, KintexPrincipal principal) {
        requireMember(principal, req.eventId());
        Map<String, Object> booth = requireBooth(req.eventId(), req.boothId());
        String tenant = TenantContext.currentTenantId();

        long price = computePrice(dbl(booth.get("areaM2")), ratePerM2());
        String company = firstNonBlank(req.company(), str(booth.get("assignedCompany")),
                principal == null ? null : principal.displayName());

        Map<String, Object> p = new HashMap<>();
        p.put("tenantId", tenant);
        p.put("eventId", req.eventId());
        p.put("boothId", req.boothId());
        p.put("userId", principal == null ? null : principal.userId());
        p.put("company", company);
        p.put("price", price);
        int n = mapper.purchaseBooth(p);
        if (n == 0) {
            throw new ApiException(ErrorCode.CONFLICT,
                    "구매를 확정할 수 없습니다 — hold 가 만료되었거나 다른 사용자가 선점한 부스입니다.");
        }
        return result("purchased", req.eventId(), req.boothId(), principal);
    }

    /** 본인 hold 해제(available 복원). */
    @Transactional
    public BoothSaleResult release(HoldRequest req, KintexPrincipal principal) {
        requireMember(principal, req.eventId());
        String tenant = TenantContext.currentTenantId();
        Map<String, Object> p = new HashMap<>();
        p.put("tenantId", tenant);
        p.put("eventId", req.eventId());
        p.put("boothId", req.boothId());
        p.put("userId", principal == null ? null : principal.userId());
        mapper.releaseHold(p);
        return result("released", req.eventId(), req.boothId(), principal);
    }

    // ── 주최자: blocked/available 전환 ─────────────────────────────────────────────
    @Transactional
    public BoothSaleResult setBlocked(BlockRequest req, KintexPrincipal principal) {
        requireManager(principal, req.eventId());
        Map<String, Object> booth = requireBooth(req.eventId(), req.boothId());
        String tenant = TenantContext.currentTenantId();
        Map<String, Object> p = base(tenant, req.eventId(), req.boothId(), booth, principal);

        if (req.blocked()) {
            p.put("reason", blankToNull(req.reason()));
            int n = mapper.blockBooth(p);
            if (n == 0) {
                throw new ApiException(ErrorCode.CONFLICT, "이미 판매된 부스는 차단할 수 없습니다.");
            }
            return result("blocked", req.eventId(), req.boothId(), principal);
        }
        mapper.unblockBooth(p);
        return result("unblocked", req.eventId(), req.boothId(), principal);
    }

    // ── 주최자: 판매가 override ────────────────────────────────────────────────────
    @Transactional
    public BoothSaleResult setPrice(PriceRequest req, KintexPrincipal principal) {
        requireManager(principal, req.eventId());
        Map<String, Object> booth = requireBooth(req.eventId(), req.boothId());
        if (req.unitPrice() != null && req.unitPrice() < 0) {
            throw new ApiException(ErrorCode.VALIDATION, "판매가는 0 이상이어야 합니다.");
        }
        String tenant = TenantContext.currentTenantId();
        Map<String, Object> p = base(tenant, req.eventId(), req.boothId(), booth, principal);
        p.put("unitPrice", req.unitPrice());
        mapper.setPrice(p);
        return result("priced", req.eventId(), req.boothId(), principal);
    }

    // ── M9 정산 청구 생성 연계(매니저) ────────────────────────────────────────────
    /**
     * 판매 확정분에 대한 M9 청구서 집계를 트리거한다(참가업체별 임대료+유틸리티).
     * settlement 공개 서비스 메서드 호출(생성만) — 내부 수정 없음. 매니저 이상만.
     */
    public GenerateResult settle(String eventId, KintexPrincipal principal) {
        requireManager(principal, eventId);
        return settlementService.generate(eventId, principal);
    }

    // ── 헬퍼 ─────────────────────────────────────────────────────────────────────
    private BoothSaleResult result(String action, String eventId, String boothId, KintexPrincipal principal) {
        String tenant = TenantContext.currentTenantId();
        Map<String, Object> r = mapper.findUnit(tenant, eventId, boothId);
        if (r == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "부스를 찾을 수 없습니다.");
        }
        boolean heldByMe = principal != null && principal.userId() != null
                && principal.userId().equals(str(r.get("heldBy")));
        return new BoothSaleResult(action, toUnit(r, ratePerM2(), heldByMe));
    }

    private BoothSaleUnit toUnit(Map<String, Object> r, long rate, boolean heldByMe) {
        double area = dbl(r.get("areaM2"));
        Long override = lngOrNull(r.get("unitPrice"));
        long price = override != null ? override : computePrice(area, rate);
        return new BoothSaleUnit(
                str(r.get("boothId")),
                str(r.get("boothNo")),
                str(r.get("boothType")),
                str(r.get("hallId")),
                str(r.get("hallLabel")),
                area,
                str(r.get("status")),
                price,
                heldByMe,
                str(r.get("heldUntil")),
                str(r.get("soldToCompany")));
    }

    /** 전이 매퍼 파라미터 공통(부스 스냅샷 포함). */
    private Map<String, Object> base(String tenant, String eventId, String boothId,
                                     Map<String, Object> booth, KintexPrincipal principal) {
        Map<String, Object> p = new HashMap<>();
        p.put("id", "bs-" + UUID.randomUUID().toString().substring(0, 12));
        p.put("tenantId", tenant);
        p.put("eventId", eventId);
        p.put("boothId", boothId);
        p.put("hallId", str(booth.get("hallId")));
        p.put("boothNo", str(booth.get("boothNo")));
        p.put("areaM2", dbl(booth.get("areaM2")));
        p.put("userId", principal == null ? null : principal.userId());
        return p;
    }

    private Map<String, Object> requireBooth(String eventId, String boothId) {
        Map<String, Object> booth = mapper.findBooth(eventId, boothId);
        if (booth == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "부스를 찾을 수 없습니다.");
        }
        return booth;
    }

    /** 판매가 = round(면적 × 요율/㎡). 요율 미로드 시 0(면적만 노출). */
    private static long computePrice(double areaM2, long ratePerM2) {
        return Math.round(areaM2 * ratePerM2);
    }

    private long ratePerM2() {
        Double r = mapper.findRatePerM2();
        return r == null ? 0L : Math.round(r);
    }

    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) {
        if (!isManager(principal, eventId)) {
            throw new ApiException(ErrorCode.FORBIDDEN, "부스 판매 관리는 주최자/홀매니저만 수행할 수 있습니다.");
        }
    }

    /** 행사 멤버(참가업체 포함) 확인 — 비멤버는 403. */
    private void requireMember(KintexPrincipal principal, String eventId) {
        if (principal == null) {
            throw new ApiException(ErrorCode.UNAUTHORIZED);
        }
        if (!principal.hasAccess(eventId)) {
            throw new ApiException(ErrorCode.FORBIDDEN);
        }
    }

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

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

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

    private static double dbl(Object o) {
        if (o == null) return 0.0;
        if (o instanceof Number n) return n.doubleValue();
        try {
            return Double.parseDouble(String.valueOf(o));
        } catch (NumberFormatException e) {
            return 0.0;
        }
    }

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