// src/main/kotlin/network/RagService.kt

import dev.langchain4j.community.rag.content.retriever.lucene.LuceneEmbeddingStore
import dev.langchain4j.data.document.Metadata
import dev.langchain4j.data.message.UserMessage
import dev.langchain4j.data.segment.TextSegment
import dev.langchain4j.model.openai.OpenAiChatModel
import dev.langchain4j.model.openai.OpenAiEmbeddingModel
import dev.langchain4j.store.embedding.EmbeddingSearchRequest
import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import model.CandleData
import network.DartCodeManager
import network.NewsService
import org.apache.lucene.store.MMapDirectory
import service.TechnicalAnalyzer
import service.TradingDecisionCallback
import service.UrlCacheManager
import java.nio.file.Paths
import java.time.Duration

object RagService {

    // 임베딩 모델 (8081) 및 채팅 모델 (8080) 설정
    private val embeddingModel = OpenAiEmbeddingModel.builder()
        .baseUrl("http://127.0.0.1:8081/v1")
        .apiKey("unused")
        .build()

    private val chatModel = OpenAiChatModel.builder()
        .baseUrl("http://127.0.0.1:8080/v1")
        .apiKey("unused")
        .temperature(0.0) // [중요] 0.0으로 설정하여 결정론적 응답 유도
        .timeout(Duration.ofSeconds(60))
        .build()


    private val embeddingStore: LuceneEmbeddingStore by lazy {
        val path = Paths.get("db/lucene_idx")
        // FSDirectory.open(path)도 가능하지만, 64bit 시스템(Mac)에선 MMapDirectory가 가장 빠릅니다.
        val directory = MMapDirectory(path)

        // 제공해주신 소스의 Builder 사용
        LuceneEmbeddingStore.builder()
            .directory(directory)
            .build()

    }


    fun active() {
        println("[Cache] Active")
        if (UrlCacheManager.isInitialized()) return
        println("[Cache] initialize")
        UrlCacheManager.initialize(embeddingStore, embeddingModel)
    }


    /**
     * 텍스트를 임베딩하여 H2 DB에 저장합니다.
     */
    fun ingestWithChunking(
        text: String,
        newsLink: String = "",
        pubDate: String = "",
        stcokName: String,
        corpCode: String,
        corpName: String,
        stockCode: String
    ) {
        val MAX_CHUNK_SIZE = 500 // 안전하게 500자 내외로 설정

        // 1. 문단 단위로 먼저 분리
        val paragraphs = text.split(Regex("\n\n+"))
        val chunks = mutableListOf<String>()
        var currentChunk = StringBuilder()

        for (para in paragraphs) {
            // 현재 청크에 문단을 더했을 때 제한을 넘으면 지금까지의 내용을 확정
            if (currentChunk.length + para.length > MAX_CHUNK_SIZE && currentChunk.isNotEmpty()) {
                chunks.add(currentChunk.toString().trim())
                currentChunk = StringBuilder()
            }
            currentChunk.append(para).append("\n\n")

            // 문단 하나 자체가 너무 긴 경우 글자 수로 강제 분할
            if (currentChunk.length > MAX_CHUNK_SIZE) {
                val longPara = currentChunk.toString()
                longPara.chunked(MAX_CHUNK_SIZE).forEach { chunks.add(it.trim()) }
                currentChunk = StringBuilder()
            }
        }

        if (currentChunk.isNotEmpty()) chunks.add(currentChunk.toString().trim())

        // 2. 쪼개진 각 청크를 루씬에 개별 임베딩하여 저장
        chunks.forEachIndexed { index, chunk ->
            if (chunk.length > 10) { // 너무 짧은 노이즈 제외
                val metadata = Metadata()
                metadata.put("link", newsLink)
                metadata.put("date", pubDate)
                metadata.put("chunk_idx", index) // 순서 정보 유지
                metadata.put("stcokName",stcokName)
                metadata.put("corpCode",corpCode)
                metadata.put("corpName",corpName)
                metadata.put("stockCode",stockCode)

                val segment = TextSegment.from(chunk, metadata)
                val embedding = embeddingModel.embed(segment).content()
                embeddingStore.add(embedding, segment)
            }
        }
        println("🔎 [Lucene] ${chunks.size}개의 청크로 인덱싱 완료")
    }

    object JsonSanitizer {
        fun formatJson(raw: String): String {
            val regex = Regex("""\{.*\}""", RegexOption.DOT_MATCHES_ALL)
            return raw.trim()
                .removePrefix("```json")
                .removePrefix("```")
                .removeSuffix("```")
                .trim()
        }
    }

