package com.zioinfo.kintex.publicsite.assistant;

import com.zioinfo.kintex.ai.AiTextRouter;
import com.zioinfo.kintex.ai.AiTextRouter.AiResult;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.publicsite.PublicGuideService;
import com.zioinfo.kintex.publicsite.assistant.dto.VisitorAssistantDtos.AssistantResult;
import com.zioinfo.kintex.publicsite.assistant.dto.VisitorAssistantDtos.Citation;
import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.PublicCalendarEventDto;
import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.TransportInfoDto;
import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.VisitorGuideDto;
import org.springframework.stereotype.Service;

import java.time.YearMonth;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 공개 AI 관람 도우미 — 자연어 질문을 <b>적재 DB(V43) 근거만</b>으로 답한다(RAG-style grounding · 비인증).
 *
 * <p><b>파이프라인</b>: 질문 → 의도 감지(교통/주차/입장/편의/접근성/개요 + 전시 일정) →
 * {@link PublicGuideService} 로 관련 레코드 조회(방문가이드·교통·연/월 캘린더) →
 * 키워드 스코어링으로 상위 근거 선별 → 근거 있으면 {@link AiTextRouter}(Claude→Ollama)로
 * <b>선별된 레코드만</b> 근거로 한국어 답변 생성 → 응답에 근거 카드 + 인용 출처 동봉.
 *
 * <p><b>환각 방지(핵심)</b>:
 * <ul>
 *   <li>근거 0건 → AI 호출 없이 "찾지 못했습니다" 정직 응답(grounded=false, provider=none).</li>
 *   <li>근거는 있으나 AI 미가용 → 지어내지 않고 <b>찾은 레코드 요약</b> 반환(degraded=true, provider="search").</li>
 *   <li>프롬프트에 "제공된 DB 근거만 사용, 없으면 모른다"는 grounding 지시. 외부 실시간 크롤 호출 금지.</li>
 * </ul>
 */
@Service
public class VisitorAssistantService {

    private static final int MAX_QUESTION = 500;
    private static final int TOP_GUIDE = 4;
    private static final int TOP_TRANSPORT = 4;
    private static final int MAX_EVENTS = 12;
    private static final int MAX_TOKENS = 700;

    private static final Pattern YEAR = Pattern.compile("(20\\d{2})\\s*년?");
    private static final Pattern MONTH = Pattern.compile("(1[0-2]|0?[1-9])\\s*월");

    /** 전시/행사 일정 의도 신호. */
    private static final String[] EVENT_INTENT = {
            "전시", "행사", "일정", "언제", "열리", "열려", "열림", "개최", "스케줄", "볼거리", "관람",
            "이번달", "이번 달", "다음달", "다음 달", "올해", "내년", "연간", "몇월", "몇 월",
            "event", "exhibition", "show", "expo", "fair", "calendar"
    };

    /** 행사명 검색에서 제외할 일반/질문/날짜 어휘(모든 행사에 매칭되는 잡음 차단). */
    private static final Set<String> NAME_STOPWORDS = Set.of(
            "전시", "행사", "일정", "스케줄", "관람", "볼거리", "언제", "얼마", "어디", "어떻게", "무엇", "뭐가",
            "있나요", "있어", "알려줘", "알려주세요", "해주세요", "하나요", "인가요", "개최", "열리", "열려",
            "이번", "다음", "올해", "내년", "연간", "이번달", "다음달", "지금", "오늘", "내일", "요즘", "최근",
            "event", "exhibition", "show", "expo", "fair", "calendar", "kintex", "킨텍스", "전시장", "전시회");

    private final PublicGuideService guideService;
    private final AiTextRouter router;

    public VisitorAssistantService(PublicGuideService guideService, AiTextRouter router) {
        this.guideService = guideService;
        this.router = router;
    }

    /** 스코어링용 근거 항목. */
    private record Scored<T>(T item, int score) {
    }

