# -*- coding: utf-8 -*-
"""
chunk_and_summary_v2.py

기능:
- 정리중 폴더 아래의 .md 파일들을 대상으로
  1) domain_prompt.txt 기반 GPT 의미 청킹
  2) 청크별 요약 생성
  3) 청크 내 이미지 참조 보존
  4) JSON 저장 (원문+청크+요약+이미지)
  5) RAG용 *_chunks.json 저장

전제:
- extract_1_v2.py 실행 후 .md 파일들이 존재할 것
- step1_domainprompt.py 실행 후 domain_prompt.txt가 존재할 것
"""

import os
import sys
import json
import re
from pathlib import Path
from datetime import datetime

from openai import OpenAI
from api_config import API_KEYS

# ===== 경로 =====
DATA_ROOT      = Path(r"D:\for python\survey_test\process")  
OUTPUT_ROOT    = Path(r"D:\for python\survey_test\output")

TEXT_DIR       = OUTPUT_ROOT / "text"
JSON_DIR       = OUTPUT_ROOT / "json"
RAG_DIR        = OUTPUT_ROOT / "rag"
CONTEXT_DIR    = OUTPUT_ROOT / "context"
LOG_DIR        = OUTPUT_ROOT / "logs"

for d in [TEXT_DIR, JSON_DIR, RAG_DIR, CONTEXT_DIR, LOG_DIR]:
    d.mkdir(parents=True, exist_ok=True)

# ===== OpenAI 설정 =====
OPENAI_API_KEY = API_KEYS.get('GPT_API_KEY', '')
GPT_MODEL      = "gpt-5-2025-08-07"

client = OpenAI(api_key=OPENAI_API_KEY)

# ===== 스킵할 폴더 =====
SKIP_DIR_NAMES = {"System Volume Information", "$RECYCLE.BIN", ".git", "__pycache__", "output"}

# ===== 이미지 참조 패턴 =====
IMAGE_PATTERN = re.compile(r'!\[([^\]]*)\]\(([^)]+)\)')


def log(msg: str):
    print(msg, flush=True)
    with (LOG_DIR / "chunk_and_summary_log.txt").open("a", encoding="utf-8") as f:
        f.write(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}\n")


def load_domain_prompt() -> str:
    p = CONTEXT_DIR / "domain_prompt.txt"
    if not p.exists():
        log(f"domain_prompt.txt가 없습니다: {p}")
        log("먼저 step1_domainprompt.py를 실행해야 합니다.")
        sys.exit(1)
    return p.read_text(encoding="utf-8", errors="ignore").strip()


def safe_rel(p: Path) -> str:
    """DATA_ROOT 기준 상대 경로 반환"""
    try:
        return str(p.relative_to(DATA_ROOT))
    except Exception:
        return str(p)


def extract_text_md(p: Path) -> str:
    """마크다운 파일 텍스트 읽기"""
    try:
        return p.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return p.read_text(encoding="cp949", errors="ignore")


def find_images_in_text(text: str) -> list:
    """텍스트에서 이미지 참조 찾기"""
    matches = IMAGE_PATTERN.findall(text)
    return [{"alt": m[0], "path": m[1]} for m in matches]


def semantic_chunk(domain_prompt: str, text: str, source_name: str):
    """GPT 기반 의미 청킹"""
    if not text.strip():
        return []

    # 텍스트가 너무 짧으면 그냥 하나의 청크로
    if len(text) < 500:
        return [{
            "title": "전체 내용",
            "keywords": "",
            "content": text
        }]

    user_prompt = f"""
아래 문서를 의미 단위(문단/항목/섹션 등)로 분리하고,
각 청크는 title / keywords / content 를 포함한 JSON 배열로 출력하라.

규칙:
1. 추측 금지, 문서 내용 기반으로만 분리
2. 이미지 참조(![...](...))는 관련 텍스트와 같은 청크에 포함
3. 각 청크는 최소 100자 이상
4. keywords는 쉼표로 구분된 핵심 키워드 3~5개

문서:
{text[:12000]}

JSON 배열만 출력하라. 다른 설명 없이.
"""

    try:
        resp = client.chat.completions.create(
            model=GPT_MODEL,
            messages=[
                {"role": "system", "content": domain_prompt + "\n\n너는 의미 기반 청킹 전문가이다. JSON 배열만 출력한다."},
                {"role": "user", "content": user_prompt},
            ],
        )
        data = resp.choices[0].message.content.strip()
        
        # JSON 파싱 시도
        # ```json ... ``` 형식 처리
        if "```json" in data:
            data = data.split("```json")[1].split("```")[0].strip()
        elif "```" in data:
            data = data.split("```")[1].split("```")[0].strip()
        
        if data.startswith("["):
            return json.loads(data)
            
    except json.JSONDecodeError as e:
        log(f"[WARN] JSON 파싱 실패 ({source_name}): {e}")
    except Exception as e:
        log(f"[WARN] semantic_chunk API 실패 ({source_name}): {e}")

    # fallback: 페이지/섹션 기반 분리
    log(f"[INFO] Fallback 청킹 적용: {source_name}")
    return fallback_chunk(text)


def fallback_chunk(text: str) -> list:
    """GPT 실패 시 대체 청킹 (페이지/섹션 기반)"""
    chunks = []
    
    # 페이지 구분자로 분리 시도
    if "## Page " in text:
        pages = re.split(r'\n## Page \d+\n', text)
        for i, page_content in enumerate(pages):
            if page_content.strip():
                chunks.append({
                    "title": f"Page {i+1}",
                    "keywords": "",
                    "content": page_content.strip()
                })
    else:
        # 빈 줄 2개 이상으로 분리
        sections = re.split(r'\n{3,}', text)
        for i, section in enumerate(sections):
            if section.strip() and len(section.strip()) > 50:
                chunks.append({
                    "title": f"섹션 {i+1}",
                    "keywords": "",
                    "content": section.strip()
                })
    
    # 청크가 없으면 전체를 하나로
    if not chunks:
        chunks.append({
            "title": "전체 내용",
            "keywords": "",
            "content": text.strip()
        })
    
    return chunks