    suspend fun processStock(stockName: String,stockCode: String,result : TradingDecisionCallback) {
        // 1. 10분간의 데이터 가져오기 (API 호출)
        coroutineScope {
            var tradingDecision : TradingDecision = TradingDecision()
            tradingDecision.stockCode = stockCode
            var corpInfo = DartCodeManager.getCorpCode(stockCode)
            corpInfo?.stockName = stockName
            corpInfo?.let { NewsService.fetchAndIngestNews(it) }

            val financialDataDeferred = async { NewsService.fetchFinancialGrowth(corpInfo?.cCode ?: "") }

            tradingDecision.financialData = financialDataDeferred.await()
            result(tradingDecision,false)

            tradingDecision.techSummary = TechnicalAnalyzer.generateComprehensiveReport()
            result(tradingDecision,false)

            val question = "${corpInfo?.cName} $stockName[$stockCode]의 향후 실적 전망과 관련된 핵심 뉴스"
            val questionEmbedding = embeddingModel.embed(question).content()
            val searchResult = embeddingStore.search(
                EmbeddingSearchRequest.builder()
                    .queryEmbedding(questionEmbedding)
                    .maxResults(3)
                    .build()
            )
            tradingDecision.newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }
            result(tradingDecision,false)
            result(decideTrading(stockCode, tradingDecision),true)

        }
    }

    fun isUrlAlreadyIndexed(url: String): Boolean {
        // 1. 메타데이터의 'link' 필드가 해당 URL과 일치하는지 필터 구성
        val filter = MetadataFilterBuilder.metadataKey("link").isEqualTo(url)

        // 2. 검색 요청 생성 (벡터 유사도와 상관없이 필터 조건에 맞는 것 1개만 찾음)
        // 주의: 인터페이스에 따라 더미 벡터(0,0,...)가 필요할 수 있습니다.
        val searchRequest = EmbeddingSearchRequest.builder()
            .filter(filter)
            .maxResults(1)
            .build()

        val result = embeddingStore.search(searchRequest)

        // 결과가 비어있지 않다면 이미 저장된 URL입니다.
        return result.matches().isNotEmpty()
    }

    /**
     * 질문과 가장 유사한 정보를 H2에서 검색하여 AI 답변을 생성합니다.
     */
    fun askWithContext(question: String,
                       corpInfo: String,
                       financialData: String,
                       days : List<CandleData>,
                       weeks : List<CandleData>,
                       monthly : List<CandleData>): String {
        val questionEmbedding = embeddingModel.embed(question).content()
        val searchResult = embeddingStore.search(
            EmbeddingSearchRequest.builder()
                .queryEmbedding(questionEmbedding)
                .maxResults(5)
                .build()
        )
        val newsContext = searchResult.matches().joinToString("\n") { it.embedded().text() }

        // 2. 종합 분석 프롬프트 구성
        val finalPrompt = """
        <|begin_of_text|><|start_header_id|>system<|end_header_id|>
        당신은 뉴스(심리), 재무(본질), 차트(추세)를 통합 분석하는 'AI 수석 애널리스트'입니다.
        제공된 데이터를 바탕으로 아래 형식을 엄격히 지켜 분석 리포트를 작성하세요.

        [데이터 세트]
        1. 기업 기본 정보: $corpInfo
        2. 재무 성장성: $financialData
        3. 기술적 추세: ${monthly}, ${weeks}, ${days}
        4. 최신 이슈(뉴스): $newsContext

        [분석 요청 사항]
        1. **업계 상황**: 해당 종목이 속한 업종의 현재 전체적인 흐름을 먼저 정리하세요.
        2. **종목 이슈 분석**: 뉴스에서 포착된 핵심 키워드와 시장의 반응을 요약하세요.
        3. **장기/단기 전략**: 
           - 장기(재무/월봉 기반): 추천 혹은 비추천 사유
           - 단기(뉴스/일봉 기반): 추천 혹은 비추천 사유
        4. **최종 결론**: '매수/관망/매도' 의견과 그에 따른 근거를 단호하게 제시하세요.
        <|eot_id|>
        <|start_header_id|>user<|end_header_id|>
        질문: $question
        <|eot_id|><|start_header_id|>assistant<|end_header_id|>
    """.trimIndent()

        val response = chatModel.chat(UserMessage.from(finalPrompt))
        println(response)
        return response.aiMessage().text()
    }

    suspend fun decideTrading(
        stockName: String,
        tempDecision: TradingDecision
    ): TradingDecision? {
        val prompt = """
 <|begin_of_text|><|start_header_id|>system<|end_header_id|>
 당신은 수치 기반의 '정량 분석(Quantitative Analysis)' 트레이딩 전문가이자 전문 애널리스트입니다.
 제공된 데이터를 바탕으로 투자 기간별 스코어를 산출하고 최종 매매 결정을 내리십시오.
 아래 데이터를 분석하여 '매수', '매도', '관망' 중 하나를 결정하세요.
 
 [데이터 요약]
 - 종목: $stockName
 - 분석: ${tempDecision.techSummary}
 - 기업/재무: ${tempDecision.financialData}
 - 시장 심리: ${tempDecision.newsContext}

 [스코어 산출 가이드 (0-100)]
 1. 초단기: 30분봉 추세, MFI, OBV 에너지가 일치하면 80점 이상.
 2. 단기: 일봉 이평선 정배열 및 3일 변동률 양수일 때 70점 이상.
 3. 중기: 주봉 추세와 재무 성장성(매출/영익)이 동반 상승 시 75점 이상.
 4. 장기: 월봉 위치와 기업의 근본적인 시장 지배력 기반 판단.

 [응답 지침 - 엄격 준수]
 1. 분석 내용에 대한 설명, 서론, 결론을 절대 작성하지 마십시오.
 2. 오직 JSON 데이터만 출력하십시오.
 3. JSON 외의 텍스트가 포함될 경우 시스템이 중단됩니다.
 4. 응답은 반드시 '{' 문자로 시작하여 '}' 문자로 끝나야 합니다.
 [응답 형식]
 반드시 아래 JSON 형식으로만 답변하십시오:
 {
   "ultraShortScore": (숫자),
   "shortTermScore": (숫자),
   "midTermScore": (숫자),
   "longTermScore": (숫자),
   "decision": "BUY" | "SELL" | "HOLD",
   "reason": "결정적 근거 한 줄",
   "confidence": 0~100
 }
 <|eot_id|>
 <|start_header_id|>user<|end_header_id|>
 모든 데이터를 종합하여 스코어링 리포트를 작성하십시오.
 <|eot_id|><|start_header_id|>assistant<|end_header_id|>
    """.trimIndent()

        val response = chatModel.chat(UserMessage.from(prompt))
        val rawResponse = response.aiMessage().text()
        val jsonResponse = JsonSanitizer.formatJson(rawResponse)
        println("📥 [AI Raw JSON]:\n$jsonResponse")

        // 2. 유연한 파서 설정 (소수점 및 예외 상황 대응)
        val lenientJson = Json {
            ignoreUnknownKeys = true
            isLenient = true
            coerceInputValues = true
        }


        // JSON 파싱 (Kotlinx Serialization 활용)
        return try {
            println(jsonResponse)
            val decision = lenientJson.decodeFromString<TradingDecision>(jsonResponse)
            decision.financialData = tempDecision.financialData
            decision.newsContext = tempDecision.newsContext
            decision.techSummary = tempDecision.techSummary
            decision.stockCode = tempDecision.stockCode
            decision
        } catch (e: dev.langchain4j.exception.InternalServerException) {
            // 서버 에러 (컨텍스트 초과 등) 발생 시 로그 남기고 null 반환 혹은 커스텀 에러 처리
            println("🚨 [AI Server Error] ${e.message}")
            if (e.message?.contains("Context size") == true) {
                println("⚠️ 데이터가 너무 많습니다. 요약 로직을 점검하세요.")
            }
            tempDecision
            null
        } catch (e: Exception) {
            println("❌ [General Error] ${e.message}")
            null
        }
    }


}
@Serializable
class TradingDecision {

    val ultraShortScore: Double = 0.0 // 초단기 (분봉/에너지)
    val shortTermScore: Double = 0.0  // 단기 (일봉/뉴스)
    val midTermScore: Double = 0.0    // 중기 (주봉/재무)
    val longTermScore: Double = 0.0
    var stockCode: String = ""
    var decision: String? = null
    var reason: String? = null
    var confidence: Double = 0.0
    var techSummary : String? = null
    var newsContext : String? = null
    var financialData : String? = null

    fun profitPossible() =
        listOf<Double>(ultraShortScore,
                shortTermScore,
                midTermScore,
                longTermScore).average()

    override fun toString(): String {
        return """
수익실현 가능성 : ${profitPossible()}
ultraShortScore :$ultraShortScore
shortTermScore :$shortTermScore
midTermScore :$midTermScore
longTermScore :$longTermScore
decision: $decision 
reason: $reason 
confidence: $confidence
기술 분석: $techSummary
뉴스: $newsContext
재무재표: $financialData
        """.trimIndent()
    }
}