    public AssistantResult ask(String questionRaw) {
        if (questionRaw == null || questionRaw.isBlank()) {
            throw new ApiException(ErrorCode.VALIDATION, "질문을 입력해 주세요.");
        }
        String question = questionRaw.strip();
        if (question.length() > MAX_QUESTION) {
            question = question.substring(0, MAX_QUESTION);
        }
        List<String> tokens = tokenize(question);

        // ── 근거 수집(DB 서빙만) ──────────────────────────────────────────────
        List<VisitorGuideDto> guides = scoreGuides(guideService.guides(null), tokens, TOP_GUIDE);
        List<TransportInfoDto> transports = scoreTransport(guideService.transport(null), tokens, TOP_TRANSPORT);
        List<PublicCalendarEventDto> events = collectEvents(question, tokens);

        boolean grounded = !guides.isEmpty() || !transports.isEmpty() || !events.isEmpty();
        List<Citation> citations = buildCitations(events, transports, guides);

        // 근거 없음 → AI 호출 없이 정직 응답(환각 차단).
        if (!grounded) {
            return new AssistantResult(question,
                    "제공된 안내 정보에서 관련 내용을 찾지 못했습니다. "
                            + "교통·주차·입장·편의시설·접근성 또는 전시 일정(연도/월/행사명) 관련으로 다시 질문해 주세요.",
                    List.of(), List.of(), List.of(), List.of(), false, "none", true);
        }

        AiResult ai = router.generate(buildPrompt(question, events, transports, guides), MAX_TOKENS);
        if (ai.usable()) {
            return new AssistantResult(question, ai.text().trim(), events, transports, guides,
                    citations, true, ai.provider(), false);
        }
        // AI 미가용 → 지어내지 말고 찾은 레코드를 요약(근거 기반·정직).
        return new AssistantResult(question, summarize(events, transports, guides), events, transports, guides,
                citations, true, "search", true);
    }

    // ── 이벤트 근거 수집 ─────────────────────────────────────────────────────
    private List<PublicCalendarEventDto> collectEvents(String question, List<String> tokens) {
        if (!hasEventIntent(question)) {
            return List.of();
        }
        String q = question.toLowerCase(Locale.ROOT).replace(" ", "");
        Integer year = firstInt(YEAR, question);
        Integer month = firstInt(MONTH, question);
        String lifecycle = parseLifecycle(question);

        // 상대 표현(이번달/다음달/올해/내년) → 연/월 산출(명시 값이 우선).
        YearMonth now = YearMonth.now();
        if (month == null && q.contains("다음달")) {
            YearMonth next = now.plusMonths(1);
            year = (year != null) ? year : next.getYear();
            month = next.getMonthValue();
        } else if (month == null && q.contains("이번달")) {
            year = (year != null) ? year : now.getYear();
            month = now.getMonthValue();
        } else if (year == null && q.contains("내년")) {
            year = now.getYear() + 1;
        } else if (year == null && q.contains("올해")) {
            year = now.getYear();
        }

        LinkedHashSet<PublicCalendarEventDto> merged = new LinkedHashSet<>();
        if (year != null || month != null || lifecycle != null) {
            merged.addAll(guideService.calendar(year, month, lifecycle, MAX_EVENTS));
        } else {
            // 시점 미지정 일반 일정 질문 → 예정(다가오는) 행사 우선.
            merged.addAll(guideService.calendar(null, null, "UPCOMING", MAX_EVENTS));
        }
        // 행사명 토큰 검색(특정 전시 지목 대응).
        List<String> nameTokens = nameSearchTokens(tokens);
        if (!nameTokens.isEmpty()) {
            merged.addAll(guideService.searchEventsByName(nameTokens, MAX_EVENTS));
        }
        return merged.stream().limit(MAX_EVENTS).toList();
    }

    private boolean hasEventIntent(String question) {
        String q = question.toLowerCase(Locale.ROOT);
        for (String kw : EVENT_INTENT) {
            if (q.contains(kw)) {
                return true;
            }
        }
        return YEAR.matcher(question).find() || MONTH.matcher(question).find();
    }

    private List<String> nameSearchTokens(List<String> tokens) {
        List<String> out = new ArrayList<>();
        for (String t : tokens) {
            if (t.length() >= 2 && !NAME_STOPWORDS.contains(t) && !t.matches(".*\\d.*") && out.size() < 5) {
                out.add(t);
            }
        }
        return out;
    }

