"""
embed_and_ingest.py
====================
PoC Task 2 - Neo4j Vector Migration
Pipeline utama: Parse PDF → Extract 5 Components → Embed → Ingest to Neo4j

Fase yang telah diimplementasikan:
  - Task 05: Parsing PDF dengan PyMuPDF
  - Task 06: Ekstraksi 5 Komponen Jurnal via Gemini LLM (Structured Output)
  - Task 07: Vektorisasi 5 Komponen via Gemini text-embedding-004 (768D)
  - Task 08: Injeksi Node ke Neo4j AuraDB (MERGE - anti duplikasi)
  - Task 09: Pembuktian Vector Search KNN (Cosine Similarity)
  - Task 10: Summary Generator (Gemini Reasoning untuk kemiripan antar jurnal)
  - Task 11: MERGE Edge (Relasi kemiripan antar node dengan skor + summary)

Cara menjalankan:
  python embed_and_ingest.py
"""

import os
import sys
import json
import glob
import uuid
import time
import hashlib
import fitz  # PyMuPDF
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from google import genai
from google.genai import types
from neo4j import GraphDatabase

# --- Load credentials ---
load_dotenv()

GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
NEO4J_URI      = os.getenv("NEO4J_URI", "")
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME", "neo4j")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "")

# === KONSTANTA ===
LITERATURE_DIR = os.path.join(os.path.dirname(__file__), "..", "Literature")
GEMINI_MODEL = "gemini-2.5-flash"
EMBEDDING_MODEL = "gemini-embedding-001"
EMBEDDING_DIMENSIONS = 768
API_RETRY_DELAY = 15  # Detik tunggu saat rate limit (free tier: 5 req/min)
API_MAX_RETRIES = 3   # Maks retry saat rate limit


# ============================================================
# FASE 1: Parsing PDF (Task 05)
# ============================================================

def extract_text_from_pdf(pdf_path: str) -> str:
    """
    Membaca seluruh halaman PDF dan menggabungkan teksnya menjadi satu string utuh.
    Menggunakan PyMuPDF (fitz) karena ringan dan handal untuk PDF akademis.

    Args:
        pdf_path: Path absolut ke file .pdf

    Returns:
        String berisi seluruh teks mentah dari PDF.
    """
    doc = fitz.open(pdf_path)
    full_text = ""
    for page_num, page in enumerate(doc, start=1):
        page_text = page.get_text("text")
        full_text += page_text
    doc.close()
    return full_text.strip()


def discover_pdfs(directory: str) -> list[str]:
    """
    Menemukan semua file *.pdf dalam sebuah direktori.

    Args:
        directory: Path ke folder yang berisi file PDF.

    Returns:
        List path absolut file PDF yang ditemukan, terurut.
    """
    pattern = os.path.join(directory, "*.pdf")
    pdf_files = sorted(glob.glob(pattern))
    return pdf_files


# ============================================================
# FASE 2: Gemini LLM Extraction (Task 06)
# ============================================================

class LiteratureComponents(BaseModel):
    """Schema Pydantic untuk 5 komponen pilar jurnal akademis."""
    background: str = Field(
        description="Latar belakang masalah atau konteks penelitian yang dibahas dalam jurnal."
    )
    methodology: str = Field(
        description="Metodologi, pendekatan, atau teknik yang digunakan dalam penelitian."
    )
    gap: str = Field(
        description="Celah penelitian (research gap) yang diidentifikasi oleh penulis."
    )
    objective: str = Field(
        description="Tujuan utama atau sasaran spesifik dari penelitian."
    )
    futurework: str = Field(
        description="Arah penelitian masa depan atau rekomendasi lanjutan yang disarankan penulis."
    )


