package com.zioinfo.kintex.module.m2;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.common.geo.GeometryCodec;
import com.zioinfo.kintex.module.m2.dto.ApplyOptionRequest;
import com.zioinfo.kintex.module.m2.dto.AutoLayoutOption;
import com.zioinfo.kintex.module.m2.dto.AutoLayoutRequest;
import com.zioinfo.kintex.module.m2.dto.BoothDto;
import com.zioinfo.kintex.module.m2.dto.HallInfo;
import com.zioinfo.kintex.module.m2.dto.LayoutDto;
import com.zioinfo.kintex.module.m2.dto.LayoutSaveRequest;
import com.zioinfo.kintex.module.m2.dto.LayoutSummary;
import com.zioinfo.kintex.module.m2.mapper.BoothMapper;
import com.zioinfo.kintex.module.m2.mapper.HallMapper;
import com.zioinfo.kintex.module.m5.RenderJobService;
import com.zioinfo.kintex.module.m5.dto.RenderJobRequest;
import com.zioinfo.kintex.rules.ComplianceReport;
import com.zioinfo.kintex.rules.ComplianceRuleEngine;
import com.zioinfo.kintex.rules.Violation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * M2 플로어플랜 서비스 — BoothMapper(PostGIS)·HallMapper 배선 완료.
 * 배치 조회/저장(폴리곤 반영)·규정 검증(통로 폭·비상구·높이·바닥하중 공간 산출)·자동배치 3안 생성/적용을 수행한다.
 *
 * <p>규정 엔진({@link ComplianceRuleEngine})은 순수 평가기 — 서비스가 PostGIS 산출값으로 metrics를 조립한다.
 * 위반 항목에는 도면 핀 매핑용 boothId/anchor(계약 확장)를 부스 단위 규칙에 한해 부여한다.
 */
@Service
public class FloorplanServiceImpl implements FloorplanService {

    private static final Logger log = LoggerFactory.getLogger(FloorplanServiceImpl.class);

    /** 자동배치 기본 부스 규격·높이·하중(규정 상한 이내로 무위반 골격). */
    private static final double DEFAULT_HEIGHT_M = 3.0;
    private static final double DEFAULT_FLOOR_LOAD = 1.0;
    private static final double PERIMETER_MARGIN_M = 5.0; // 홀 외곽 주통로 여유

    private final BoothMapper boothMapper;
    private final HallMapper hallMapper;
    private final ComplianceRuleEngine ruleEngine;
    private final RenderJobService renderJobService;
    private final ObjectMapper objectMapper;

    public FloorplanServiceImpl(BoothMapper boothMapper, HallMapper hallMapper,
                                ComplianceRuleEngine ruleEngine, RenderJobService renderJobService,
                                ObjectMapper objectMapper) {
        this.boothMapper = boothMapper;
        this.hallMapper = hallMapper;
        this.ruleEngine = ruleEngine;
        this.renderJobService = renderJobService;
        this.objectMapper = objectMapper;
    }

    // ---------------------------------------------------------------- 조회 -----

