package com.zioinfo.kintex.work.search;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zioinfo.kintex.ai.AiTextRouter;
import com.zioinfo.kintex.ai.AiTextRouter.AiResult;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.system.SystemAccessGuard;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

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

/**
 * G-07 자연어 통합검색 — 질문을 {@link AiTextRouter}로 구조화(키워드·대상 유형 JSON)한 뒤 기존 키워드 검색을
 * 조합 호출하고, 실제 결과를 근거로 요약을 생성한다.
 *
 * <p><b>파이프라인</b>: 질문 → (1)구조화 추출(Claude→Ollama, JSON) → (2)키워드별 {@link SearchMapper} 검색 병합·중복제거·유형필터
 * → (3)결과 제목 근거 요약(2차 호출, 결과 있을 때만). 어느 단계든 AI 미가용이면 <b>원문 키워드 검색으로 강등</b>(degraded=true).
 *
 * <p><b>환각 방지</b>: 요약은 실제 조회된 결과 제목만 근거로 생성하도록 지시하고, 없는 사실을 지어내지 않게 한다.
 * 결과 자체는 DB 검색이 권위이며 AI 는 해석·요약만 담당한다.
 */
@Service
public class SearchAiService {

    private static final Logger log = LoggerFactory.getLogger(SearchAiService.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();

    /** 검색 가능한 유형(AI 지목값 검증 화이트리스트). */
    private static final Set<String> VALID_TYPES = Set.of(
            "WORKLOG", "NOTICE", "MEETING", "REPORT", "OPINION",
            "EVENT", "COMPANY", "AUCTION", "BOOTH", "LEAD", "CMS");

    private final SearchMapper mapper;
    private final SystemAccessGuard scope;
    private final AiTextRouter router;

    public SearchAiService(SearchMapper mapper, SystemAccessGuard scope, AiTextRouter router) {
        this.mapper = mapper;
        this.scope = scope;
        this.router = router;
    }

    public AiSearchResult search(KintexPrincipal principal, String question, int limit) {
        if (question == null || question.isBlank()) {
            return new AiSearchResult("", List.of(), List.of(), null, List.of(), 0, true, "none");
        }
        int size = limit <= 0 ? 30 : Math.min(limit, 100);
        boolean seeAll = scope.canSeeAll(principal);
        String q = question.trim();

        // (1) 구조화 추출
        AiResult ext = router.generate(buildExtractPrompt(q), 384);
        List<String> keywords = new ArrayList<>();
        List<String> types = new ArrayList<>();
        String provider = ext.provider();
        boolean degraded = !ext.usable();
        if (!degraded) {
            Parsed parsed = parse(ext.text());
            if (parsed == null) {
                degraded = true;
            } else {
                keywords = parsed.keywords();
                types = parsed.types();
            }
        }
        // 강등: 원문 전체를 단일 키워드로 검색.
        if (degraded || keywords.isEmpty()) {
            keywords = new ArrayList<>(List.of(q));
            if (degraded) {
                types = new ArrayList<>();
            }
        }

        // (2) 키워드별 검색 병합(중복 제거) + 유형 필터.
        Set<String> typeFilter = new LinkedHashSet<>(types);
        Map<String, SearchResultItem> merged = new LinkedHashMap<>();
        for (String kw : keywords) {
            if (kw == null || kw.isBlank()) {
                continue;
            }
            List<Map<String, Object>> rows = mapper.searchAll(kw.trim(), principal.userId(), seeAll, size);
            for (Map<String, Object> r : rows) {
                String type = str(r.get("type"));
                if (!typeFilter.isEmpty() && (type == null || !typeFilter.contains(type))) {
                    continue;
                }
                String id = str(r.get("id"));
                SearchResultItem item = new SearchResultItem(type, id, str(r.get("title")),
                        str(r.get("createdAt")), routeFor(type, id));
                merged.putIfAbsent(type + "|" + id, item);
                if (merged.size() >= size) {
                    break;
                }
            }
            if (merged.size() >= size) {
                break;
            }
        }
        List<SearchResultItem> results = new ArrayList<>(merged.values());

        // (3) 근거 요약(결과 있고 AI 해석이 살아 있을 때만 시도).
        String summary = null;
        if (!degraded && !results.isEmpty()) {
            AiResult sum = router.generate(buildSummaryPrompt(q, results), 384);
            if (sum.usable()) {
                summary = sum.text().trim();
                provider = sum.provider();
            }
        }

        return new AiSearchResult(q, keywords, new ArrayList<>(typeFilter), summary,
                results, results.size(), degraded, provider);
    }

    // ── 프롬프트 ─────────────────────────────────────────────────────

    private String buildExtractPrompt(String question) {
        return "당신은 킨텍스 전시운영 통합검색의 질의 분석기다. 사용자의 자연어 질문에서 검색에 쓸 키워드와 대상 유형을 뽑아 JSON 객체 하나로만 응답하라.\n"
                + "대상 유형(정확히 이 값만 사용): WORKLOG(업무일지), NOTICE(공지), MEETING(회의록), REPORT(업무보고), OPINION(의견), "
                + "EVENT(행사), COMPANY(업체), AUCTION(옥션), BOOTH(부스), LEAD(리드), CMS(콘텐츠).\n"
                + "규칙(엄수):\n"
                + "1) 키는 정확히 keywords(핵심 검색어 문자열 배열, 1~5개, 조사·불용어 제외), types(대상 유형 문자열 배열, 특정할 수 없으면 빈 배열) 2개.\n"
                + "2) 없는 유형을 지어내지 말고, 유형이 불분명하면 types 를 빈 배열로 둔다.\n"
                + "3) 설명·머리말·마크다운·코드펜스 없이 JSON 객체 텍스트만 출력하라.\n\n"
                + "질문: " + question;
    }

    private String buildSummaryPrompt(String question, List<SearchResultItem> results) {
        StringBuilder sb = new StringBuilder();
        int n = Math.min(results.size(), 20);
        for (int i = 0; i < n; i++) {
            SearchResultItem r = results.get(i);
            sb.append("- [").append(typeLabel(r.type())).append("] ").append(r.title()).append('\n');
        }
        return "당신은 통합검색 결과 요약기다. 아래 '검색 결과 목록'만 근거로 사용자의 질문에 대한 2~3문장 한국어 요약을 작성하라.\n"
                + "규칙: 목록에 없는 사실·수치를 지어내지 마라. 목록이 질문과 관련 없으면 관련 결과가 없다고 답하라. 요약 문장만 출력(머리말 없이).\n\n"
                + "질문: " + question + "\n\n검색 결과 목록:\n" + sb;
    }

    // ── 파싱 ─────────────────────────────────────────────────────────

    private record Parsed(List<String> keywords, List<String> types) {
    }

    private Parsed parse(String raw) {
        String json = stripToJson(raw);
        if (json == null) {
            return null;
        }
        try {
            JsonNode root = MAPPER.readTree(json);
            List<String> keywords = new ArrayList<>();
            JsonNode kw = root.get("keywords");
            if (kw != null && kw.isArray()) {
                for (JsonNode n : kw) {
                    String v = text(n);
                    if (v != null && !v.isBlank()) {
                        keywords.add(v.trim());
                    }
                }
            }
            List<String> types = new ArrayList<>();
            JsonNode tp = root.get("types");
            if (tp != null && tp.isArray()) {
                for (JsonNode n : tp) {
                    String v = text(n);
                    if (v != null) {
                        String u = v.trim().toUpperCase();
                        if (VALID_TYPES.contains(u) && !types.contains(u)) {
                            types.add(u);
                        }
                    }
                }
            }
            return new Parsed(keywords, types);
        } catch (Exception e) {
            return null;
        }
    }

    private static String stripToJson(String raw) {
        if (raw == null || raw.isBlank()) {
            return null;
        }
        String s = raw.trim();
        int start = s.indexOf('{');
        int end = s.lastIndexOf('}');
        return (start < 0 || end <= start) ? null : s.substring(start, end + 1);
    }

    /** SearchService 와 동일한 route 파생(민감정보 미포함). */
    private static String routeFor(String type, String id) {
        if (type == null) {
            return null;
        }
        return switch (type) {
            case "WORKLOG" -> "/work/worklog";
            case "NOTICE" -> "/work/notice";
            case "MEETING" -> "/work/meeting";
            case "REPORT" -> "/work/report";
            case "OPINION" -> "/work/opinion";
            case "EVENT" -> "/schedule";
            case "COMPANY" -> "/admin/companies";
            case "AUCTION" -> id == null ? "/auctions" : "/auctions/" + id;
            case "BOOTH" -> "/booth-sales";
            case "LEAD" -> "/leads";
            case "CMS" -> "/cms";
            default -> null;
        };
    }

    private static String typeLabel(String type) {
        if (type == null) {
            return "";
        }
        return switch (type) {
            case "WORKLOG" -> "업무일지";
            case "NOTICE" -> "공지";
            case "MEETING" -> "회의록";
            case "REPORT" -> "업무보고";
            case "OPINION" -> "의견";
            case "EVENT" -> "행사";
            case "COMPANY" -> "업체";
            case "AUCTION" -> "옥션";
            case "BOOTH" -> "부스";
            case "LEAD" -> "리드";
            case "CMS" -> "콘텐츠";
            default -> type;
        };
    }

    private static String text(JsonNode n) {
        return (n == null || n.isNull()) ? null : n.asText();
    }

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