package com.zioinfo.kintex.ai.nlquery;

import com.zioinfo.kintex.ai.AiTextRouter;
import com.zioinfo.kintex.ai.AiTextRouter.AiResult;
import com.zioinfo.kintex.ai.nlquery.dto.NlQueryDtos.RuleCitation;
import com.zioinfo.kintex.ai.nlquery.dto.NlQueryDtos.RulesChatResult;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.rules.ComplianceRuleSet;
import com.zioinfo.kintex.rules.RateRuleSet;
import com.zioinfo.kintex.rules.RuleSetLoader;
import org.springframework.stereotype.Service;

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

/**
 * F025 규정 자연어 챗봇 — 룰셋(규정·요율 JSON)을 코퍼스로 근거 기반 답변한다.
 *
 * <p><b>파이프라인</b>: 질문 → 키워드 매칭으로 관련 룰 항목 선별(근거) → {@link AiTextRouter}(Claude→Ollama)로
 * <b>선별된 룰만</b> 근거로 답변 생성(룰 code/값 인용 필수) → 응답에 인용 룰 동봉.
 *
 * <p><b>환각 방지(핵심)</b>:
 * <ul>
 *   <li>관련 룰을 하나도 못 찾으면 → AI 호출 없이 "규정에서 찾지 못했습니다" 정직 응답(grounded=false).</li>
 *   <li>룰은 찾았으나 AI 미가용 → AI 지어내기 대신 <b>찾은 룰 항목 자체를 요약</b>해 반환(degraded=true, provider="rules").</li>
 *   <li>프롬프트에 "제공된 규정 항목 밖의 내용은 지어내지 마라"를 명시.</li>
 * </ul>
 */
@Service
public class RulesChatService {

    private final RuleSetLoader loader;
    private final AiTextRouter router;

    public RulesChatService(RuleSetLoader loader, AiTextRouter router) {
        this.loader = loader;
        this.router = router;
    }

    /** 검색용 룰 항목(내부). */
    private record Item(String code, String label, String detail, String haystack) {
        RuleCitation toCitation() {
            return new RuleCitation(code, label, detail);
        }
    }

    public RulesChatResult chat(String question) {
        if (question == null || question.isBlank()) {
            throw new ApiException(ErrorCode.VALIDATION, "질문을 입력해 주세요.");
        }
        List<Item> corpus = buildCorpus();
        List<Item> matched = match(question, corpus, 6);

        // 근거 없음 → AI 호출 없이 정직 응답(환각 차단).
        if (matched.isEmpty()) {
            return new RulesChatResult(question,
                    "규정에서 관련 내용을 찾지 못했습니다. 높이·리깅·방염·바닥하중·통로·임대료·전기/네트워크 등 규정/요율 키워드로 다시 질문해 주세요.",
                    List.of(), false, "none", true);
        }

        List<RuleCitation> citations = matched.stream().map(Item::toCitation).toList();
        AiResult ai = router.generate(buildPrompt(question, matched), 700);

        if (ai.usable()) {
            return new RulesChatResult(question, ai.text().trim(), citations, true, ai.provider(), false);
        }
        // AI 미가용 → 지어내지 말고 찾은 룰 항목을 그대로 요약(근거 기반·정직).
        StringBuilder sb = new StringBuilder("관련 규정 항목입니다(AI 요약 미가용 — 규정 원문 기준):\n");
        for (Item it : matched) {
            sb.append("· [").append(it.code()).append("] ").append(it.label());
            if (it.detail() != null && !it.detail().isBlank()) {
                sb.append(" — ").append(it.detail());
            }
            sb.append('\n');
        }
        return new RulesChatResult(question, sb.toString().trim(), citations, true, "rules", true);
    }

    private String buildPrompt(String question, List<Item> matched) {
        StringBuilder rules = new StringBuilder();
        for (Item it : matched) {
            rules.append("- code=").append(it.code())
                    .append(" | ").append(it.label());
            if (it.detail() != null && !it.detail().isBlank()) {
                rules.append(" | ").append(it.detail());
            }
            rules.append('\n');
        }
        String version = loader.compliance() != null ? loader.compliance().rulesetVersion() : "compliance";
        return "당신은 킨텍스 전시 규정·요율 안내 도우미다. 아래 '규정 항목'만 근거로 사용자 질문에 한국어로 간결히 답하라.\n\n"
                + "규정 항목(룰셋 " + version + "):\n" + rules + "\n"
                + "규칙(엄수):\n"
                + "1) 위 규정 항목에 있는 내용만 사용하라. 항목 밖의 수치·규정을 지어내지 마라.\n"
                + "2) 답변에 근거가 된 규정 code 를 대괄호로 인용하라(예: [HEIGHT_MAX]).\n"
                + "3) 제공된 항목으로 답할 수 없으면 '제공된 규정에서 확인되지 않습니다'라고 답하라.\n"
                + "4) 규정은 사전 안내이며 최종 승인은 킨텍스·구조기술사 판단임을 필요 시 덧붙여라.\n\n"
                + "질문: " + question;
    }

