<?php

namespace platyFramework;

/**
 * @example
 * require_once("./ptyLibrary_PHP/aivectorembedding/EmbeddingChunkSplitter.php");
 * $m = new EmbeddingChunkSplitter(maxTokens: 1024 * 1023 # chatgpt 4.0 mini 기준 chunks 크기
 * , mainRatio: 0.9);
 * $chunks = $m->createChunks($markdown);
 */
class EmbeddingChunkSplitter
{
    private int $maxTokens;
    private float $mainRatio;
    // private int $overlapTokens;

    /**
     * @param int $maxTokens 최대 토큰 수 (기본값: 16000)
     * @param float $mainRatio 메인 컨텐츠 비율 (기본값: 0.7, 앞뒤 15%씩 오버랩)
     */
    public function __construct(int $maxTokens = 16000, float $mainRatio = 0.7)
    {
        $this->maxTokens = $maxTokens;
        $this->mainRatio = $mainRatio;
//        $this->overlapTokens = (int)(($maxTokens * (1 - $mainRatio)) / 2);
//        pdfe($this->overlapTokens, "overlapTokens");
    }

    /**
     * 텍스트 배열을 임베딩에 최적화된 청크로 분할
     *
     * @param array $texts 입력 텍스트 배열
     * @return array 분할된 텍스트 청크 배열
     */
    public function createChunks(string|array $texts): array
    {
        $resultTexts = [];
        $previousOverlap = '';

        if (is_string($texts))
            $texts = [$texts];

        foreach ($texts as $text) {
            // 헤더 추출
            $headers = $this->extractHeaders($text);
            // pdfe($headers);
            $headerText = implode("\n", $headers);

            // 본문 추출 (헤더 제외)
            $contentLines = $this->extractContentLines($text, $headers);
            // pdfe($contentLines, "contentLines");

            // 청크를 배열로 관리 (성능 개선)
            $currentChunkLines = [$headerText];
            $currentTokenCount = $this->estimateTokens($headerText);

            if (!empty($previousOverlap)) {
                $currentChunkLines[] = $previousOverlap;
                $currentTokenCount += $this->estimateTokens($previousOverlap) + 2; // +2 for "\n\n"
            }

            // 본문 추가
            foreach ($contentLines as $i => $line) {
                // 현재 라인의 토큰만 계산 (증분 방식으로 O(n²) -> O(n) 개선)
                $lineTokens = $this->estimateTokens($line) + 1; // +1 for newline

                if ($i % 100 == 0) {
                    pd("", "[ $i / " . count($contentLines) . " ] chunking");
                    pd($currentTokenCount, "[ $i / " . count($contentLines) . " ] currentTokenCount");
                    pd($lineTokens, "[ $i / " . count($contentLines) . " ] lineTokens");
                    pd($this->maxTokens, "[ $i / " . count($contentLines) . " ] maxTokens");
                }

                if ($currentTokenCount + $lineTokens > $this->maxTokens) {

//                    pdf($currentTokenCount, "$i. tokenCount");
//                    pdf($this->maxTokens, "$i. maxTokens");

                    // 현재 청크 저장
                    $currentChunk = implode("\n", $currentChunkLines);
                    if (!empty(trim($currentChunk))) {
                        $resultTexts[] = trim($currentChunk);
                    }

                    // 오버랩 생성 (마지막 15%)
                    $previousOverlap = $this->createOverlap($currentChunk);

                    // 새 청크 시작 (헤더 + 오버랩 + 현재 라인)
                    $currentChunkLines = [$headerText, $previousOverlap, $line];
                    $currentTokenCount = $this->estimateTokens($headerText) +
                                       $this->estimateTokens($previousOverlap) +
                                       $lineTokens + 2; // +2 for "\n\n"
                } else {
                    $currentChunkLines[] = $line;
                    $currentTokenCount += $lineTokens;
                }
            }

            // 마지막 청크 저장
            $currentChunk = implode("\n", $currentChunkLines);
            if (!empty(trim($currentChunk))) {
                // pdf($currentChunk, "last! $currentChunk");
                $resultTexts[] = trim($currentChunk);
            }

            // 다음 문서를 위한 오버랩 생성
            $previousOverlap = $this->createOverlap($currentChunk);
        }

        pdf($resultTexts, "resultTexts", showArray: true);
        return $resultTexts;
    }

