<?php

namespace platyFramework;

//require_once("./db.php");
//require_once("./Elastic.php");
//require_once("./ptyCliLog.php");

ini_set('memory_limit', '2G');
ini_set('max_execution_time', '0');
set_time_limit(0);


require_once(__DIR__ . "/../elastic/Elastic.php");

class NewEmbeddingSectionText
{
    /** @var string 섹션 제목 */
    public string $sectionTitle;

    /** @var string 섹션 본문 텍스트 */
    public string $text;
}

class SomeEmbeddingItemsModel extends ptyItemsModel
{
    protected $vectorTableName;

    // protected $_tableName;
    protected $indexName;

    /** @var x_DBConnector */
    // protected $db;

    /** @var Elastic */
    private $elastic;

    /** @var ptyCliLog */
    protected $cliLog;

    /*
    public function __construct($db, $tableName, $vectorTableName, Elastic $elastic, $cliLog)
    {
        define('PLATYFRAMEWORK_DB_CHARSET', 'utf8mb4');
        define('PLATYFRAMEWORK_DB_COLLATE', 'utf8mb4_unicode_ci');

        date_default_timezone_set('Asia/Seoul');
        $this->cliLog = $cliLog;

        $this->vectorTableName = $vectorTableName;

        $this->_tableName = $tableName;
        $this->indexName = $vectorTableName; // "ai_vector_embedding_" . $tableName;
        $this->db = $db;
        $this->db->connect();
        $this->cliLog->mysql("디비 연결됨");

        $this->elastic = $elastic;
        $this->_migrate();
        $this->buildElasticIndex();
    }
    */

    public function __construct(string $vectorTableName, Elastic $elastic, ptyCliLog $cliLog)
    {
        $this->vectorTableName = $vectorTableName;
        $this->_tableName = $vectorTableName;
        $this->elastic = $elastic;
        $this->cliLog = $cliLog;
        $this->indexName = $this->vectorTableName;

        parent::__construct(tableName: $vectorTableName);

        $this->buildElasticIndex();
    }