    // ── 코퍼스 구성 ──

    private List<Item> buildCorpus() {
        List<Item> items = new ArrayList<>();
        ComplianceRuleSet c = loader.compliance();
        if (c != null && c.rules() != null) {
            for (ComplianceRuleSet.Rule r : c.rules()) {
                String detail = complianceDetail(r);
                String hay = norm(String.join(" ",
                        safe(r.code()), safe(r.label()), safe(r.group()), safe(r.metric()), safe(r.unit()),
                        safe(r.severity()), detail));
                items.add(new Item(safe(r.code()), safe(r.label()), detail, hay));
            }
        }
        RateRuleSet rates = loader.rates();
        if (rates != null) {
            addRate(items, rates.rental(), "RATE_RENTAL", "임대료·전시장 임대 요율 임대 요금 rental 전시장 로비 옥외 이벤트홀 성수기 할증 보증금 계약금");
            addRate(items, rates.utility(), "RATE_UTILITY", "유틸리티 전기 전력 kw 네트워크 인터넷 급수 배수 급배수 배관 압축공기 요금 요율 utility power network");
            addRate(items, rates.boothStandard(), "RATE_BOOTH", "기본부스 조립부스 프리미엄 부스 표준 사양 스포트라이트 콘센트 전력");
        }
        return items;
    }

    private String complianceDetail(ComplianceRuleSet.Rule r) {
        StringBuilder d = new StringBuilder();
        if (r.operator() != null) {
            d.append(r.operator());
        }
        if (r.threshold() != null) {
            d.append(' ').append(trimNum(r.threshold()));
        }
        if (r.min() != null && r.max() != null) {
            d.append(' ').append(trimNum(r.min())).append('~').append(trimNum(r.max()));
        }
        if (r.unit() != null) {
            d.append(r.unit());
        }
        if (r.hallLimits() != null && !r.hallLimits().isEmpty()) {
            d.append(" 홀별한도=").append(r.hallLimits());
        }
        if (r.forbidden() != null && !r.forbidden().isEmpty()) {
            d.append(" 금지=").append(r.forbidden());
        }
        if (r.requiresDocument() != null) {
            d.append(" 필요서류=").append(r.requiresDocument());
        }
        if (r.severity() != null) {
            d.append(" (").append("block".equals(r.severity()) ? "차단" : "경고").append(")");
        }
        return d.toString().trim();
    }

    private void addRate(List<Item> items, Map<String, Object> map, String code, String keywords) {
        if (map == null || map.isEmpty()) {
            return;
        }
        String detail = flatten(map);
        String hay = norm(code + " " + keywords + " " + detail);
        items.add(new Item(code, rateLabel(code), detail, hay));
    }

    private static String rateLabel(String code) {
        return switch (code) {
            case "RATE_RENTAL" -> "임대 요율(전시장/로비/옥외/이벤트홀·성수기 할증·보증금)";
            case "RATE_UTILITY" -> "유틸리티 요율(전기·네트워크·급배수·압축공기)";
            case "RATE_BOOTH" -> "기본/프리미엄 부스 표준 사양";
            default -> code;
        };
    }

    private static String flatten(Map<String, Object> map) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> e : map.entrySet()) {
            if (sb.length() > 0) {
                sb.append(", ");
            }
            sb.append(e.getKey()).append('=').append(e.getValue());
        }
        return sb.toString();
    }

    // ── 키워드 매칭 ──

    /** 질문 토큰(길이≥2)이 항목 haystack 에 등장하는 개수로 스코어링, 상위 N 반환(스코어>0). */
    private List<Item> match(String question, List<Item> corpus, int topN) {
        String[] tokens = norm(question).split("\\s+");
        record Scored(Item item, int score) {
        }
        List<Scored> scored = new ArrayList<>();
        for (Item it : corpus) {
            int s = 0;
            for (String tok : tokens) {
                if (tok.length() >= 2 && it.haystack().contains(tok)) {
                    s++;
                }
            }
            if (s > 0) {
                scored.add(new Scored(it, s));
            }
        }
        scored.sort((a, b) -> Integer.compare(b.score(), a.score()));
        List<Item> out = new ArrayList<>();
        for (int i = 0; i < Math.min(topN, scored.size()); i++) {
            out.add(scored.get(i).item());
        }
        return out;
    }

    private static String norm(String s) {
        return s == null ? "" : s.toLowerCase(Locale.ROOT);
    }

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

    private static String trimNum(double d) {
        return d == Math.floor(d) ? String.valueOf((long) d) : String.valueOf(d);
    }
}