# Prompt sistem untuk mengarahkan Gemini mengekstrak 5 komponen
EXTRACTION_SYSTEM_PROMPT = """You are a meticulous academic research analyst. Your task is to read the full text of a scientific paper and extract EXACTLY 5 structured components in English. Each component must be a concise paragraph (2-4 sentences) that captures the essence of that aspect.

The 5 components are:
1. **background**: The research context, motivation, and problem domain.
2. **methodology**: The specific methods, techniques, models, or algorithms used.
3. **gap**: The research gap or limitation in existing work that the paper addresses.
4. **objective**: The primary goal or specific aim of the research.
5. **futurework**: Future research directions or recommendations proposed by the authors.

Rules:
- Write each component in English, even if the paper is in another language.
- Be precise and factual. Do not hallucinate or invent information not present in the paper.
- Each component should be self-contained and understandable without reading the others.
- If a component is not explicitly stated in the paper, infer it carefully from the context."""


def extract_components_with_gemini(raw_text: str) -> LiteratureComponents | None:
    """
    Mengirim teks mentah jurnal ke Gemini LLM untuk diekstrak menjadi 5 komponen
    terstruktur menggunakan Pydantic schema (Structured Output).

    Args:
        raw_text: Teks mentah dari PDF jurnal.

    Returns:
        LiteratureComponents jika berhasil, None jika gagal.
    """
    client = genai.Client(api_key=GEMINI_API_KEY)

    # Potong teks jika terlalu panjang (safety measure untuk token limit)
    # Gemini 2.5 memiliki context window besar, tapi kita batasi 30k karakter untuk efisiensi
    truncated_text = raw_text[:30000]
    if len(raw_text) > 30000:
        truncated_text += "\n\n[... TEXT TRUNCATED FOR PROCESSING ...]"

    user_prompt = f"Here is the full text of a scientific paper. Extract the 5 components:\n\n{truncated_text}"

    try:
        response = client.models.generate_content(
            model=GEMINI_MODEL,
            contents=user_prompt,
            config=types.GenerateContentConfig(
                system_instruction=EXTRACTION_SYSTEM_PROMPT,
                response_mime_type="application/json",
                response_schema=LiteratureComponents,
                temperature=0.2,  # Low temperature untuk konsistensi
            ),
        )

        # Parse response ke Pydantic model
        components = LiteratureComponents.model_validate_json(response.text)
        return components

    except Exception as e:
        # Retry jika rate limited
        if "429" in str(e) or "RESOURCE_EXHAUSTED" in str(e):
            print(f"   ⏳ Rate limit hit, menunggu {API_RETRY_DELAY}s...")
            time.sleep(API_RETRY_DELAY)
            try:
                response = client.models.generate_content(
                    model=GEMINI_MODEL,
                    contents=user_prompt,
                    config=types.GenerateContentConfig(
                        system_instruction=EXTRACTION_SYSTEM_PROMPT,
                        response_mime_type="application/json",
                        response_schema=LiteratureComponents,
                        temperature=0.2,
                    ),
                )
                components = LiteratureComponents.model_validate_json(response.text)
                return components
            except Exception as e2:
                print(f"   ❌ Gemini API Error (retry): {e2}")
                return None
        print(f"   ❌ Gemini API Error: {e}")
        return None


# ============================================================
# FASE 3: Gemini Embedding (Task 07)
# ============================================================

# Nama 5 komponen yang akan di-embed (sesuai field di LiteratureComponents)
COMPONENT_NAMES = ["background", "methodology", "gap", "objective", "futurework"]