    function _migrate()
    {

        if (!$this->db->sql_result("SHOW TABLES LIKE '$this->vectorTableName'")) {
            $this->db->sql_query("
			CREATE TABLE IF NOT EXISTS `{$this->vectorTableName}` (
			`id` int(11) NOT NULL AUTO_INCREMENT,
			`descId` int(11) NULL DEFAULT 0,
			`enabled` tinyint(1) NOT NULL DEFAULT '1',
			`regDateTime` datetime NULL,

			`ip` varchar(64) NULL,
			`serviceUserId` varchar(64) NOT NULL,
			`userId` varchar(64) NOT NULL,
			
            `serviceName` varchar(64) null default '',
            `serviceId` varchar(64) null default '',
			    
            `docId` varchar(64) null default '',
            `docIndex` varchar(64) null default '',
			    
            `title` TEXT null comment '문서 제목',
			    
            `text` MEDIUMTEXT NULL comment '원문 전체 텍스트',
			    
            `chunks` JSON NULL COMMENT 'AI 임베딩을 위한 chunks 된 백터 결과 json',
            `embeddings` JSON NULL COMMENT 'AI 임베딩을 위한 chunks 된 백터 결과 json',
			    
            `embeddingStatus` varchar(50) NULL DEFAULT 'NOT_STARTED' COMMENT '벡터 임베딩 값 생성 여부. NOT_STARTED: 시작되지 않음. IN_PROGRESS: 진행중, SUCCESS:성공, FAILURE: 실패',

			PRIMARY KEY (`id`),
			INDEX (`enabled`),
            INDEX `idx_serviceName` (`serviceName`),
            INDEX `idx_serviceId` (`serviceId`),
            INDEX `idx_docId` (`docId`),
            INDEX `idx_docIndex` (`docIndex`),
            INDEX `idx_title` (`title`(100)),
            INDEX `idx_text` (`text`(5)),
            # INDEX `idx_chunks` (`chunks`(5)),
            # INDEX `idx_embeddings` (`embeddings`(5)),
            INDEX `idx_embeddingStatus` (`embeddingStatus`),
            UNIQUE KEY `uniq_serviceNameId` (`serviceName`, `serviceId`, `docIndex`)
			) ENGINE=InnoDB DEFAULT CHARSET=" . PLATYFRAMEWORK_DB_CHARSET . " COLLATE=" . PLATYFRAMEWORK_DB_COLLATE . " AUTO_INCREMENT=1;
		");
        }

        /*
        if (!$this->db->sql_result("SHOW COLUMNS FROM `$this->vectorTableName` LIKE 'title'")) {
            $this->db->sql_query("ALTER TABLE `$this->vectorTableName` ADD `title` varchar(256) null default '' COMMENT '제목'");
        }

        $this->cliLog->mysql("{$this->vectorTableName} 의 AI 테이블 마이그레이션 체크");
        if (!$this->db->sql_result("SHOW COLUMNS FROM `$this->vectorTableName` LIKE 'aiVectorEmbedding_text'")) {
            $this->db->sql_query("ALTER TABLE `$this->vectorTableName` ADD `aiVectorEmbedding_text` mediumtext null COMMENT 'AI 를 위한 원문 전체'");
        }


        if (!$this->db->sql_result("SHOW COLUMNS FROM `$this->vectorTableName` LIKE 'aiVectorEmbedding_chunks'")) {
            $this->db->sql_query("ALTER TABLE `$this->vectorTableName` ADD `aiVectorEmbedding_chunks` json null COMMENT 'AI 임베딩을 위한 chunks 된 JSON 텍스트'");
        }

        if (!$this->db->sql_result("SHOW COLUMNS FROM `$this->vectorTableName` LIKE 'aiVectorEmbedding_results'")) {
            $this->db->sql_query("ALTER TABLE `$this->vectorTableName` ADD `aiVectorEmbedding_results` json null COMMENT 'AI 임베딩을 위한 chunks 된 백터 결과 json'");
        }

        if (!$this->db->sql_result("SHOW COLUMNS FROM `$this->vectorTableName` LIKE 'aiVectorEmbedding_dateTime'")) {
            $this->db->sql_query("ALTER TABLE `{$this->vectorTableName}` ADD `aiVectorEmbedding_dateTime` datetime default NULL");
        }

        if (!$this->db->sql_result("SHOW INDEX from `$this->vectorTableName` where Key_name = 'idx_aiVectorEmbedding_status'")) {
            $this->db->sql_query("ALTER TABLE `$this->vectorTableName` ADD aiVectorEmbedding_status ENUM('NOT_STARTED', 'IN_PROGRESS', 'SUCCESS', 'FAILURE') NOT NULL DEFAULT 'NOT_STARTED' COMMENT '벡터 임베딩 값 생성 여부. NOT_STARTED: 시작되지 않음. IN_PROGRESS: 진행중, SUCCESS:성공, FAILURE: 실패'");
            $this->db->sql_query("ALTER TABLE `$this->vectorTableName` ADD INDEX `idx_aiVectorEmbedding_status` (`aiVectorEmbedding_status`)");
        }
        */

        $this->cliLog->mysql("{$this->vectorTableName} 의 AI 테이블 마이그레이션 체크 완료");
    }

    public function resetInProgressItems()
    {
        $this->cliLog->info("resetInProgressItems() 중간에 중지된 항목에 대해 리셋. IN_PROGRESS 항목을 NOT_STARTED 로 전환.");

        # [cpueblo]
        $this->db->sql_query("delete from {$this->vectorTableName} where aiVectorEmbedding_status = 'NOT_STARTED' OR aiVectorEmbedding_status = 'IN_PROGRESS'");

        $this->db->sql_query("update {$this->vectorTableName} set aiVectorEmbedding_status = 'NOT_STARTED' WHERE aiVectorEmbedding_status = 'IN_PROGRESS'");

        $this->cliLog->info("resetInProgressItems() finished");
    }

    public function removeAIVectorEmbeddings()
    {
        $this->cliLog->info("removeAIVectorEmbeddings() 모든 항목을 NOT_STARTED 로 세팅하여 전체 재빌드 대상으로 설정");
        # $this->db->sql_query("update {$this->_tableName} set aiVectorEmbedding_status = 'NOT_STARTED' where aiVectorEmbedding_status != 'NOT_STARTED'");
        // $this->db->sql_query("update {$this->vectorTableName} set aiVectorEmbedding_status = 'NOT_STARTED' where aiVectorEmbedding_status IN ('IN_PROGRESS', 'SUCCESS', 'FAILURE')");
        $this->db->sql_query("TRUNCATE TABLE {$this->vectorTableName}");
    }

//
//    /**
//     * 새로운 아이템을 가져오는 메서드 (필수 구현)
//     *
//     * @return ptyArrayItemModel
//     * @property string $serviceName
//     * @property string $serviceId
//     * @property string $documentTitle
//     * @property EmbeddingSectionText[] $sectionTexts
//     * /
//     */
//    abstract protected function getNewItem();
//
//    abstract protected function getTitleItemsByIds(array $serviceIds);
//
//    /**
//     * 벡터 임베딩 결과를 DB에 업데이트하는 메서드 (필수 구현)
//     * @param array|null $item 업데이트할 아이템 데이터
//     * @return void
//     */
//    abstract protected function updateVectorItem(?ptyArrayItemModel $item, string $status = 'SUCCESS');


    function updateTitleFields($fieldName = "title")
    {
        $this->cliLog->info("updateTitleFields() 시작. 인덱스: {$this->indexName}");

        // title 필드가 인덱스에 없으면 먼저 추가
        $this->migrateStringField($fieldName, 500);

        $scrollSize = 100; // 한 번에 가져올 문서 수
        $scrollTime = '2m'; // scroll 유지 시간
        $totalUpdated = 0;
        $iterationCount = 0; // 반복 횟수 카운트
        $maxIterations = 10000; // 최대 반복 횟수 (안전장치)

        // 초기 scroll search 시작 - title이 없는 문서만 조회
        $this->cliLog->info("$fieldName 이 비어있는 문서 검색 시작...");

        $searchBody = [
            "size" => $scrollSize,
            "query" => [
                "bool" => [
                    "must_not" => [
                        "exists" => [
                            "field" => $fieldName
                        ]
                    ]
                ]
            ],
            "_source" => ["serviceName", "serviceId", "chunkId", "title"]
        ];

        try {
            // print_r($searchBody); exit;
            echo "scrollTime = $scrollTime\n";
            $response = $this->elastic->search("{$this->indexName}/_search?scroll={$scrollTime}", $searchBody);
            $scrollId = $response['_scroll_id'] ?? null;
            $hits = $response['hits']['hits'] ?? [];
            $total = $response['hits']['total']['value'] ?? 0;
            // print_r($response); exit;

            $this->cliLog->info("$fieldName 이 없는 문서 총 {$total}건 발견");

            // print_r($hits); exit;

            while (!empty($hits)) {
                $iterationCount++;

                // 안전장치: 최대 반복 횟수 체크
                if ($iterationCount > $maxIterations) {
                    $this->cliLog->warning("최대 반복 횟수({$maxIterations})에 도달. 종료합니다.");
                    break;
                }

                $this->cliLog->info("배치 처리중... (반복: {$iterationCount}, 현재까지 업데이트: {$totalUpdated}건)");

                // precedent_mst에서 한 번에 조회할 serviceId 목록 생성
                $serviceIds = [];
                $docsByServiceId = [];

                foreach ($hits as $hit) {
                    $source = $hit['_source'];
                    $serviceId = $source['serviceId'] ?? null;

                    if ($serviceId) { // 모든 chunk에 대해 업데이트
                        if (!in_array($serviceId, $serviceIds)) {
                            $serviceIds[] = $serviceId;
                        }
                        if (!isset($docsByServiceId[$serviceId])) {
                            $docsByServiceId[$serviceId] = [];
                        }
                        $docsByServiceId[$serviceId][] = $hit['_id'];
                    }
                }

                if (!empty($serviceIds)) {
                    // MySQL에서 한 번에 조회

                    // print_r($serviceIds); exit;
                    $titleMap = $this->getTitleItemsByIds($serviceIds);

                    // 각 문서의 title 업데이트
                    foreach ($docsByServiceId as $serviceId => $docIds) {
                        $title = $titleMap[$serviceId] ?? null;

                        if ($title) {
                            foreach ($docIds as $docId) {
                                try {
                                    $updateData = [
                                        "doc" => [
                                            "title" => $title
                                        ]
                                    ];

                                    $this->elastic->post("{$this->indexName}/_update/{$docId}", $updateData);
                                    $totalUpdated++;

                                    if ($totalUpdated % 10 == 0) {
                                        $this->cliLog->info("진행중... {$totalUpdated}건 업데이트 완료");
                                    }
                                } catch (Exception $e) {
                                    $this->cliLog->warning("문서 업데이트 실패 - docId: {$docId}, 오류: " . $e->getMessage());
                                }
                            }
                        } else {
                            $this->cliLog->warning("precedent_mst에서 serviceId={$serviceId}에 대한 prec_case_name을 찾을 수 없음");
                        }
                    }
                }

                // 다음 배치 가져오기
                if (!$scrollId) {
                    break;
                }

                try {
                    $scrollBody = [
                        "scroll" => $scrollTime,
                        "scroll_id" => $scrollId
                    ];

                    $response = $this->elastic->post("_search/scroll", $scrollBody);
                    $scrollId = $response['_scroll_id'] ?? null;
                    $hits = $response['hits']['hits'] ?? [];

                    print_r($response);

                    // 더 이상 결과가 없으면 종료
                    if (empty($hits)) {
                        $this->cliLog->info("더 이상 처리할 문서가 없습니다. 종료합니다.");
                        break;
                    }

                } catch (Exception $e) {
                    $this->cliLog->error("scroll 조회 실패: " . $e->getMessage());
                    break;
                }
            }

            // scroll 정리
            if ($scrollId) {
                try {
                    $this->elastic->delete("_search/scroll/{$scrollId}");
                } catch (Exception $e) {
                    // scroll 삭제 실패는 무시 (자동으로 만료됨)
                }
            }

            $this->cliLog->success("updateTitleFields() 완료. 총 {$totalUpdated}건 업데이트됨");

        } catch (Exception $e) {
            $this->cliLog->error("updateTitleFields() 오류: " . $e->getMessage());
            throw $e;
        }
    }

    function run()
    {
        $this->cliLog->info("벡터 임베딩 시작. DB 테이블명 = $this->_tableName, 임베딩 테이블명 = $this->vectorTableName, Elastic Index 명 = $this->indexName");

        while (1) {
            $this->cliLog->separator();

//            $cnt = $this->db->sql_fetch("select now() as now, (select count(*) from {$this->vectorTableName} where aiVectorEmbedding_status != 'NOT_STARTED') as count, (select count(*) from $this->vectorTableName) as totalCount");
//            $percent = sprintf("%2.1f", ($cnt['count'] / $cnt['totalCount']) * 100);
//            $this->cliLog->info("[ {$cnt['now']} {$cnt['count']} / {$cnt['totalCount']} 건. $percent % ]");

            $this->cliLog->info("getNewItem!");
            # region 새로운 아이템으로부터 aiVectorEmbedding_text 를 chunks 로 변환
            $item = $this->getNewItem();
            // pdfe($item);

            // 검증
            if (true) {
                if ($item->sectionTexts == "") {
                    print_r($item);
                    echo "aiVectorEmbedding_text is empty. skipped";
                    $this->updateVectorItem($item, status: "FAILURE");
                    continue;
                }


                if ($item->serviceName == "") die ("serviceName 을 지정해야 함");
                if ($item->serviceId == "") die ("serviceId 을 지정해야 함");
            }

            $serviceName = $item->serviceName;
            $serviceId = $item->serviceId;
            $title = $item->documentTitle;
            $text = $item->sectionTexts;

            // $this->embeddingByText($serviceName, $serviceId, $title, $text);

            // text 를 이용하여 chunk vector 값을 추출

            $embeddings = [];
            if (is_array($text)) {
                /**
                 * text 는 아래처럼 구성되어 있음
                 * text[]['title'] = 문서 제목
                 * text[]['sectionTitle'] = 섹션 제목
                 * text[]['text'] = 섹션의 내용
                 */
                foreach ($text as $sectionId => $_it) {
                    $documentTitle = $title;
                    $sectionTitle = $_it['sectionTitle'];
                    $sectionText = $_it['sectionText'];
                    $chunks = $this->chunksText($sectionText);
                    pd($chunks, "documentTitle: $documentTitle, sectionTitle: $sectionTitle, sectionId: $sectionId, chunks = ");

                    // [test]
                    // if ($sectionId > 3) break;

                    foreach ($chunks as $chunkId => $chunk) {

                        $this->cliLog->info("벡터 생성중: documentTitle: $documentTitle, sectionTitle: $sectionTitle, sectionId: $sectionId");
                        $embedding = $this->getClaudeVoyage3largeVectorEmbedding($chunk);
                        $embeddings[] = $embedding;
                        pd($embedding, "embeddings");
                        $this->recordElastic(
                            serviceName: $serviceName,
                            serviceId: $serviceId,
                            documentTitle: $documentTitle,
                            sectionId: $sectionId,
                            sectionTitle: $sectionTitle,
                            chunkId: $chunkId,
                            chunkText: $chunk,
                            embedding: $embedding,
                        );
                    }

                    $item->_item['results']['sections'][$sectionId]['sectionId'] = $sectionId;
                    $item->_item['results']['sections'][$sectionId]['sectionTitle'] = $sectionTitle;
                    $item->_item['results']['sections'][$sectionId]['sectionText'] = $sectionText;
                    $item->_item['results']['sections'][$sectionId]['chunkTexts'] = $chunks;
                    $item->_item['results']['sections'][$sectionId]['chunkEmbeddings'] = $embeddings;
                }

            }


            $this->updateVectorItem($item);
        }

        $this->cliLog->info("벡터 임베딩 종료");
    }


    /**
     * 긴 텍스트를 chunk 단위로 분할하는 예시
     * 기준: text-embedding-3-small (최대 8191 tokens)
     * 한국어 1자 ≈ 1.5 토큰 가정
     */
    function chunksText($text, $maxTokens = 20 * 1024)
    {
        /*
// 한국어 기준: 1자 ≈ 1.5 토큰 → 안전하게 1자당 2토큰으로 잡음
        $maxChars = intval($maxTokens / 2);

        $chunks = [];
        $length = mb_strlen($text, 'UTF-8');
        $offset = 0;

        while ($offset < $length) {
            $chunk = mb_substr($text, $offset, $maxChars, 'UTF-8');
            $chunks[] = $chunk;
            $offset += $maxChars;
        }

        return $chunks;
        */

        require_once("./ptyLibrary_PHP/aivectorembedding/EmbeddingChunkSplitter.php");
        $m = new EmbeddingChunkSplitter(maxTokens: $maxTokens, mainRatio: 0.8);
        $s = $m->createChunks([$text]);

        return $s;
    }


    public function getOpenAIVectorEmbedding($text)
    {
// follaw_vector_embedding
        $apiKey = "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";

// OpenAI Embeddings API 호출
        $ch = curl_init("https://api.openai.com/v1/embeddings");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            "Content-Type: application/json",
            "Authorization: Bearer " . $apiKey
        ]);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
            "input" => $text,
            "model" => "text-embedding-3-large" // 또는 text-embedding-3-large
        ]));

        $response = curl_exec($ch);
        if ($response === false) {
            throw new Exception("Curl error: " . curl_error($ch));
        }
        curl_close($ch);

        $result = json_decode($response, true);
        if (!isset($result["data"][0]["embedding"])) {
            throw new Exception("Embedding not found in response: " . $response . ", text = $text");
        }

