package com.zioinfo.kintex.rules;

import org.springframework.stereotype.Service;

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

/**
 * 규정 룰 엔진 — 룰셋 데이터(규정) + 측정값 맵을 평가해 {@link ComplianceReport}를 산출한다.
 *
 * <p>측정값 맵({@code metrics})은 서비스 계층이 조립한다: 공간값(최소 통로 폭·바닥하중·비상구 차단 수 등)은
 * PostGIS 매퍼 쿼리 결과(kintex-db-engineer), 그 외(높이·방염 여부·간판 등)는 요청/설계 데이터에서.
 * 엔진은 데이터 소스를 모르는 순수 평가기이므로 규정 개정 시 JSON만 교체하면 된다.
 *
 * <p>지원 연산자: lte, gte, between, isTrue, eq, lteHall(홀별 상한), excludesAll(금지 목록 미포함).
 * 측정값이 없는 규칙은 "미평가(=통과로 집계)"로 처리하되 리포트 카운트에는 반영하지 않는다.
 */
@Service
public class ComplianceRuleEngine {

    private final RuleSetLoader ruleSets;

    public ComplianceRuleEngine(RuleSetLoader ruleSets) {
        this.ruleSets = ruleSets;
    }

    /**
     * @param module   대상 모듈("M2"|"M3"|"M4")
     * @param metrics  측정값 맵(예: {"booth.height_m":5.4, "layout.min_aisle_width_m":2.8, ...})
     * @param hallId   홀 식별자(홀별 상한 규칙용, 없으면 null)
     */
    public ComplianceReport evaluate(String module, Map<String, Object> metrics, String hallId) {
        ComplianceRuleSet rs = ruleSets.compliance();
        if (rs == null || rs.rules() == null) {
            return new ComplianceReport("compliance-unloaded", null, 0, 0, 0, true, List.of());
        }
        List<Violation> violations = new ArrayList<>();
        int pass = 0;
        int pin = 0;

        for (ComplianceRuleSet.Rule rule : rs.rules()) {
            if (rule.module() != null && !rule.module().contains(module)) {
                continue;
            }
            Object value = metrics == null ? null : metrics.get(rule.metric());
            Boolean ok = evaluateRule(rule, value, hallId);
            if (ok == null) {
                continue; // 미평가(측정값 없음)
            }
            if (ok) {
                pass++;
            } else {
                pin++;
                violations.add(new Violation(
                        pin, rule.code(), rule.group(), rule.label(),
                        rule.severity(), describeMeasured(rule, value), rule.requiresDocument()));
            }
        }

        int blocks = (int) violations.stream().filter(v -> "block".equals(v.severity())).count();
        int warns = violations.size() - blocks;
        boolean submittable = blocks == 0;
        return new ComplianceReport(rs.rulesetVersion(), rs.disclaimer(),
                blocks, warns, pass, submittable, violations);
    }

    /** @return true=통과, false=위반, null=미평가(측정값 없음). */
    private Boolean evaluateRule(ComplianceRuleSet.Rule rule, Object value, String hallId) {
        String op = rule.operator();
        if (op == null) {
            return null;
        }
        switch (op) {
            case "lte": {
                Double v = asDouble(value);
                return v == null ? null : v <= nz(rule.threshold());
            }
            case "gte": {
                Double v = asDouble(value);
                return v == null ? null : v >= nz(rule.threshold());
            }
            case "eq": {
                Double v = asDouble(value);
                return v == null ? null : v.doubleValue() == nz(rule.threshold());
            }
            case "between": {
                Double v = asDouble(value);
                if (v == null) return null;
                return v >= nz(rule.min()) && v <= nz(rule.max());
            }
            case "isTrue": {
                Boolean b = asBoolean(value);
                return b == null ? null : b;
            }
            case "lteHall": {
                Double v = asDouble(value);
                if (v == null) return null;
                Map<String, Double> limits = rule.hallLimits();
                double limit = limits == null ? Double.MAX_VALUE
                        : limits.getOrDefault(hallId, limits.getOrDefault("default", Double.MAX_VALUE));
                return v <= limit;
            }
            case "excludesAll": {
                if (!(value instanceof Collection<?> col)) return null;
                List<String> forbidden = rule.forbidden();
                if (forbidden == null) return true;
                return col.stream().map(String::valueOf).noneMatch(forbidden::contains);
            }
            default:
                return null;
        }
    }

    private String describeMeasured(ComplianceRuleSet.Rule rule, Object value) {
        if (value == null) return null;
        String unit = rule.unit() == null ? "" : rule.unit();
        return "측정 " + value + unit;
    }

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

    private static Double asDouble(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 asBoolean(Object o) {
        if (o == null) return null;
        if (o instanceof Boolean b) return b;
        String s = String.valueOf(o).trim().toLowerCase();
        if (s.equals("true") || s.equals("false")) return Boolean.parseBoolean(s);
        return null;
    }
}