def embed_components(components: LiteratureComponents) -> dict[str, list[float]] | None:
    """
    Mengambil 5 komponen teks dari LiteratureComponents dan mengirimnya
    ke Gemini text-embedding-004 untuk diubah menjadi vektor 768 dimensi.

    Args:
        components: Objek LiteratureComponents berisi 5 teks komponen.

    Returns:
        Dict mapping nama komponen ke array float 768D, atau None jika gagal.
        Contoh: {"background": [0.012, -0.034, ...], "methodology": [...], ...}
    """
    client = genai.Client(api_key=GEMINI_API_KEY)

    # Kumpulkan 5 teks komponen secara berurutan
    component_texts = [getattr(components, name) for name in COMPONENT_NAMES]

    try:
        # Batch embed: kirim 5 teks sekaligus dalam satu API call
        # output_dimensionality=768 untuk match dengan Neo4j vector index
        response = client.models.embed_content(
            model=EMBEDDING_MODEL,
            contents=component_texts,
            config={'output_dimensionality': EMBEDDING_DIMENSIONS},
        )

        # Mapping hasil ke dict {nama_komponen: vektor}
        embeddings_dict = {}
        for i, name in enumerate(COMPONENT_NAMES):
            vector = response.embeddings[i].values
            embeddings_dict[name] = list(vector)

        return embeddings_dict

    except Exception as e:
        print(f"   ❌ Embedding API Error: {e}")
        return None


# ============================================================
# FASE 4: Injeksi Node ke Neo4j (Task 08)
# ============================================================

def generate_article_id(filename: str) -> str:
    """
    Membuat article_id deterministik dari nama file PDF.
    Menggunakan hash MD5 agar ID konsisten setiap kali file yang sama di-ingest,
    sehingga MERGE bisa mendeteksi duplikasi.

    Args:
        filename: Nama file PDF (tanpa path).

    Returns:
        String UUID-like identifier.
    """
    hash_hex = hashlib.md5(filename.encode('utf-8')).hexdigest()
    return str(uuid.UUID(hash_hex))


def ingest_node_to_neo4j(
    driver,
    article_id: str,
    title: str,
    components: LiteratureComponents,
    embeddings: dict[str, list[float]],
) -> bool:
    """
    Menanam (MERGE) satu node :Literature ke Neo4j AuraDB beserta
    5 properti teks komponen dan 5 properti vektor embedding.

    Menggunakan MERGE (bukan CREATE) agar:
    - Tidak terjadi duplikasi jika file yang sama di-ingest ulang.
    - Properti ter-update otomatis jika ada perubahan.

    Args:
        driver: Neo4j driver instance.
        article_id: ID unik jurnal (deterministik dari nama file).
        title: Judul jurnal (dari nama file PDF).
        components: Objek LiteratureComponents berisi 5 teks.
        embeddings: Dict mapping {komponen: vektor_768D}.

    Returns:
        True jika berhasil, False jika gagal.
    """
    cypher = """
    MERGE (n:Literature {article_id: $article_id})
    SET n.title                    = $title,
        n.text_background          = $text_background,
        n.text_methodology         = $text_methodology,
        n.text_gap                 = $text_gap,
        n.text_objective           = $text_objective,
        n.text_futurework          = $text_futurework,
        n.embedding_background     = $emb_background,
        n.embedding_methodology    = $emb_methodology,
        n.embedding_gap            = $emb_gap,
        n.embedding_objective      = $emb_objective,
        n.embedding_futurework     = $emb_futurework
    RETURN n.article_id AS id, n.title AS title
    """

    params = {
        "article_id":       article_id,
        "title":            title,
        "text_background":  components.background,
        "text_methodology": components.methodology,
        "text_gap":         components.gap,
        "text_objective":   components.objective,
        "text_futurework":  components.futurework,
        "emb_background":   embeddings["background"],
        "emb_methodology":  embeddings["methodology"],
        "emb_gap":          embeddings["gap"],
        "emb_objective":    embeddings["objective"],
        "emb_futurework":   embeddings["futurework"],
    }

    try:
        with driver.session() as session:
            result = session.run(cypher, params)
            record = result.single()
            if record:
                print(f"      📌 Node MERGED: id={record['id'][:12]}... title=\"{record['title'][:50]}...\"")
                return True
            return False
    except Exception as e:
        print(f"   ❌ Neo4j Error: {e}")
        return False


# ============================================================
# FASE 5: Vector Search KNN (Task 09)
# ============================================================

# Konfigurasi Vector Search
SIMILARITY_THRESHOLD = 0.65  # Ambang batas minimum cosine similarity
TOP_K = 5                    # Jumlah maksimal tetangga terdekat

