package com.zioinfo.signage.rag;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 얇은 중앙 guardia-rag REST 클라이언트 (ESN 전용).
 *
 * <p><b>핵심 설계</b>: ESN 은 LangChain·벡터 로직을 Java 에 재구현하지 않는다(13벌 재구현 금지).
 * 중앙 Python guardia-rag 의 검색·기법·구조화·검증·에이전틱을 REST 로 호출하기만 한다.
 * 미가용/타임아웃/RAM 부족 시 <b>절대 예외를 전파하지 않고</b> {@code degraded:true} 가 표시된
 * 폴백 결과를 돌려준다(서비스 계층이 기존 OllamaClient/룰 폴백 수행).
 *
 * <p>WebFlux 미의존 — 기존 {@code OllamaClient} 와 동일하게 {@code java.net.http.HttpClient} 사용.
 *
 * <p><b>보안 불변</b>: 온프레미스 전용(루프백). 응답에서 자격증명/PII/스택트레이스 미노출.
 * 오류는 요약 1줄만 로깅.
 *
 * <p><b>테넌트 격리</b>: 모든 호출에 {@code solution=esn} + {@code collection=signage__<TENANT>} 을
 * 실어 테넌트별 컬렉션을 분리한다.
 *
 * <p>호출 계약(중앙):
 * <ul>
 *   <li>POST /rag/answer     — 검색(retrieval_mode)+근거생성+인용+guardrail</li>
 *   <li>POST /rag/verify     — 근거검증(faithfulness/grounded)</li>
 *   <li>POST /rag/structured — format:json 결정론 JSON</li>
 *   <li>POST /rag/agent      — tool-use 에이전틱 루프</li>
 *   <li>POST /rag/feedback   — 👍/👎 피드백 (solution=esn 격리)</li>
 *   <li>POST /rag/ingest     — 색인 (테넌트 컬렉션)</li>
 * </ul>
 */
@Slf4j
@Component
public class SignageRagClient {