def summary_chunk(domain_prompt: str, text: str, limit: int = 300) -> str:
    """청크 요약 생성"""
    if not text.strip():
        return ""
    
    # 이미지 참조 제거 후 요약 (텍스트만)
    text_only = IMAGE_PATTERN.sub('', text).strip()
    
    if len(text_only) < 100:
        return text_only
    
    prompt = f"""
아래 텍스트를 {limit}자 이내로 사실 기반으로 요약하라.
추측 금지, 고유명사와 수치는 보존.

{text_only[:8000]}
"""
    try:
        resp = client.chat.completions.create(
            model=GPT_MODEL,
            messages=[
                {"role": "system", "content": domain_prompt + "\n\n너는 사실만 요약하는 전문가이다."},
                {"role": "user", "content": prompt},
            ],
        )
        return resp.choices[0].message.content.strip()
    except Exception as e:
        log(f"[WARN] summary 실패: {e}")
        return text_only[:limit]


def save_chunk_files(src: Path, text: str, domain_prompt: str) -> int:
    """
    의미 청킹 → 요약 → JSON 저장
    
    Returns:
        생성된 청크 수
    """
    stem = src.stem
    folder_ctx = safe_rel(src.parent)
    
    # 원문 저장
    (TEXT_DIR / f"{stem}_text.txt").write_text(text, encoding="utf-8", errors="ignore")
    
    # 의미 청킹
    chunks = semantic_chunk(domain_prompt, text, src.name)
    
    if not chunks:
        log(f"[WARN] 청크 없음: {src.name}")
        return 0
    
    rag_items = []
    
    for idx, ch in enumerate(chunks, start=1):
        content = ch.get("content", "")
        
        # 요약 생성
        summ = summary_chunk(domain_prompt, content, 300)
        
        # 이 청크에 포함된 이미지 찾기
        images_in_chunk = find_images_in_text(content)
        
        rag_items.append({
            "source": src.name,
            "source_path": safe_rel(src),
            "chunk": idx,
            "total_chunks": len(chunks),
            "title": ch.get("title", ""),
            "keywords": ch.get("keywords", ""),
            "text": content,
            "summary": summ,
            "folder_context": folder_ctx,
            "images": images_in_chunk,
            "has_images": len(images_in_chunk) > 0
        })
    
    # JSON 저장
    (JSON_DIR / f"{stem}.json").write_text(
        json.dumps(rag_items, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )
    
    # RAG용 JSON 저장
    (RAG_DIR / f"{stem}_chunks.json").write_text(
        json.dumps(rag_items, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )
    
    return len(chunks)


def main():
    log("=" * 60)
    log("청킹/요약 파이프라인 시작")
    log(f"데이터 폴더: {DATA_ROOT}")
    log(f"출력 폴더: {OUTPUT_ROOT}")
    log("=" * 60)
    
    # 도메인 프롬프트 로드
    domain_prompt = load_domain_prompt()
    log(f"도메인 프롬프트 로드 완료 ({len(domain_prompt)}자)")
    
    # 통계
    stats = {"docs": 0, "chunks": 0, "images": 0, "errors": 0}
    
    # .md 파일 찾기
    md_files = []
    for root, dirs, files in os.walk(DATA_ROOT):
        dirs[:] = [d for d in dirs if d not in SKIP_DIR_NAMES and not d.startswith(".")]
        for fname in files:
            if fname.lower().endswith(".md"):
                md_files.append(Path(root) / fname)
    
    log(f"\n총 {len(md_files)}개 .md 파일 발견\n")
    
    for idx, fpath in enumerate(md_files, 1):
        try:
            rel_path = safe_rel(fpath)
            log(f"[{idx}/{len(md_files)}] {rel_path}")
            
            # 텍스트 읽기
            text = extract_text_md(fpath)
            
            if not text.strip():
                log(f"    ⚠ 빈 파일, 스킵")
                continue
            
            # 이미지 개수 확인
            images = find_images_in_text(text)
            stats["images"] += len(images)
            
            # 청킹 및 저장
            chunk_count = save_chunk_files(fpath, text, domain_prompt)
            
            stats["docs"] += 1
            stats["chunks"] += chunk_count
            
            log(f"    ✓ {chunk_count}개 청크, {len(images)}개 이미지")
            
        except Exception as e:
            stats["errors"] += 1
            log(f"    ✗ 오류: {e}")
    
    # 전체 통계 저장
    summary = {
        "processed_at": datetime.now().isoformat(),
        "data_root": str(DATA_ROOT),
        "output_root": str(OUTPUT_ROOT),
        "statistics": stats
    }
    
    (LOG_DIR / "chunk_summary_stats.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )
    
    # 결과 출력
    log("\n" + "=" * 60)
    log("청킹/요약 완료!")
    log("=" * 60)
    log(f"처리된 문서: {stats['docs']}개")
    log(f"생성된 청크: {stats['chunks']}개")
    log(f"포함된 이미지: {stats['images']}개")
    log(f"오류: {stats['errors']}개")
    log(f"\n결과 저장 위치:")
    log(f"  - 원문: {TEXT_DIR}")
    log(f"  - JSON: {JSON_DIR}")
    log(f"  - RAG: {RAG_DIR}")


if __name__ == "__main__":
    main()