# Mapping nama komponen ke nama index Neo4j
INDEX_MAP = {
    "background":  "background_emb",
    "methodology": "methodology_emb",
    "gap":         "gap_emb",
    "objective":   "objective_emb",
    "futurework":  "futurework_emb",
}


def search_similar_nodes(
    driver,
    article_id: str,
    embeddings: dict[str, list[float]],
) -> dict[str, list[dict]]:
    """
    Mencari node :Literature yang mirip dengan node baru menggunakan
    Native Vector Search KNN (db.index.vector.queryNodes) pada kelima
    indeks vektor secara terpisah.

    Args:
        driver: Neo4j driver instance.
        article_id: ID node baru (untuk mengecualikan dirinya sendiri).
        embeddings: Dict {komponen: vektor_768D} dari node baru.

    Returns:
        Dict mapping {komponen: [list of {article_id, title, score}]}.
        Hanya mengembalikan hasil di atas SIMILARITY_THRESHOLD.
    """
    results = {}

    for comp_name, vector in embeddings.items():
        index_name = INDEX_MAP[comp_name]

        cypher = """
        CALL db.index.vector.queryNodes($index_name, $top_k, $query_vector)
        YIELD node, score
        WHERE node.article_id <> $self_id
        RETURN node.article_id AS id,
               node.title AS title,
               score,
               node['text_' + $comp_name] AS neighbor_text
        ORDER BY score DESC
        """

        params = {
            "index_name":   index_name,
            "top_k":        TOP_K,
            "query_vector": vector,
            "self_id":      article_id,
            "comp_name":    comp_name,
        }

        try:
            with driver.session() as session:
                records = list(session.run(cypher, params))
                matches = []
                for rec in records:
                    if rec["score"] >= SIMILARITY_THRESHOLD:
                        matches.append({
                            "article_id":    rec["id"],
                            "title":         rec["title"],
                            "score":         round(rec["score"], 4),
                            "neighbor_text": rec["neighbor_text"] or "",
                        })
                results[comp_name] = matches
        except Exception as e:
            print(f"      ❌ Vector Search Error ({comp_name}): {e}")
            results[comp_name] = []

    return results


# ============================================================
# FASE 6: Summary Generator (Task 10)
# ============================================================

SUMMARY_SYSTEM_PROMPT = """You are a concise academic similarity analyst. Given two text excerpts from different research papers representing the same academic component (e.g., both are 'methodology' sections), explain in exactly 1-2 sentences WHY they are similar.

Rules:
- Be specific: mention shared concepts, techniques, or themes.
- Be concise: maximum 2 sentences.
- Write in English.
- Focus on substantive academic similarity, not superficial word overlap.
- Do NOT start with "Both papers" — vary your phrasing."""


def generate_similarity_summary(
    comp_name: str,
    source_text: str,
    neighbor_text: str,
) -> str | None:
    """
    Mengirim sepasang teks komponen dari 2 jurnal ke Gemini untuk
    mendapatkan reasoning 1-2 kalimat mengapa mereka mirip.

    Args:
        comp_name: Nama komponen (background, methodology, dll).
        source_text: Teks komponen dari jurnal sumber.
        neighbor_text: Teks komponen dari jurnal tetangga.

    Returns:
        String reasoning (1-2 kalimat) atau None jika gagal.
    """
    client = genai.Client(api_key=GEMINI_API_KEY)

    user_prompt = f"""Compare these two '{comp_name}' excerpts from different papers and explain their similarity:

--- Paper A ({comp_name}) ---
{source_text}

--- Paper B ({comp_name}) ---
{neighbor_text}

Why are they similar?"""

    try:
        response = client.models.generate_content(
            model=GEMINI_MODEL,
            contents=user_prompt,
            config=types.GenerateContentConfig(
                system_instruction=SUMMARY_SYSTEM_PROMPT,
                temperature=0.3,
                max_output_tokens=200,
            ),
        )
        summary = response.text.strip()
        return summary
    except Exception as e:
        print(f"      ❌ Summary Generator Error: {e}")
        return None