    /**
     * 마크다운 헤더 추출
     *
     * @param string $text 입력 텍스트
     * @return array 헤더 라인 배열
     */
    private function extractHeaders(string $text): array
    {
        $lines = explode("\n", $text);
        $headers = [];

        foreach ($lines as $line) {
            $trimmed = trim($line);
            // #, ##, ###, #### 로 시작하는 헤더 또는 메타데이터(- 로 시작)
            if (preg_match('/^#{1,4}\s+/', $trimmed) || preg_match('/^-\s+/', $trimmed)) {
                $headers[] = $line;
            } else {
                // 첫 번째 본문이 나오면 헤더 섹션 종료
                if (!empty($trimmed) && count($headers) > 0) {
                    break;
                }
            }
        }

        return $headers;
    }

    /**
     * 본문 라인 추출 (헤더 제외)
     *
     * @param string $text 입력 텍스트
     * @param array $headers 헤더 배열
     * @return array 본문 라인 배열
     */
    private function extractContentLines(string $text, array $headers): array
    {
        $lines = explode("\n", $text);
        $headerCount = count($headers);
        $contentLines = [];
        $inContent = false;

        foreach ($lines as $line) {
            $trimmed = trim($line);

            // 헤더 섹션 건너뛰기
            if (!$inContent) {
                $isHeader = preg_match('/^#{1,4}\s+/', $trimmed) ||
                    preg_match('/^-\s+/', $trimmed) ||
                    empty($trimmed);

                if (!$isHeader && !empty($trimmed)) {
                    $inContent = true;
                }
            }

            if ($inContent) {
                $contentLines[] = $line;
            }
        }

        return $contentLines;
    }

    /**
     * 토큰 수 추정 (한글 고려)
     * voyage-3-large는 대략 1 토큰 ≈ 1.5-2 글자 (한글 기준)
     *
     * @param string $text 텍스트
     * @return int 추정 토큰 수
     */
    private function estimateTokens(string $text): int
    {
        // 빈 문자열 체크
        if (empty($text)) return 0;

        // 한글 문자 수 계산 (더 효율적인 방식)
        $totalChars = mb_strlen($text);
        $nonKoreanText = preg_replace('/[\x{AC00}-\x{D7AF}]/u', '', $text);
        $koreanChars = $totalChars - mb_strlen($nonKoreanText);

        // 영문/숫자/기호 문자 수
        $otherChars = $totalChars - $koreanChars;

        // 한글: 1.5 글자 ≈ 1 토큰, 영문: 4 글자 ≈ 1 토큰
        return (int)(($koreanChars / 1.5) + ($otherChars / 4));
    }

    /**
     * 청크의 마지막 15%로 오버랩 생성
     *
     * @param string $chunk 현재 청크
     * @return string 오버랩 텍스트
     */
    private function createOverlap(string $chunk): string
    {
        $lines = explode("\n", $chunk);
        $totalLines = count($lines);

        // $overlapLineCount = max(1, (int)($totalLines * 0.15));
        $overlapLineCount = max(1, (int)($totalLines * (1 - $this->mainRatio) / 2));

        // 마지막 15% 라인 추출
        $overlapLines = array_slice($lines, -$overlapLineCount);

        return implode("\n", $overlapLines);
    }
}

// 사용 예제
/*
$splitter = new EmbeddingChunkSplitter(16000, 0.7);

$texts = [
    "# 대기환경보전법\n- 법령 ID : 00177320270110_276715\n...",
    "# 대기환경보전법\n- 법령 ID : 00177320270110_276715\n...",
    // ... more texts
];

$chunks = $splitter->createChunks($texts);

foreach ($chunks as $index => $chunk) {
    echo "=== Chunk $index ===\n";
    echo $chunk . "\n\n";
}
*/