    private String parseLifecycle(String question) {
        if (question.contains("예정") || question.contains("다가오")) {
            return "UPCOMING";
        }
        if (question.contains("진행") || question.contains("진행중") || question.contains("지금")) {
            return "ONGOING";
        }
        if (question.contains("종료") || question.contains("끝난") || question.contains("지난")) {
            return "ENDED";
        }
        return null;
    }

    // ── 키워드 스코어링 ──────────────────────────────────────────────────────
    private List<VisitorGuideDto> scoreGuides(List<VisitorGuideDto> corpus, List<String> tokens, int topN) {
        List<Scored<VisitorGuideDto>> scored = new ArrayList<>();
        for (VisitorGuideDto g : corpus) {
            String hay = norm(String.join(" ",
                    safe(g.category()), safe(g.title()), safe(g.summary()), safe(g.body())));
            int s = score(hay, tokens);
            if (s > 0) {
                scored.add(new Scored<>(g, s));
            }
        }
        return topItems(scored, topN);
    }

    private List<TransportInfoDto> scoreTransport(List<TransportInfoDto> corpus, List<String> tokens, int topN) {
        List<Scored<TransportInfoDto>> scored = new ArrayList<>();
        for (TransportInfoDto t : corpus) {
            String hay = norm(String.join(" ",
                    safe(t.mode()), safe(t.lineName()), safe(t.route()), safe(t.fromPoint()),
                    safe(t.travelTime()), safe(t.note())));
            int s = score(hay, tokens);
            if (s > 0) {
                scored.add(new Scored<>(t, s));
            }
        }
        return topItems(scored, topN);
    }

    private static <T> List<T> topItems(List<Scored<T>> scored, int topN) {
        scored.sort((a, b) -> Integer.compare(b.score(), a.score()));
        List<T> out = new ArrayList<>();
        for (int i = 0; i < Math.min(topN, scored.size()); i++) {
            out.add(scored.get(i).item());
        }
        return out;
    }

    private static int score(String haystack, List<String> tokens) {
        int s = 0;
        for (String tok : tokens) {
            if (tok.length() >= 2 && haystack.contains(tok)) {
                s++;
            }
        }
        return s;
    }

    // ── 프롬프트 · 요약 ──────────────────────────────────────────────────────
    private String buildPrompt(String question, List<PublicCalendarEventDto> events,
                               List<TransportInfoDto> transports, List<VisitorGuideDto> guides) {
        StringBuilder ev = new StringBuilder();
        for (PublicCalendarEventDto e : events) {
            ev.append("- [EVENT ").append(safe(e.id())).append("] ").append(safe(e.name()));
            if (e.startDate() != null) {
                ev.append(" | 기간=").append(safe(e.startDate()));
                if (e.endDate() != null) {
                    ev.append("~").append(safe(e.endDate()));
                }
            }
            if (e.hallLabel() != null) {
                ev.append(" | 장소=").append(safe(e.hallLabel()));
            }
            if (e.lifecycle() != null) {
                ev.append(" | 상태=").append(lifecycleKo(e.lifecycle()));
            }
            ev.append('\n');
        }
        StringBuilder tr = new StringBuilder();
        for (TransportInfoDto t : transports) {
            tr.append("- [TRANSPORT ").append(safe(t.id())).append("] ").append(safe(t.lineName()));
            if (t.route() != null) {
                tr.append(" | 경로=").append(safe(t.route()));
            }
            if (t.travelTime() != null) {
                tr.append(" | 시간=").append(safe(t.travelTime()));
            }
            tr.append('\n');
        }
        StringBuilder gd = new StringBuilder();
        for (VisitorGuideDto g : guides) {
            gd.append("- [GUIDE ").append(safe(g.id())).append("] ").append(safe(g.title()))
                    .append(" | ").append(safe(g.summary() != null ? g.summary() : g.body()))
                    .append('\n');
        }

        StringBuilder p = new StringBuilder();
        p.append("당신은 킨텍스(KINTEX) 전시장 공개 관람 안내 도우미다. ")
                .append("아래 'DB 근거'만 사용해 관람객 질문에 한국어로 친절하고 간결하게 답하라.\n\n");
        if (ev.length() > 0) {
            p.append("[전시·행사 일정]\n").append(ev).append('\n');
        }
        if (tr.length() > 0) {
            p.append("[교통]\n").append(tr).append('\n');
        }
        if (gd.length() > 0) {
            p.append("[방문가이드]\n").append(gd).append('\n');
        }
        p.append("규칙(엄수):\n")
                .append("1) 위 'DB 근거'에 있는 내용만 사용하라. 근거 밖의 날짜·요금·노선·시설을 지어내지 마라.\n")
                .append("2) 근거로 답할 수 없으면 '제공된 안내 정보에서 확인되지 않습니다'라고 정직하게 답하라.\n")
                .append("3) 요금·시간·주소 등 수치는 근거에 적힌 그대로만 인용하라.\n")
                .append("4) 정확한 입장 조건·주차 운영은 행사별 공지 확인이 필요함을 필요 시 덧붙여라.\n")
                .append("5) 마크다운 없이 자연스러운 문장으로 답하라.\n\n")
                .append("질문: ").append(question);
        return p.toString();
    }