    @Override
    public LayoutDto getLayout(String eventId, String hallId, Integer version) {
        Map<String, Object> header = boothMapper.findLayout(eventId, hallId, version);
        if (header == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "해당 홀의 배치안을 찾을 수 없습니다.");
        }
        return assembleLayout(eventId, hallId, header);
    }

    // ---------------------------------------------------------------- 저장 -----

    @Override
    @Transactional
    public LayoutDto saveLayout(String eventId, String hallId, LayoutSaveRequest request) {
        return persistBooths(eventId, hallId, request.name(), request.version(), request.booths());
    }

    @Override
    @Transactional
    public LayoutDto applyOption(String eventId, String hallId, ApplyOptionRequest request) {
        // 적용/병합은 항상 새 버전으로 저장(version=null → 다음 버전) 후 규정 재검증.
        String name = request.name() == null ? "자동배치 적용" : request.name();
        return persistBooths(eventId, hallId, name, null, request.booths());
    }

    /** 배치안 upsert + 부스 폴리곤 교체 저장 → 규정 검증 반영된 LayoutDto. */
    private LayoutDto persistBooths(String eventId, String hallId, String name,
                                    Integer requestedVersion, List<BoothDto> booths) {
        int version = requestedVersion != null ? requestedVersion : nextVersion(eventId, hallId);

        Map<String, Object> params = new LinkedHashMap<>();
        params.put("eventId", eventId);
        params.put("hallId", hallId);
        params.put("version", version);
        params.put("name", name);
        params.put("status", "draft");
        String layoutId = boothMapper.upsertLayout(params);

        List<Map<String, Object>> boothRows = new ArrayList<>();
        for (BoothDto b : booths) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("boothId", b.boothId());
            row.put("boothNo", b.boothNo());
            row.put("type", b.type());
            row.put("wkt", GeometryCodec.toPolygonWkt(b.polygon()));
            row.put("sizeW", b.sizeM() != null && b.sizeM().size() > 0 ? b.sizeM().get(0) : null);
            row.put("sizeD", b.sizeM() != null && b.sizeM().size() > 1 ? b.sizeM().get(1) : null);
            row.put("heightM", b.heightM());
            row.put("floorLoad", b.floorLoadTPerM2());
            row.put("assignedCompanyName", b.assignedCompanyName());
            row.put("premium", b.premium());
            boothRows.add(row);
        }
        boothMapper.replaceBooths(layoutId, boothRows);

        Map<String, Object> header = boothMapper.findLayout(eventId, hallId, version);
        if (header == null) {
            throw new ApiException(ErrorCode.INTERNAL, "배치안 저장 결과를 확인할 수 없습니다.");
        }
        log.info("배치 저장: event={} hall={} version={} booths={}", eventId, hallId, version, booths.size());
        return assembleLayout(eventId, hallId, header);
    }

    // ------------------------------------------------------------ 규정 검증 -----

    @Override
    public ComplianceReport validate(String eventId, String hallId, Integer version) {
        Map<String, Object> header = boothMapper.findLayout(eventId, hallId, version);
        if (header == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "검증할 배치안을 찾을 수 없습니다.");
        }
        String layoutId = str(header.get("layoutId"));
        return evaluateLayout(layoutId, hallId, loadBooths(layoutId));
    }

    /** PostGIS 산출값으로 metrics 조립 → 룰 엔진 평가 → 부스 단위 위반에 도면 핀 앵커 부여. */
    private ComplianceReport evaluateLayout(String layoutId, String hallId, List<BoothDto> booths) {
        Map<String, Object> metrics = new LinkedHashMap<>();

        Double minAisle = boothMapper.minAisleWidth(layoutId);
        if (minAisle != null) {
            metrics.put("layout.min_aisle_width_m", round2(minAisle));
        }
        Integer exitsBlocked = boothMapper.countExitsBlocked(layoutId, hallId);
        metrics.put("layout.exits_blocked_count", exitsBlocked == null ? 0 : exitsBlocked);

        Integer overlaps = boothMapper.countOverlappingBooths(layoutId);
        metrics.put("layout.overlap_count", overlaps == null ? 0 : overlaps);

        BoothDto maxHeight = maxBy(booths, BoothDto::heightM);
        if (maxHeight != null && maxHeight.heightM() != null) {
            metrics.put("booth.height_m", maxHeight.heightM());
        }
        BoothDto maxLoad = maxBy(booths, BoothDto::floorLoadTPerM2);
        if (maxLoad != null && maxLoad.floorLoadTPerM2() != null) {
            metrics.put("booth.floor_load_t_per_m2", maxLoad.floorLoadTPerM2());
        }

        ComplianceReport report = ruleEngine.evaluate("M2", metrics, hallId);
        List<Violation> located = attachAnchors(report.violations(), maxHeight, maxLoad);
        return new ComplianceReport(report.rulesetVersion(), report.disclaimer(),
                report.blockCount(), report.warnCount(), report.passCount(),
                report.submittable(), located);
    }

    /** 부스 단위 규칙(HEIGHT_MAX·FLOOR_LOAD) 위반에 해당 부스 id·중심 앵커 부여(계약 확장, 프론트 gap #1). */
    private List<Violation> attachAnchors(List<Violation> violations, BoothDto heightBooth, BoothDto loadBooth) {
        if (violations == null || violations.isEmpty()) {
            return violations;
        }
        List<Violation> out = new ArrayList<>(violations.size());
        for (Violation v : violations) {
            BoothDto b = switch (v.code() == null ? "" : v.code()) {
                case "HEIGHT_MAX" -> heightBooth;
                case "FLOOR_LOAD" -> loadBooth;
                default -> null;
            };
            if (b != null) {
                out.add(v.withLocation(b.boothId(), GeometryCodec.centroid(b.polygon())));
            } else {
                out.add(v);
            }
        }
        return out;
    }

    // -------------------------------------------------------- 자동배치 3안 -----

    @Override
    public List<AutoLayoutOption> autoGenerate(String eventId, String hallId, AutoLayoutRequest request) {
        HallInfo hall = loadHallInfo(hallId);
        if (hall == null || hall.dimsM() == null || hall.dimsM().size() < 2) {
            throw new ApiException(ErrorCode.NOT_FOUND, "홀 규격을 찾을 수 없어 자동배치를 생성할 수 없습니다.");
        }
        int optionCount = Math.max(1, request.optionCount());
        double hallW = hall.dimsM().get(0);
        double hallD = hall.dimsM().get(1);

        // 3안: 통로 폭·부스 규격을 달리해 서로 구분되는 후보를 결정적으로 생성.
        double[][] variants = {
                {3.0, 3.0, 3.0},   // A: 표준 3×3, 통로 3.0m
                {3.0, 3.0, 3.5},   // B: 표준 3×3, 통로 3.5m(넓은 피난)
                {6.0, 3.0, 3.0},   // C: 6×3 프리미엄 지향, 통로 3.0m
        };
        List<AutoLayoutOption> options = new ArrayList<>();
        for (int i = 0; i < optionCount; i++) {
            double[] vp = variants[i % variants.length];
            char tag = (char) ('A' + i);
            List<BoothDto> booths = packBooths(vp[0], vp[1], vp[2], hallW, hallD, request, tag);

            double salesArea = booths.stream()
                    .mapToDouble(b -> vp[0] * vp[1]).sum();
            LayoutSummary summary = new LayoutSummary(
                    booths.size(), request.targetBoothCount(), round2(salesArea),
                    vp[2], 0, 0);

            String s7JobId = publishS7Preview(eventId, hallId, hall, tag);
            options.add(new AutoLayoutOption("opt-" + tag, "배치안 " + tag, summary, booths, s7JobId));
        }
        log.info("자동배치 생성: event={} hall={} options={} target={}",
                eventId, hallId, options.size(), request.targetBoothCount());
        return options;
    }

    /** 제약 기반 그리드 패킹 — 외곽 주통로·통로 폭·프리미엄 비율을 반영해 목표 수까지 배치. */
    private List<BoothDto> packBooths(double boothW, double boothD, double aisle,
                                      double hallW, double hallD, AutoLayoutRequest req, char tag) {
        List<BoothDto> booths = new ArrayList<>();
        int target = req.targetBoothCount();
        int premiumTarget = (int) Math.round(target * clamp01(req.premiumRatio()));
        double stepX = boothW + aisle;
        double stepY = boothD + aisle;
        double usableW = hallW - 2 * PERIMETER_MARGIN_M;
        double usableD = hallD - 2 * PERIMETER_MARGIN_M;

        int index = 0;
        for (double y = PERIMETER_MARGIN_M; y + boothD <= PERIMETER_MARGIN_M + usableD && index < target; y += stepY) {
            for (double x = PERIMETER_MARGIN_M; x + boothW <= PERIMETER_MARGIN_M + usableW && index < target; x += stepX) {
                index++;
                boolean premium = index <= premiumTarget;
                String boothNo = String.format("%c-%03d", tag, index);
                List<List<Double>> polygon = rect(x, y, boothW, boothD);
                booths.add(new BoothDto(null, boothNo, premium ? "island" : "assembled",
                        polygon, List.of(boothW, boothD), DEFAULT_HEIGHT_M, DEFAULT_FLOOR_LOAD,
                        null, premium));
            }
        }
        return booths;
    }

    /** S7 홀 전경(조감) 프리뷰 발행 — 렌더 인프라 장애 시에도 자동배치가 성립하도록 degraded(null) 허용. */
    private String publishS7Preview(String eventId, String hallId, HallInfo hall, char tag) {
        try {
            Map<String, Object> scene = new LinkedHashMap<>();
            Map<String, Object> hallScene = new LinkedHashMap<>();
            hallScene.put("id", hallId);
            hallScene.put("dims_m", hall.dimsM());
            if (hall.ceilingM() != null) {
                hallScene.put("ceiling_m", hall.ceilingM());
            }
            scene.put("hall", hallScene);
            scene.put("option", "배치안 " + tag);
            RenderJobRequest req = new RenderJobRequest("S7", scene, null);
            return renderJobService.publish(eventId, "HALL-" + hallId, req).jobId();
        } catch (RuntimeException e) {
            log.warn("S7 프리뷰 발행 실패(degraded): event={} hall={} 사유={}", eventId, hallId, e.getMessage());
            return null;
        }
    }

    // ------------------------------------------------------------- 조립 유틸 -----

    private LayoutDto assembleLayout(String eventId, String hallId, Map<String, Object> header) {
        String layoutId = str(header.get("layoutId"));
        List<BoothDto> booths = loadBooths(layoutId);
        HallInfo hall = loadHallInfo(hallId);
        ComplianceReport report = evaluateLayout(layoutId, hallId, booths);

        Double salesArea = boothMapper.sumSalesArea(layoutId);
        Double minAisle = boothMapper.minAisleWidth(layoutId);
        LayoutSummary summary = new LayoutSummary(
                booths.size(),
                hall != null && hall.targetBoothCount() != null ? hall.targetBoothCount() : 0,
                salesArea == null ? 0.0 : round2(salesArea),
                minAisle == null ? 0.0 : round2(minAisle),
                report.blockCount(), report.warnCount());

        return new LayoutDto(layoutId, eventId, hallId,
                intVal(header.get("version")), str(header.get("name")), str(header.get("status")),
                booths, hall, summary, str(header.get("updatedAt")));
    }

    private List<BoothDto> loadBooths(String layoutId) {
        List<Map<String, Object>> rows = boothMapper.findBooths(layoutId);
        List<BoothDto> out = new ArrayList<>();
        if (rows == null) {
            return out;
        }
        for (Map<String, Object> r : rows) {
            List<List<Double>> polygon = GeometryCodec.parsePolygonRing(objectMapper, str(r.get("polygonJson")));
            Double sw = dbl(r.get("sizeW"));
            Double sd = dbl(r.get("sizeD"));
            List<Double> sizeM = sw != null && sd != null ? List.of(sw, sd) : null;
            out.add(new BoothDto(
                    str(r.get("boothId")), str(r.get("boothNo")), str(r.get("type")),
                    polygon, sizeM, dbl(r.get("heightM")), dbl(r.get("floorLoadTPerM2")),
                    str(r.get("assignedCompanyName")), Boolean.TRUE.equals(bool(r.get("premium")))));
        }
        return out;
    }

    private HallInfo loadHallInfo(String hallId) {
        Map<String, Object> h = hallMapper.findHall(hallId, com.zioinfo.kintex.tenant.TenantContext.currentTenantId());
        if (h == null) {
            return null;
        }
        Double w = dbl(h.get("widthM"));
        Double d = dbl(h.get("depthM"));
        List<Double> dims = w != null && d != null ? List.of(w, d) : null;

        List<HallInfo.Trench> trenches = new ArrayList<>();
        List<Map<String, Object>> tr = hallMapper.findTrenches(hallId);
        if (tr != null) {
            for (Map<String, Object> t : tr) {
                List<String> supplies = new ArrayList<>();
                if (Boolean.TRUE.equals(bool(t.get("supplyPower")))) supplies.add("power");
                if (Boolean.TRUE.equals(bool(t.get("supplyWater")))) supplies.add("water");
                if (Boolean.TRUE.equals(bool(t.get("supplyAir")))) supplies.add("air");
                if (Boolean.TRUE.equals(bool(t.get("supplyNetwork")))) supplies.add("network");
                if (Boolean.TRUE.equals(bool(t.get("supplyGas")))) supplies.add("gas");
                trenches.add(new HallInfo.Trench(str(t.get("trenchId")),
                        nz(dbl(t.get("x"))), nz(dbl(t.get("y"))),
                        Boolean.TRUE.equals(bool(t.get("assumed"))), supplies));
            }
        }
        List<HallInfo.Exit> exits = new ArrayList<>();
        List<Map<String, Object>> ex = hallMapper.findExits(hallId);
        if (ex != null) {
            for (Map<String, Object> e : ex) {
                exits.add(new HallInfo.Exit(nz(dbl(e.get("x"))), nz(dbl(e.get("y"))),
                        nz(dbl(e.get("clearanceM")))));
            }
        }
        return new HallInfo(str(h.get("hallId")), str(h.get("label")), dims, dbl(h.get("heightM")),
                intOrNull(h.get("boothCapacity")), dbl(h.get("floorLoadTPerM2")),
                Boolean.TRUE.equals(bool(h.get("assumedTrench"))), trenches, exits);
    }

    private int nextVersion(String eventId, String hallId) {
        Map<String, Object> latest = boothMapper.findLayout(eventId, hallId, null);
        if (latest == null) {
            return 1;
        }
        return intVal(latest.get("version")) + 1;
    }

    // --------------------------------------------------------- 좌표/형변환 -----

    private static List<List<Double>> rect(double x, double y, double w, double d) {
        return List.of(List.of(x, y), List.of(x + w, y), List.of(x + w, y + d),
                List.of(x, y + d), List.of(x, y));
    }

    private static BoothDto maxBy(List<BoothDto> booths, java.util.function.Function<BoothDto, Double> f) {
        BoothDto best = null;
        double bestVal = Double.NEGATIVE_INFINITY;
        for (BoothDto b : booths) {
            Double v = f.apply(b);
            if (v != null && v > bestVal) {
                bestVal = v;
                best = b;
            }
        }
        return best;
    }

    private static double clamp01(double v) {
        return v < 0 ? 0 : Math.min(v, 1);
    }

    private static double round2(double v) {
        return Math.round(v * 100.0) / 100.0;
    }

    private static double nz(Double d) {
        return d == null ? 0.0 : d;
    }

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

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

    private static Boolean bool(Object o) {
        if (o == null) return null;
        if (o instanceof Boolean b) return b;
        return Boolean.parseBoolean(String.valueOf(o));
    }

    private static int intVal(Object o) {
        Double d = dbl(o);
        return d == null ? 0 : d.intValue();
    }

    private static Integer intOrNull(Object o) {
        Double d = dbl(o);
        return d == null ? null : d.intValue();
    }
}
