package com.zioinfo.esn.rag;

import com.zioinfo.esn.config.OllamaClient;
import com.zioinfo.esn.rag.RagToggleService.RagToggles;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

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

/**
 * ESN 대표 AI 기능 RAG 배선 레이어(순증·비파괴, 테넌트 격리).
 *
 * <p>기존 {@link OllamaClient}(폴백 내장) 및 {@code AiController} 는 <b>불변</b>. 본 서비스는
 * 중앙 guardia-rag 를 경유하는 <b>별개 레이어</b>로, 두 대표 기능을 전환한다:
 * <ol>
 *   <li><b>알람 원인분석·우선순위</b> → {@code /rag/agent}(tool-use 다단계 진단)
 *       + GraphRAG(retrieval_mode=graph 의존성 추적, graphrag 토글 on 시) + {@code /rag/structured}
 *       (우선순위 결정론 JSON). 토글 off/미가용 시 기존 {@link OllamaClient#chat} 로 무손실 폴백.</li>
 *   <li><b>POS 데이터 자동 분류</b> → {@code /rag/structured}(format:json 결정론).
 *       confidence 임계 미만이면 {@code needsReview:true}. 미가용 시 OllamaClient 폴백.</li>
 * </ol>
 *
 * <p>모든 응답에 적용 metadata(retrievalMode·rerank·structured·toolUse·degraded)를 표기해
 * 토글 effect 를 관측 가능하게 한다.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class RagAiService {

    private final EsnRagClient rag;
    private final RagToggleService toggles;
    private final EsnToolRegistry tools;
    private final OllamaClient ollama;   // 기존 폴백 재사용(불변)

    /** 알람 우선순위·근본원인 결정론 JSON 스키마. */
    private static final Map<String, Object> ALARM_SCHEMA = Map.of(
        "type", "object",
        "properties", Map.of(
            "rootCauseHypothesis", Map.of("type", "string"),
            "priority", Map.of("type", "string"),               // P1~P4
            "groupKey", Map.of("type", "string"),
            "affectedDeviceCount", Map.of("type", "integer"),
            "recommendedActions", Map.of("type", "array", "items", Map.of("type", "string")),
            "confidence", Map.of("type", "number")
        ),
        "required", List.of("priority", "rootCauseHypothesis")
    );

    /** POS 분류 결정론 JSON 스키마. */
    private static final Map<String, Object> POS_SCHEMA = Map.of(
        "type", "object",
        "properties", Map.of(
            "classification", Map.of("type", "string"),         // NORMAL|ABNORMAL
            "category", Map.of("type", "string"),               // PRICE_OK|PRICE_MISMATCH|MISSING|OUTLIER ...
            "reason", Map.of("type", "string"),
            "confidence", Map.of("type", "number")
        ),
        "required", List.of("classification", "confidence")
    );

    // ── ① ESL/HCore 알람 원인분석·우선순위 ───────────────────────────────────
    @SuppressWarnings("unchecked")
    public Map<String, Object> analyzeAlarm(String tenantCode, Map<String, Object> req, String actor) {
        RagToggles t = toggles.current(tenantCode);
        String alarmType = str(req.get("alarmType"));
        String severity = str(req.getOrDefault("severity", "LOW"));
        String message = str(req.get("message"));
        String storeCode = str(req.get("storeCode"));
        String query = String.format(
            "ESL/HCore 알람의 근본원인과 우선순위를 진단하라. 유형=%s, 심각도=%s, 매장=%s, 메시지=%s. "
            + "허브 다운이 하위 단말 다중 알람을 유발하는 연쇄(cascading) 가능성을 의존성 그래프로 검토하고, "
            + "같은 근본원인의 파생 알람은 하나로 그룹핑하라.",
            alarmType, severity, storeCode, message);

        Map<String, Object> out = new LinkedHashMap<>();

        if (!t.ragEnabled || !rag.available()) {
            return alarmFallback(out, t, req);
        }

        // tool-use 토글 on → /rag/agent 다단계 진단, off → graph/answer 단발
        Map<String, Object> evidence;
        if (t.toolUse) {
            evidence = rag.agent(tenantCode, query, tools.alarmDiagnosisTools(),
                toggles.effectiveMode(t), t.agentMaxSteps, t.generationModel);
        } else {
            evidence = rag.answer(tenantCode, query, toggles.effectiveMode(t),
                t.rerank, t.topK, t.generationModel, true);
        }
        if (bool(evidence.get("degraded"))) {
            return alarmFallback(out, t, req);
        }

        // 우선순위·그룹핑은 결정론 JSON(/structured)으로 수렴 — 파싱 실패 차단
        String evText = str(evidence.getOrDefault("answer", ""));
        Map<String, Object> sres = rag.structured(tenantCode,
            query + "\n근거:\n" + evText + "\n위 근거로 priority(P1~P4)·rootCause·group·조치를 JSON 으로만 산출.",
            ALARM_SCHEMA, t.generationModel, t.temperature);

        Map<String, Object> data = sres.get("data") instanceof Map ? (Map<String, Object>) sres.get("data") : Map.of();
        if (bool(sres.get("degraded")) || data.isEmpty()) {
            return alarmFallback(out, t, req);
        }

        out.put("rootCauseHypothesis", data.get("rootCauseHypothesis"));
        out.put("priority", data.getOrDefault("priority", "P3"));
        out.put("groupKey", data.get("groupKey"));
        out.put("affectedDeviceCount", data.get("affectedDeviceCount"));
        out.put("recommendedActions", data.get("recommendedActions"));
        out.put("confidence", data.get("confidence"));
        out.put("citations", evidence.get("citations"));
        out.put("answerId", evidence.get("answer_id"));
        out.put("engine", t.toolUse ? "RAG_AGENT" : "RAG_GRAPH");
        out.put("degraded", false);
        out.putAll(meta(t, t.toolUse ? "agent" : "answer", true));
        return out;
    }

    private Map<String, Object> alarmFallback(Map<String, Object> out, RagToggles t, Map<String, Object> req) {
        // 비파괴 폴백: 기존 OllamaClient 룰/모델 + 규칙 기반 우선순위
        String prompt = String.format("ESL 알람 분석: 유형=%s 심각도=%s 메시지=%s. 원인·조치 3줄.",
            str(req.get("alarmType")), str(req.getOrDefault("severity", "LOW")), str(req.get("message")));
        String analysis = ollama.chat(prompt, "ESL 시스템 전문 AI 엔지니어. 실용 조치 제안.");
        out.put("rootCauseHypothesis", analysis);
        out.put("priority", rulePriority(str(req.getOrDefault("severity", "LOW"))));
        out.put("recommendedActions", List.of("디바이스 연결 상태 확인", "허브 통신 점검", "지속 시 현장 엔지니어 파견"));
        out.put("engine", "OLLAMA_FALLBACK");
        out.put("degraded", true);
        out.put("degraded_reason", t.ragEnabled ? "rag_unavailable" : "rag_disabled");
        out.putAll(meta(t, "fallback", false));
        return out;
    }

    private String rulePriority(String severity) {
        switch (severity == null ? "" : severity.toUpperCase()) {
            case "CRITICAL": return "P1";
            case "HIGH":     return "P2";
            case "MEDIUM":   return "P3";
            default:         return "P4";
        }
    }

    // ── ② POS 데이터 자동 분류 (결정론) ─────────────────────────────────────
    @SuppressWarnings("unchecked")
    public Map<String, Object> classifyPos(String tenantCode, Map<String, Object> req, String actor) {
        RagToggles t = toggles.current(tenantCode);
        String productCode = str(req.get("productCode"));
        String price = str(req.getOrDefault("price", "0"));
        String salePrice = str(req.getOrDefault("salePrice", "0"));
        Map<String, Object> out = new LinkedHashMap<>();

        if (!t.ragEnabled || !rag.available() || !t.structured) {
            return posFallback(out, t, req);
        }

        String prompt = String.format(
            "POS 가격 데이터를 NORMAL/ABNORMAL 로 분류하라. 상품코드=%s, 정가=%s, 판매가=%s. "
            + "판매가>정가, 음수, 비현실적 값, 누락을 ABNORMAL 로 판정하고 category·reason·confidence(0~1)를 JSON 으로만.",
            productCode, price, salePrice);

        Map<String, Object> sres = rag.structured(tenantCode, prompt, POS_SCHEMA, t.generationModel, t.temperature);
        Map<String, Object> data = sres.get("data") instanceof Map ? (Map<String, Object>) sres.get("data") : Map.of();

        if (bool(sres.get("degraded")) || data.isEmpty() || data.get("classification") == null) {
            return posFallback(out, t, req);
        }

        double conf = num(data.get("confidence"), 0.0);
        boolean needsReview = conf < t.faithfulnessThreshold;
        out.put("classification", String.valueOf(data.get("classification")).toUpperCase());
        out.put("category", data.get("category"));
        out.put("reason", data.get("reason"));
        out.put("confidence", conf);
        out.put("needsReview", needsReview);   // 임계 미만 → 수동 검토
        out.put("productCode", productCode);
        out.put("engine", "RAG_STRUCTURED");
        out.put("degraded", false);
        out.putAll(meta(t, "structured", true));
        return out;
    }

    private Map<String, Object> posFallback(Map<String, Object> out, RagToggles t, Map<String, Object> req) {
        String price = str(req.getOrDefault("price", "0"));
        String salePrice = str(req.getOrDefault("salePrice", "0"));
        // 규칙 우선(결정론) — 판매가>정가 또는 음수면 ABNORMAL
        String cls = "NORMAL";
        String reason = "규칙 기반: 가격 범위 정상";
        try {
            double p = Double.parseDouble(price), s = Double.parseDouble(salePrice);
            if (p < 0 || s < 0 || s > p) { cls = "ABNORMAL"; reason = "규칙 기반: 음수 또는 판매가>정가"; }
        } catch (Exception ignore) {
            // 숫자 파싱 실패 시 모델 보조
            String r = ollama.chat("POS 가격 검증: 정가=" + price + " 판매가=" + salePrice, "POS 데이터 품질 관리 AI.");
            cls = r.toUpperCase().contains("ABNORMAL") ? "ABNORMAL" : "NORMAL";
            reason = "모델 폴백 판정";
        }
        out.put("classification", cls);
        out.put("reason", reason);
        out.put("confidence", 0.0);
        out.put("needsReview", true);   // 폴백은 항상 검토 권고
        out.put("productCode", str(req.get("productCode")));
        out.put("engine", "RULE_FALLBACK");
        out.put("degraded", true);
        out.put("degraded_reason", t.ragEnabled ? "rag_unavailable" : "rag_disabled");
        out.putAll(meta(t, "fallback", false));
        return out;
    }

    // ── WISE AI 자유 질의 (중앙 /rag/answer 경유 — 근거·인용·환각차단) ──────────
    /**
     * WISE AI 자유 질의. 중앙 {@code /rag/answer}(검색+생성+검증+guardrail)를 경유해 근거 답변을 반환한다.
     * 토글 off/미가용/degraded 시 스택 미노출 요약 메시지로 안전 degraded. 테넌트 격리(컬렉션 분리).
     * 기존 alarm/pos 경로 불변 — 순증 메서드.
     *
     * @return {@code {answer, citations[], sources[], abstained, grounded, faithfulness, answerId, degraded, applied{...}}}
     */
    @SuppressWarnings("unchecked")
    public Map<String, Object> wiseAsk(String tenantCode, String query, String retrievalMode) {
        RagToggles t = toggles.current(tenantCode);
        Map<String, Object> out = new LinkedHashMap<>();
        if (query == null || query.isBlank()) {
            out.put("answer", "");
            out.put("degraded", true);
            out.put("degraded_reason", "empty_query");
            out.putAll(meta(t, "none", false));
            return out;
        }
        String mode = (retrievalMode != null && !retrievalMode.isBlank())
            ? retrievalMode : toggles.effectiveMode(t);

        if (!t.ragEnabled || !rag.available()) {
            out.put("answer", "AI 서비스 일시 불가 — 잠시 후 다시 시도해 주세요.");
            out.put("degraded", true);
            out.put("degraded_reason", t.ragEnabled ? "rag_unavailable" : "rag_disabled");
            out.putAll(meta(t, "answer", false));
            return out;
        }

        Map<String, Object> res = rag.answer(tenantCode, query, mode, t.rerank, t.topK, t.generationModel, true);
        if (bool(res.get("degraded"))) {
            out.put("answer", "AI 서비스 일시 불가 — 잠시 후 다시 시도해 주세요.");
            out.put("degraded", true);
            out.put("degraded_reason", str(res.getOrDefault("degraded_reason", "rag_empty")));
            out.putAll(meta(t, "answer", false));
            return out;
        }

        out.put("answer", res.get("answer"));
        out.put("citations", res.get("citations"));
        out.put("sources", res.get("sources"));
        out.put("abstained", res.get("abstained"));
        out.put("grounded", res.get("grounded"));
        out.put("faithfulness", res.get("faithfulness"));
        out.put("answerId", res.get("answer_id"));
        out.put("engine", "RAG_ANSWER");
        out.put("degraded", false);
        out.putAll(meta(t, "answer", false));
        return out;
    }

    // ── 피드백 / 토글 ────────────────────────────────────────────────────────
    public Map<String, Object> feedback(String tenantCode, Map<String, Object> req, String actor) {
        String userRef = "u_" + Integer.toHexString((actor == null ? "anon" : actor).hashCode());
        return rag.feedback(tenantCode,
            str0(req.get("answerId")), str0(req.get("query")), str0(req.get("answer")),
            str(req.getOrDefault("verdict", "down")), str0(req.get("correction")), userRef);
    }

    public Map<String, Object> currentToggles(String tenantCode) {
        Map<String, Object> m = toggles.current(tenantCode).toMap();
        m.put("ragAvailable", rag.available());
        return m;
    }

    public Map<String, Object> updateToggle(String tenantCode, String key, String value) {
        Map<String, Object> m = toggles.update(tenantCode, key, value).toMap();
        m.put("ragAvailable", rag.available());
        return m;
    }

    // ── helpers ─────────────────────────────────────────────────────────────
    private Map<String, Object> meta(RagToggles t, String technique, boolean structured) {
        Map<String, Object> m = new LinkedHashMap<>();
        Map<String, Object> applied = new LinkedHashMap<>();
        applied.put("tenantCode", t.tenantCode);
        applied.put("retrievalMode", toggles.effectiveMode(t));
        applied.put("rerank", t.rerank);
        applied.put("graphrag", t.graphrag);
        applied.put("toolUse", t.toolUse);
        applied.put("structured", structured);
        applied.put("technique", technique);
        m.put("applied", applied);
        return m;
    }

    private boolean bool(Object o) { return Boolean.TRUE.equals(o) || "true".equalsIgnoreCase(String.valueOf(o)); }
    private double num(Object o, double def) {
        try { return Double.parseDouble(String.valueOf(o)); } catch (Exception e) { return def; }
    }
    private String str(Object o) { return o == null ? "" : String.valueOf(o); }
    private String str0(Object o) { return o == null ? null : String.valueOf(o); }
}