# ============================================================
# FASE 7: MERGE Edge (Task 11)
# ============================================================

# Mapping nama komponen ke tipe relasi Neo4j
EDGE_TYPE_MAP = {
    "background":  "SIMILAR_BACKGROUND",
    "methodology": "SIMILAR_METHODOLOGY",
    "gap":         "SIMILAR_GAP",
    "objective":   "SIMILAR_OBJECTIVE",
    "futurework":  "SIMILAR_FUTUREWORK",
}


def merge_similarity_edges(
    driver,
    source_article_id: str,
    search_results: dict[str, list[dict]],
) -> dict[str, int]:
    """
    Membuat (MERGE) relasi kemiripan antar node :Literature di Neo4j.
    Setiap komponen mendapat tipe edge sendiri, misalnya:
      (a)-[:SIMILAR_METHODOLOGY {score: 0.77, summary: "..."}]->(b)

    Menggunakan MERGE agar tidak ada duplikasi edge jika pipeline di-run ulang.

    Args:
        driver: Neo4j driver instance.
        source_article_id: ID node sumber.
        search_results: Dict {komponen: [{article_id, score, summary, ...}]}.

    Returns:
        Dict {komponen: jumlah_edge_dibuat}.
    """
    edge_counts = {}

    for comp_name, matches in search_results.items():
        edge_type = EDGE_TYPE_MAP[comp_name]
        count = 0

        for m in matches:
            target_id = m["article_id"]
            score = m["score"]
            reasoning = m.get("reasoning", m.get("summary", ""))

            # Cypher: MERGE edge dengan tipe dinamis menggunakan APOC-free approach
            # Karena Neo4j tidak mendukung parameter sebagai tipe relasi,
            # kita gunakan f-string untuk tipe (aman karena nilainya dari konstanta kita)
            cypher = f"""
            MATCH (a:Literature {{article_id: $source_id}})
            MATCH (b:Literature {{article_id: $target_id}})
            MERGE (a)-[r:{edge_type}]->(b)
            SET r.score   = $score,
                r.reasoning = $reasoning
            RETURN type(r) AS rel_type
            """

            params = {
                "source_id": source_article_id,
                "target_id": target_id,
                "score":     score,
                "reasoning": reasoning,
            }

            try:
                with driver.session() as session:
                    result = session.run(cypher, params)
                    record = result.single()
                    if record:
                        count += 1
            except Exception as e:
                print(f"      ❌ Edge Error ({comp_name}): {e}")

        edge_counts[comp_name] = count

    return edge_counts


# ============================================================
# MAIN
# ============================================================