//        echo "response ====\n";
//        print_r($response);

        return $result;
// $vector = $result["data"][0]["embedding"];
//        echo "vector ====\n";
//        print_r($vector);

// return $vector;
    }

    /**
     * 차원수 = 1024
     * @param $text
     * @return mixed
     * @throws Exception
     */
    public function getClaudeVoyage3largeVectorEmbedding($text)
    {
        // Voyage AI API Key (Anthropic의 파트너인 Voyage AI 사용)
        # $apiKey = "pa-YOUR_VOYAGE_API_KEY_HERE";
        # $apiKey = "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";
        $apiKey = "pa-ALJwit6aYzcbCD-xd-ZPrntFQd9jjMBQ3O0QGQp395o";

        // Voyage AI Embeddings API 호출
        $ch = curl_init("https://api.voyageai.com/v1/embeddings");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            "Content-Type: application/json",
            "Authorization: Bearer " . $apiKey
        ]);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
            "input" => $text,
            "model" => "voyage-3-large" // voyage-3-large 모델 (1024 dimensions)
        ]));

        $response = curl_exec($ch);
        if ($response === false) {
            throw new Exception("Curl error: " . curl_error($ch));
        }
        curl_close($ch);

        $result = json_decode($response, true);
        if (!isset($result["data"][0]["embedding"])) {
            throw new Exception("Embedding not found in response: " . $response . ", text = $text");
        }

        // print_r($result);

        return $result;
    }


    public function deleteElasticMetaIndex()
    {
        // 인덱스 존재 여부 확인
        if ($this->elastic->indexExists($this->indexName)) {
            // 임시로 인덱스가 있으면 삭제
            $this->cliLog->warning("기존 인덱스 삭제 중: {$this->indexName}");
            try {
                $this->elastic->delete($this->indexName);
                $this->cliLog->success("인덱스 삭제 완료");
            } catch (Exception $e) {
                $this->cliLog->error("인덱스 삭제 실패: " . $e->getMessage());
                throw $e;
            }
        }
    }

    private function buildElasticIndex()
    {
        if ($this->elastic->indexExists($this->indexName)) {
            $this->cliLog->elastic("buildElasticIndex() 인덱스가 이미 존재하므로 생성을 건너뜀: {$this->indexName}");
            return true;
        }

        $mappingData = [
            "mappings" => [
                "properties" => [

                    "serviceName" => ["type" => "keyword"],
                    "serviceId" => ["type" => "keyword"],

                    "docId" => ["type" => "integer"],
                    "docIndex" => ["type" => "integer"],

                    "title" => [
                        "type" => "text",
                        "fields" => [
                            "keyword" => [
                                "type" => "keyword",
                                "ignore_above" => 512,
                            ]
                        ]
                    ],

                    "text" => ["type" => "text"],
                    "chunks" => ["type" => "text"],

                    "vector" => [
                        "type" => "dense_vector",
                        "dims" => 1024 /* voyage3-large = 1024, small = 1536, large = 3072 */,
                        "index" => true,
                        "similarity" => "cosine"
                    ],
                    "regDateTime" => [
                        "type" => "date",
                        "format" => "strict_date_optional_time||epoch_millis"
                        // "format" => "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss||epoch_millis"
                    ],
                ]
            ]
        ];

        $this->cliLog->warning("새 Elasticsearch 인덱스 생성: {$this->indexName}");

        try {
            $response = $this->elastic->put($this->indexName, $mappingData);

            if (isset($response['acknowledged']) && $response['acknowledged']) {
                $this->cliLog->success("인덱스 생성 완료");
                return true;
            } else {
                $this->cliLog->warning("인덱스 생성 응답이 예상과 다름", $response);
                return false;
            }

        } catch (Exception $e) {
            if (strpos($e->getMessage(), 'resource_already_exists_exception') !== false) {
                $this->cliLog->warning("인덱스가 이미 존재함, 계속 진행");
                return true;
            } else {
                $this->cliLog->error("인덱스 생성 오류: " . $e->getMessage());
                throw $e;
            }
        }
    }

    /**
     * 인덱스에 문자열 필드가 없으면 추가하는 범용 메서드
     * @param string $fieldName 추가할 필드명
     * @param int $length keyword의 ignore_above 값 (기본값: 256)
     * @return bool
     */
    public function migrateStringField(string $fieldName, int $length = 256)
    {
        $this->cliLog->info("{$fieldName} 필드 존재 여부 확인 중: {$this->indexName}");

        try {
            // 인덱스가 존재하지 않으면 건너뜀
            if (!$this->elastic->indexExists($this->indexName)) {
                $this->cliLog->warning("인덱스가 존재하지 않아 {$fieldName} 필드 추가를 건너뜀: {$this->indexName}");
                return false;
            }

            // 현재 매핑 조회
            $mapping = $this->elastic->getMapping($this->indexName);

            // 매핑에서 properties 추출
            $properties = null;
            if (isset($mapping[$this->indexName]['mappings']['properties'])) {
                $properties = $mapping[$this->indexName]['mappings']['properties'];
            }

            // 필드가 이미 존재하는지 확인
            if ($properties && isset($properties[$fieldName])) {
                $this->cliLog->info("{$fieldName} 필드가 이미 존재함");
                return true;
            }

            // 필드 추가
            $this->cliLog->warning("{$fieldName} 필드가 없어서 추가 중... (keyword length: {$length})");

            $newProperties = [
                $fieldName => [
                    "type" => "text",
                    "fields" => [
                        "keyword" => [
                            "type" => "keyword",
                            "ignore_above" => $length
                        ]
                    ]
                ]
            ];

            $response = $this->elastic->updateMapping($this->indexName, $newProperties);

            if (isset($response['acknowledged']) && $response['acknowledged']) {
                $this->cliLog->success("{$fieldName} 필드 추가 완료");
                return true;
            } else {
                $this->cliLog->warning("{$fieldName} 필드 추가 응답이 예상과 다름", $response);
                return false;
            }

        } catch (Exception $e) {
            $this->cliLog->error("{$fieldName} 필드 추가 중 오류 발생: " . $e->getMessage());
            throw $e;
        }
    }

    /**
     * @param string $serviceName
     * @param string $serviceId
     * @param mixed $documentTitle
     * @param string $sectionId
     * @param string $sectionTitle
     * @param string $chunkId 청크 인덱스 ID
     * @param string $chunkText 청크 텍스트
     * @param array $embedding 임베딩 결과 배열
     * <pre>
     * {
     * "object": "list",
     * "data": {
     * 0: {
     * "object": "embedding",
     * "embedding": {
     * 0: -0.02778046 (numeric) ,
     * 1: 0.007716794 (numeric) ,
     * 2: 0.02794292 (numeric) ,
     * 3: 0.041589461 (numeric) ,
     * </pre>
     *
     * @return void
     */
    public function recordElastic(string $serviceName,
                                  string $serviceId,
                                  mixed  $documentTitle,
                                  string $docIndex,
                                  string $chunkIndex,
                                  string $chunk,
                                  array  $embedding,
    )
    {
        try {
            $embeddingArray = $embedding['data'][0]['embedding'];

            // Elasticsearch에 저장할 데이터 구성
            $data = [
                "serviceName" => $serviceName,
                "serviceId" => $serviceId,
                "documentTitletitle" => $documentTitle,
                "docIndex" => $docIndex,
                "chunkIndex" => $chunkIndex,
                "chunk" => $chunk,
                "vector" => $embeddingArray,
                "regDateTime" => date('Y-m-d\TH:i:sP')  // 2025-10-30 15:30:45+09:00
            ];

            // Elasticsearch document ID 생성
            $docId = "{$serviceName}_{$serviceId}_doc_{$docIndex}_chunk_{$chunkIndex}";

            $this->cliLog->info("Elastic 문서 생성중. 문서 id = $docId");

            $response = $this->elastic->post("{$this->indexName}/_doc/$docId", $data);

            if (isset($response['result'])) {
                $this->cliLog->success("Elastic 문서 생성 성공. {$response['result']}");
            } else {
                $this->cliLog->warning("Elastic 문서 생성. 예상과 다른 응답", $response);
            }

        } catch (\Exception $e) {
            $this->cliLog->error("Elastic 문서 생성. 인덱싱 오류: " . $e->getMessage());
        }
    }

    public function embeddingChunkStatusItems($log)
    {
        while (1) {
            try {
                $this->db->sql_query("START TRANSACTION");

                // region 카운터 정보
                {
                    // 카운터 획득
                    $totalCount = $this->db->sql_result("select count(*) from {$this->_tableName} where enabled = 1 ");
                    $count = $this->db->sql_result("select count(*) from {$this->_tableName} where enabled = 1 and embeddingStatus = 'SUCCESS_ELASTIC'");

                    $log->info("[ $count / $totalCount ] 진행율 " . sprintf("%2.1f%%", ($count / $totalCount) * 100));
                }
                // endregion

                // region SUCCESS_CHUNK 항목 아이템. => $item or exit
                {
                    $q = "SELECT * FROM {$this->_tableName} where embeddingStatus = 'SUCCESS_CHUNK' LIMIT 1 FOR UPDATE SKIP LOCKED";
                    $log->info("아이템 검색중. q = $q");
                    $item = $this->db->sqlFetchItem($q);

                    // pd($item, "item");
                    if ($item->docId === NULL || $item->docId == "") {
                        $log->info("데이터가 없음");
                        $this->db->sql_query("COMMIT");
                        return;
                    }

                    $item = SomeEmbeddingItemModel::buildByMap($item->_item);
                    $item->_tableName = $this->_tableName;

                    $log->info("아이템 검색됨 = {$item->docId}");
                }
                // endregion

                $item->embeddingStatus = "IN_PROGRESS";
                $item->updateWithItemName("embeddingStatus");
                $this->db->sql_query("COMMIT");

                $item->processVectorEmbeddingItems($this);

            } catch (\Throwable $e) {

                $log->error("에러. q = $q");
                ptyShowThrowable($e, $log);
                exit;
            }
        }

    }

    public function deleteEmbeddingItemsByServiceId(string $serviceName, string $serviceId, Elastic $elastic)
    {
        // serviceId 필드 값으로 문서 삭제
        // 예: "serviceId": "01291120251125_279905"
        $this->db->sql_query("DELETE FROM {$this->_tableName} WHERE serviceName = '$serviceName' and serviceId = '$serviceId'");
        $elastic->deleteBySourceField(
            indexName: "{$this->_tableName}",
            fieldName: "serviceId",
            fieldValue: $serviceId
        );
    }
}

?>