    private final RagProperties props;
    private final ObjectMapper json = new ObjectMapper();
    private final HttpClient http = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(4))
        .build();

    public SignageRagClient(RagProperties props) {
        this.props = props;
    }

    /** 테넌트별 컬렉션 키: {@code signage__LGINNOTEK} 등. null/blank → 솔루션 공용. */
    public String collectionFor(String tenantCode) {
        if (tenantCode == null || tenantCode.isBlank()) return props.getSolution();
        return props.getSolution() + "__" + tenantCode.trim().toUpperCase();
    }

    /** 중앙 RAG 사용 가능 여부(설정 enabled + trust/settings 도달). */
    public boolean available() {
        if (!props.isEnabled()) return false;
        try {
            HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(props.getBaseUrl() + "/rag/trust/settings?solution=" + props.getSolution()))
                .GET()
                .timeout(Duration.ofSeconds(3))
                .build();
            HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
            return resp.statusCode() >= 200 && resp.statusCode() < 500;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * POST /rag/answer — 검색(retrieval_mode: vector|hybrid|graph)+생성+검증+guardrail.
     *
     * @return {@code {answer, grounded, faithfulness, citations[], sources[], guardrail{...}, answer_id, degraded}}.
     *         미가용 시 {@code degraded:true}.
     */
    public Map<String, Object> answer(String tenantCode, String query, String retrievalMode,
                                      boolean rerank, int topK, String model, boolean verify) {
        if (!props.isEnabled()) return degraded("rag_disabled");
        try {
            Map<String, Object> options = new LinkedHashMap<>();
            options.put("rerank", rerank);
            options.put("stream", false);
            options.put("verify", verify);
            if (model != null) options.put("model", model);

            Map<String, Object> body = base(tenantCode);
            body.put("query", query);
            body.put("retrieval_mode", retrievalMode == null ? "vector" : retrievalMode);
            body.put("top_k", topK <= 0 ? 5 : topK);
            body.put("options", options);
            Map<String, Object> res = post("/rag/answer", body);
            return res != null ? res : degraded("rag_no_response");
        } catch (Exception e) {
            log.warn("RAG /answer 일시 불가 — ESN 폴백: {}", summarize(e));
            return degraded("rag_unavailable");
        }
    }

    /**
     * POST /rag/structured — 스키마 강제 결정론 JSON.
     *
     * @return {@code {data:{...}, valid, fallback_used, degraded}}. 미가용 시 {@code degraded:true}.
     */
    public Map<String, Object> structured(String tenantCode, String prompt, Map<String, Object> schema,
                                          String model, double temperature) {
        if (!props.isEnabled()) return degraded("rag_disabled");
        try {
            Map<String, Object> body = base(tenantCode);
            body.put("prompt", prompt);
            if (schema != null) body.put("schema", schema);
            body.put("format", "json");
            if (model != null) body.put("model", model);
            body.put("temperature", temperature);
            body.put("max_retries", 1);
            Map<String, Object> res = post("/rag/structured", body);
            return res != null ? res : degraded("rag_no_response");
        } catch (Exception e) {
            log.warn("RAG /structured 일시 불가 — ESN 폴백: {}", summarize(e));
            return degraded("rag_unavailable");
        }
    }

    /**
     * POST /rag/agent — 에이전틱 tool-use 루프(다단계 진단).
     *
     * @param tools   ESN 도구 디스크립터 목록(이름·설명·파라미터 스키마).
     * @param maxSteps 루프 상한(서버 RAM/시간 보호).
     * @return {@code {answer, steps[], tool_calls[], citations[], answer_id, degraded}}.
     */
    public Map<String, Object> agent(String tenantCode, String query, Object tools,
                                     String retrievalMode, int maxSteps, String model) {
        if (!props.isEnabled()) return degraded("rag_disabled");
        try {
            Map<String, Object> body = base(tenantCode);
            body.put("query", query);
            if (tools != null) body.put("tools", tools);
            body.put("retrieval_mode", retrievalMode == null ? "vector" : retrievalMode);
            body.put("max_steps", maxSteps <= 0 ? 4 : Math.min(maxSteps, 8));
            if (model != null) body.put("model", model);
            Map<String, Object> res = post("/rag/agent", body);
            return res != null ? res : degraded("rag_no_response");
        } catch (Exception e) {
            log.warn("RAG /agent 일시 불가 — ESN 폴백: {}", summarize(e));
            return degraded("rag_unavailable");
        }
    }

    /** POST /rag/verify — 근거검증(grounded/faithfulness). */
    public Map<String, Object> verify(String tenantCode, String answer, String context) {
        if (!props.isEnabled()) return degraded("rag_disabled");
        try {
            Map<String, Object> body = base(tenantCode);
            body.put("answer", answer);
            if (context != null) body.put("context", context);
            Map<String, Object> res = post("/rag/verify", body);
            return res != null ? res : degraded("rag_no_response");
        } catch (Exception e) {
            log.warn("RAG /verify 일시 불가: {}", summarize(e));
            return degraded("rag_unavailable");
        }
    }

    /** POST /rag/feedback — 👍/👎 피드백(solution=esn 격리). */
    public Map<String, Object> feedback(String tenantCode, String answerId, String query, String answer,
                                        String verdict, String correction, String userRef) {
        if (!props.isEnabled()) return Map.of("stored", false, "degraded", true);
        try {
            Map<String, Object> body = base(tenantCode);
            if (answerId != null) body.put("answer_id", answerId);
            if (query != null) body.put("query", query);
            if (answer != null) body.put("answer", answer);
            body.put("verdict", verdict);
            if (correction != null) body.put("correction", correction);
            if (userRef != null) body.put("user_ref", userRef);
            Map<String, Object> res = post("/rag/feedback", body);
            return res != null ? res : Map.of("stored", false, "degraded", true);
        } catch (Exception e) {
            log.warn("RAG /feedback 일시 불가: {}", summarize(e));
            return Map.of("stored", false, "degraded", true);
        }
    }

    /** POST /rag/ingest — 테넌트 컬렉션 색인. 색인 스크립트/훅에서 사용. */
    public Map<String, Object> ingest(String tenantCode, Object documents) {
        if (!props.isEnabled()) return degraded("rag_disabled");
        try {
            Map<String, Object> body = base(tenantCode);
            body.put("documents", documents);
            Map<String, Object> res = post("/rag/ingest", body);
            return res != null ? res : degraded("rag_no_response");
        } catch (Exception e) {
            log.warn("RAG /ingest 일시 불가: {}", summarize(e));
            return degraded("rag_unavailable");
        }
    }

    // ── helpers ───────────────────────────────────────────────────────────
    private Map<String, Object> base(String tenantCode) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("solution", props.getSolution());
        m.put("collection", collectionFor(tenantCode));
        if (tenantCode != null && !tenantCode.isBlank()) {
            m.put("tenant_code", tenantCode.trim().toUpperCase());
        }
        return m;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> post(String path, Map<String, Object> body) throws Exception {
        String payload = json.writeValueAsString(body);
        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create(props.getBaseUrl() + path))
            .header("Content-Type", "application/json")
            .header("X-Solution-Key", props.getSolution())
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .timeout(Duration.ofMillis(props.getTimeoutMs()))
            .build();
        HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
        if (resp.statusCode() < 200 || resp.statusCode() >= 300) {
            log.warn("RAG {} 비정상 status={} — 폴백", path, resp.statusCode());
            return null;
        }
        String b = resp.body();
        if (b == null || b.isBlank()) return null;
        return json.readValue(b, Map.class);
    }

    private Map<String, Object> degraded(String reason) {
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("degraded", true);
        out.put("degraded_reason", reason);
        return out;
    }

    /** 스택트레이스 미노출: 메시지 1줄만 요약. */
    private String summarize(Exception e) {
        String m = e.getMessage();
        if (m == null) return e.getClass().getSimpleName();
        return m.length() > 160 ? m.substring(0, 160) : m;
    }
}