def main():
    print("=" * 60)
    print("  PoC Task 2 - embed_and_ingest.py")
    print("  Fase Aktif: FULL PIPELINE (Task 05-11)")
    print("=" * 60)

    # Validasi credentials
    if not GEMINI_API_KEY:
        print("\n❌ ERROR: GEMINI_API_KEY belum terisi di file .env")
        sys.exit(1)
    if not all([NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD]):
        print("\n❌ ERROR: Kredensial Neo4j belum lengkap di file .env")
        sys.exit(1)

    # Setup Neo4j driver
    driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))
    try:
        driver.verify_connectivity()
        print("\n🔌 Koneksi ke Neo4j AuraDB: BERHASIL")
    except Exception as e:
        print(f"\n❌ Gagal koneksi ke Neo4j: {e}")
        sys.exit(1)

    # Temukan semua PDF
    pdf_files = discover_pdfs(LITERATURE_DIR)

    if not pdf_files:
        print(f"\n⚠️  Tidak ada file PDF ditemukan di: {LITERATURE_DIR}")
        print("   Silakan taruh file .pdf ke folder Literature/ lalu jalankan ulang.")
        return

    print(f"\n📁 Ditemukan {len(pdf_files)} file PDF di folder Literature/:\n")

    for idx, pdf_path in enumerate(pdf_files, start=1):
        filename = os.path.basename(pdf_path)
        filesize_kb = os.path.getsize(pdf_path) / 1024

        print(f"{'─' * 60}")
        print(f"📄 [{idx}] {filename} ({filesize_kb:.1f} KB)")
        print(f"{'─' * 60}")

        # === FASE 1: Parsing PDF ===
        print("   🔄 Fase 1: Membaca teks dari PDF...")
        raw_text = extract_text_from_pdf(pdf_path)
        word_count = len(raw_text.split())
        print(f"   ✅ Teks diekstrak: {word_count} kata")

        # === FASE 2: Gemini Extraction ===
        print(f"   🔄 Fase 2: Mengirim ke Gemini ({GEMINI_MODEL}) untuk ekstraksi 5 komponen...")
        components = extract_components_with_gemini(raw_text)

        if components is None:
            print("   ❌ Gagal mengekstrak komponen. Melompat ke PDF berikutnya.\n")
            continue

        # Tampilkan hasil JSON
        components_dict = components.model_dump()
        print(f"   ✅ Ekstraksi berhasil! Hasil JSON:\n")
        formatted_json = json.dumps(components_dict, indent=2, ensure_ascii=False)
        for line in formatted_json.split("\n"):
            print(f"      {line}")

        # Validasi 5 key tersedia
        expected_keys = {"background", "methodology", "gap", "objective", "futurework"}
        actual_keys = set(components_dict.keys())
        if actual_keys == expected_keys:
            print(f"\n   ✅ Validasi key: 5/5 komponen terkonfirmasi → PASS")
        else:
            missing = expected_keys - actual_keys
            print(f"\n   ⚠️  Key tidak lengkap, hilang: {missing}")

        # === FASE 3: Gemini Embedding ===
        print(f"   🔄 Fase 3: Mengirim 5 komponen ke Gemini ({EMBEDDING_MODEL}) untuk vektorisasi...")
        embeddings = embed_components(components)

        if embeddings is None:
            print("   ❌ Gagal membuat embedding. Melompat ke PDF berikutnya.\n")
            continue

        # Validasi dimensi vektor
        all_valid = True
        for comp_name, vector in embeddings.items():
            dim = len(vector)
            status = "✅" if dim == EMBEDDING_DIMENSIONS else "❌"
            if dim != EMBEDDING_DIMENSIONS:
                all_valid = False
            print(f"      {status} embedding_{comp_name}: len={dim} (expected {EMBEDDING_DIMENSIONS})")

        if all_valid:
            print(f"\n   ✅ Semua 5 vektor terkonfirmasi {EMBEDDING_DIMENSIONS}D → PASS")
        else:
            print(f"\n   ⚠️  Ada vektor dengan dimensi tidak sesuai!")

        # Preview: tampilkan 5 angka pertama dari tiap vektor
        print(f"\n   📊 Preview vektor (5 angka pertama per komponen):")
        for comp_name, vector in embeddings.items():
            preview = [f"{v:.6f}" for v in vector[:5]]
            print(f"      {comp_name}: [{', '.join(preview)}, ...]")

        # === FASE 4: Injeksi Node ke Neo4j ===
        article_id = generate_article_id(filename)
        title = os.path.splitext(filename)[0]  # Nama file tanpa .pdf
        print(f"\n   🔄 Fase 4: MERGE Node ke Neo4j (id={article_id[:12]}...)")
        success = ingest_node_to_neo4j(driver, article_id, title, components, embeddings)

        if success:
            print(f"   ✅ Node berhasil di-MERGE ke Neo4j → PASS")
        else:
            print(f"   ❌ Gagal meng-MERGE node ke Neo4j")
            print()
            continue

        # === FASE 5: Vector Search KNN ===
        print(f"\n   🔄 Fase 5: Vector Search KNN (threshold={SIMILARITY_THRESHOLD}, top_k={TOP_K})...")
        search_results = search_similar_nodes(driver, article_id, embeddings)

        found_any = False
        for comp_name, matches in search_results.items():
            if matches:
                found_any = True
                for m in matches:
                    print(f"      🔗 [{comp_name.upper()}] Ditemukan node setara ID:{m['article_id'][:12]}... "
                          f"Cosine Similarity: {m['score']:.4f}")
                    print(f"         ↳ \"{m['title'][:60]}...\"")
            else:
                print(f"      ⬜ [{comp_name.upper()}] Tidak ada tetangga di atas threshold {SIMILARITY_THRESHOLD}")

        if found_any:
            print(f"\n   ✅ Vector Search KNN berhasil menemukan kecocokan → PASS")
        else:
            print(f"\n   ℹ️  Belum ada tetangga yang cukup mirip (mungkin node pertama di database)")

        # === FASE 6: Summary Generator ===
        if found_any:
            print(f"\n   🔄 Fase 6: Generating similarity summaries via Gemini...")
            components_dict = components.model_dump()
            for comp_name, matches in search_results.items():
                for m in matches:
                    source_text = components_dict[comp_name]
                    neighbor_text = m.get("neighbor_text", "")
                    if not neighbor_text:
                        print(f"      ⚠️  [{comp_name.upper()}] Teks tetangga kosong, skip reasoning.")
                        continue

                    reasoning = generate_similarity_summary(comp_name, source_text, neighbor_text)
                    if reasoning:
                        m["reasoning"] = reasoning
                        print(f"      📝 [{comp_name.upper()}] → \"{reasoning}\"")
                    else:
                        m["reasoning"] = ""
                        print(f"      ❌ [{comp_name.upper()}] Gagal membuat reasoning")
                    # Rate limit guard antara Gemini calls
                    time.sleep(3)

            print(f"\n   ✅ Reasoning generation selesai → PASS")

        # === FASE 7: MERGE Edge ===
        if found_any:
            print(f"\n   🔄 Fase 7: MERGE Edge relasi kemiripan ke Neo4j...")
            edge_counts = merge_similarity_edges(driver, article_id, search_results)

            for comp_name, count in edge_counts.items():
                edge_type = EDGE_TYPE_MAP[comp_name]
                if count > 0:
                    print(f"      ✅ [:{edge_type}] → {count} edge(s) created/updated")
                else:
                    print(f"      ⬜ [:{edge_type}] → 0 edges (no matches above threshold)")

            total_edges = sum(edge_counts.values())
            print(f"\n   ✅ Total {total_edges} edge(s) di-MERGE → PASS")

        print()

        # Delay antar PDF untuk menghindari rate limit
        if idx < len(pdf_files):
            print(f"   ⏳ Menunggu {API_RETRY_DELAY}s sebelum PDF berikutnya (rate limit guard)...")
            time.sleep(API_RETRY_DELAY)

    # Verifikasi akhir: hitung jumlah node dan edge di Neo4j
    print(f"{'─' * 60}")
    print(f"📋 Verifikasi Akhir:")
    with driver.session() as session:
        result = session.run("MATCH (n:Literature) RETURN count(n) AS total")
        total_nodes = result.single()["total"]
        print(f"   🔷 Total node :Literature di AuraDB = {total_nodes}")

        result = session.run("MATCH ()-[r]->() WHERE type(r) STARTS WITH 'SIMILAR_' RETURN type(r) AS rel, count(r) AS cnt ORDER BY rel")
        records = list(result)
        total_edges = 0
        for rec in records:
            print(f"   🔗 [:{rec['rel']}] = {rec['cnt']} edge(s)")
            total_edges += rec['cnt']
        print(f"   📊 Total edge kemiripan = {total_edges}")
    print(f"{'─' * 60}")

    driver.close()

    print(f"\n{'═' * 60}")
    print(f"✅ Pipeline Fase 1-7 selesai untuk {len(pdf_files)} PDF.")
    print(f"{'═' * 60}")


if __name__ == "__main__":
    main()