    /** AI 미가용 시 근거 레코드 요약(지어내기 없음). */
    private String summarize(List<PublicCalendarEventDto> events,
                             List<TransportInfoDto> transports, List<VisitorGuideDto> guides) {
        StringBuilder sb = new StringBuilder("관련 안내 정보입니다(AI 요약 미가용 — 원문 기준):\n");
        for (PublicCalendarEventDto e : events) {
            sb.append("· ").append(safe(e.name()));
            if (e.startDate() != null) {
                sb.append(" (").append(safe(e.startDate()));
                if (e.endDate() != null) {
                    sb.append("~").append(safe(e.endDate()));
                }
                sb.append(", ").append(lifecycleKo(e.lifecycle())).append(")");
            }
            sb.append('\n');
        }
        for (TransportInfoDto t : transports) {
            sb.append("· ").append(safe(t.lineName()));
            if (t.route() != null) {
                sb.append(" — ").append(safe(t.route()));
            }
            sb.append('\n');
        }
        for (VisitorGuideDto g : guides) {
            sb.append("· ").append(safe(g.title()));
            String detail = g.summary() != null ? g.summary() : g.body();
            if (detail != null && !detail.isBlank()) {
                sb.append(" — ").append(detail);
            }
            sb.append('\n');
        }
        return sb.toString().trim();
    }

    private List<Citation> buildCitations(List<PublicCalendarEventDto> events,
                                          List<TransportInfoDto> transports, List<VisitorGuideDto> guides) {
        List<Citation> cites = new ArrayList<>();
        for (PublicCalendarEventDto e : events) {
            cites.add(new Citation("EVENT", e.id(), e.name(), null));
        }
        for (TransportInfoDto t : transports) {
            cites.add(new Citation("TRANSPORT", t.id(), t.lineName(), t.sourceUrl()));
        }
        for (VisitorGuideDto g : guides) {
            cites.add(new Citation("GUIDE", g.id(), g.title(), g.sourceUrl()));
        }
        return cites;
    }

    private static String lifecycleKo(String lifecycle) {
        if (lifecycle == null) {
            return "";
        }
        return switch (lifecycle) {
            case "UPCOMING" -> "예정";
            case "ONGOING" -> "진행중";
            case "ENDED" -> "종료";
            default -> lifecycle;
        };
    }

    // ── 토큰화 helpers ──────────────────────────────────────────────────────
    private static List<String> tokenize(String question) {
        String[] raw = norm(question).split("[\\s,·.!?()\\[\\]/]+");
        List<String> out = new ArrayList<>();
        for (String r : raw) {
            if (r.length() >= 2) {
                out.add(r);
            }
        }
        return out;
    }

    private static Integer firstInt(Pattern p, String s) {
        Matcher m = p.matcher(s);
        return m.find() ? Integer.valueOf(m.group(1)) : null;
    }

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

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