import os
import string
import re
import tempfile
import numpy as np

from dotenv import load_dotenv
from flask import Flask, request, jsonify
from flask_cors import CORS

# OCR dependencies
from pypdf import PdfReader
from pdf2image import convert_from_path
import pytesseract

from pinecone import Pinecone, ServerlessSpec
from pinecone.exceptions import NotFoundException

from sentence_transformers import SentenceTransformer
from groq import Groq



app = Flask(__name__)
CORS(app)


load_dotenv()
PINECONE_API_KEY     = os.getenv("PINECONE_API_KEY")
PINECONE_ENVIRONMENT = os.getenv("PINECONE_ENVIRONMENT")  
GROQ_API_KEY = os.getenv("GROQ_API_KEY")

print("DEBUG — Current working directory:", os.getcwd())
print("DEBUG — raw PINECONE_ENVIRONMENT:", repr(os.getenv("PINECONE_ENVIRONMENT")))
print("DEBUG — raw PINECONE_API_KEY    :", repr(os.getenv("PINECONE_API_KEY")))
print("DEBUG — raw TOGETHER_API_KEY    :", repr(os.getenv("TOGETHER_API_KEY")))

if not PINECONE_API_KEY or not PINECONE_ENVIRONMENT:
    raise RuntimeError("Pastikan variabel PINECONE_API_KEY dan PINECONE_ENVIRONMENT sudah di‐set di .env")


parts = PINECONE_ENVIRONMENT.rsplit("-", 1)
if len(parts) != 2:
    raise RuntimeError("Format PINECONE_ENVIRONMENT harus '<region>-<cloud>'. Misal: 'us-west1-gcp'")
PINECONE_REGION = parts[0]  
PINECONE_CLOUD  = parts[1]  

# Buat instance client Pinecone
pc = Pinecone(api_key=PINECONE_API_KEY)

# Nama index yang akan dipakai
index_name = "techbot"

# Jika index belum ada, buat satu kali dengan spec serverless
all_index_names = pc.list_indexes().names()
if index_name not in all_index_names:
    pc.create_index(
        name=index_name,
        dimension=384,
        metric="cosine",
        spec=ServerlessSpec(
            cloud=PINECONE_CLOUD,       
            region=PINECONE_REGION      
        )
    )

index = pc.Index(index_name)

# Inisialisasi model embedding & Together AI

embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
client = Groq(api_key=GROQ_API_KEY)

# Daftar stopwords (untuk preprocessing teks)
stop_words = set([
    "adalah","agar","akan","aku","antara","adapun","atau","awal","bahwa","bagi","bahkan",
    "bukan","bukanlah","dari","dan","dengan","di","untuk","pada","oleh","sebelum","saja",
    "selama","seperti","semua","sendiri","seolah","sehingga","sesuai","seperti","terhadap",
    "tersebut","terutama","dalam","di","ini","itu","yang","yangmana","jika","ke","lebih","melalui"
])

def preprocess_text(text: str) -> list[str]:
    """
    Pra-proses teks:
    - Segmentasi topik: definisi, aturan, prosedur, regulasi, tips, pendaftaran
    - Normalisasi format numerik: Rp/$ → angka
    - Highlight keyword penting
    - Hapus simbol/noise
    - Cocok untuk berbagai domain: akademis, administratif, teknis, dan regulasi
    """
    # Segmentasi multi-pattern - General document sections - General document sections
    segments = re.split(
        r'\n\d+\.\s|'                          # 1. 2. 3.
        r'\n[A-Da-d]\.\s|'                     # A. B.
        r'\n(?:Bab|Chapter|Section) .*?|'     # Bab/Chapter
        r'(?=^Pengertian|^Definisi|^Deskripsi|^Hukum|^Aturan|^Prinsip|^Tips|^Cara|^Prosedur|^Pemeriksaan|^Verifikasi|^Tahap|^Undang-Undang|^Peraturan|^Kebijakan|^Larangan|^Persyaratan|^Izin|^Ketentuan|^Pendaftaran|^Pembayaran|^Jadwal|^Syarat|^Manfaat|^Risiko|^Solusi|^Alternatif)', 
        text,
        flags=re.MULTILINE
    )

    cleaned_chunks = []

    for segment in segments:
        if not segment.strip():
            continue

        # Normalisasi format numerik (Rp, $, dll)
        segment = re.sub(r'(?:Rp\.?|Rp\s|[$€£¥]|USD|IDR)?\s?([\d.]+)', lambda m: str(int(m.group(1).replace('.', ''))), segment)

        # Highlight keyword penting - General domain keywords
        keywords = [
            # Umum
            'syarat', 'pengertian', 'definisi', 'hukum', 'aturan', 'prinsip', 'tujuan',
            'prosedur', 'tahap', 'langkah', 'cara', 'tips', 'saran', 'rekomendasi',
            
            # Administratif & legal
            'peraturan', 'undang-undang', 'izin', 'kebijakan', 'sertifikasi', 'akreditasi',
            'dokumen', 'formulir', 'aplikasi', 'verifikasi', 'validasi', 'persetujuan',
            'dilarang', 'wajib', 'disarankan', 'opsional', 'persyaratan', 'ketentuan',
            
            # Pendaftaran & akademis
            'pendaftaran', 'enrollment', 'program', 'kursus', 'modul', 'penilaian',
            'sertifikat', 'kompetensi', 'hasil', 'output', 'capaian', 'grade',
            
            # Pembayaran & keuangan
            'harga', 'biaya', 'tarif', 'bayar', 'pembayaran', 'invoice', 'transaksi',
            'diskon', 'cicilan', 'promo', 'paket', 'kuota', 'alokasi',
            
            # Prosedural & teknis
            'diagnosis', 'pemeriksaan', 'check', 'verifikasi', 'validasi', 'review',
            'analisis', 'hasil', 'laporan', 'dokumentasi', 'catatan', 'log',
            
            # Kesehatan & keselamatan (general)
            'kesehatan', 'keselamatan', 'risiko', 'manfaat', 'efek', 'dampak',
            'pencegahan', 'penanganan', 'emergency', 'darurat', 'solusi',
            
            # Informasi kontak & tempat
            'kontak', 'hubungi', 'kantor', 'lokasi', 'alamat', 'telepon', 'email',
            'website', 'jam operasional', 'waktu', 'jam buka'
        ]
        for kw in keywords:
            segment = re.sub(fr'\b({kw})\b', lambda m: m.group(1).upper(), segment, flags=re.IGNORECASE)

        # Bersihkan simbol noise
        segment = re.sub(r'[\*\-•]', '', segment)
        segment = re.sub(r'\s+', ' ', segment).strip()

        cleaned_chunks.append(segment)

    return cleaned_chunks

#normalisasi query
def clean_text(text: str) -> str:
    """
    Lowercase, hilangkan tanda baca, angka, dan stopwords Indonesian.
    """
    text = text.lower()
    # hapus tanda baca
    text = text.translate(str.maketrans("", "", string.punctuation))
    # hapus angka
    text = re.sub(r"\d+", "", text)
    # hapus stopwords
    words = [w for w in text.split() if w not in stop_words]
    return " ".join(words)

def chunk_text(text: str, chunk_size: int = 512) -> list[str]:
    """
    Memecah teks panjang menjadi beberapa chunk per `chunk_size` kata.
    """
    words = text.split()
    return [
        " ".join(words[i : i + chunk_size])
        for i in range(0, len(words), chunk_size)
    ]



def extract_text_from_pdf(pdf_path: str) -> str:
    """Ambil teks dari PDF: parsing text + OCR fallback."""
    # 1) Coba baca teks langsung dari PDF
    text = ""
    try:
        reader = PdfReader(pdf_path)
        for page in reader.pages:
            page_text = page.extract_text() or ""
            text += page_text + "\n"
    except Exception as e:
        # Jika gagal baca PDF native, lanjut ke OCR
        text = ""

    text = text.strip()
    if text:
        return text

    # 2) OCR fallback (untuk scanned PDF)
    try:
        images = convert_from_path(pdf_path, dpi=300)
        ocr_text = []
        for img in images:
            page_text = pytesseract.image_to_string(img, lang='eng+ind')
            ocr_text.append(page_text)
        return "\n".join(ocr_text).strip()
    except Exception as e:
        raise RuntimeError(f"OCR gagal: {e}")


def upsert_document_to_pinecone(doc_id: int, content: str):
    """
    Preprocess `content`, lalu upsert ke Pinecone sebagai vector embedding.
    """
    #Proses teks dengan preprocess_text untuk pembersihan
    processed_content = preprocess_text(content)  # Menggunakan fungsi preprocessing untuk membersihkan teks

    #Membagi teks yang sudah diproses menjadi chunk lebih kecil
    for processed_chunk in processed_content:
        # Memecah chunk lebih besar ke dalam ukuran chunk yang lebih kecil
        chunks = chunk_text(processed_chunk)

        # Untuk setiap chunk, buat embedding dan lakukan upsert ke Pinecone
        for i, chunk in enumerate(chunks):
            emb = embedding_model.encode(chunk).tolist()
            index.upsert([  
                (f"{doc_id}_chunk_{i}", emb, {"text": chunk})
            ])

# Endpoint /upsert  → menerima JSON {doc_id, content}

@app.route("/upsert", methods=["POST"])
def route_upsert():
    data = request.get_json(force=True)
    doc_id  = data.get("doc_id")
    content = data.get("content")
    if doc_id is None or content is None:
        return jsonify({"error": "doc_id dan content wajib diberikan"}), 400

    upsert_document_to_pinecone(doc_id, content)
    return jsonify({"message": "Document upserted successfully!"})

#  Endpoint /reset-index  → hanya clear semua vectors

@app.route("/extract-pdf", methods=["POST"])
def route_extract_pdf():
    if 'pdf_file' not in request.files:
        return jsonify({"error": "pdf_file field is wajib"}), 400

    file = request.files['pdf_file']
    if file.filename == '':
        return jsonify({"error": "Nama file kosong"}), 400

    with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
        file.save(tmp.name)
        tmp_path = tmp.name

    try:
        text = extract_text_from_pdf(tmp_path)
    except Exception as e:
        return jsonify({"error": str(e)}), 500
    finally:
        try:
            os.remove(tmp_path)
        except OSError:
            pass

    return jsonify({"text": text})


@app.route("/reset-index", methods=["POST"])
def route_reset_index():
    try:
        index.delete(delete_all=True)
    except NotFoundException:
        # Jika namespace tidak ada (index kosong), abaikan
        pass

    return jsonify({"message": "Index cleared (all vectors deleted)!"})


def cosine_similarity(vec1, vec2):
    vec1 = np.array(vec1)
    vec2 = np.array(vec2)
    return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))


# def retrieve_relevant_chunk(question: str):
#     clean_q = clean_text(question)
#     q_emb = embedding_model.encode(clean_q).tolist()

#     res = index.query(
#         vector=q_emb,
#         top_k=1,
#         include_metadata=True,
#         include_values=True  # ⬅️ Tambahkan ini untuk ambil vektor dokumen
#     )

#     if res.matches:
#         match = res.matches[0]
#         doc_text = match.metadata.get("text", "")
#         doc_emb = match.values

#         # Hitung cosine similarity
#         cos_sim = cosine_similarity(q_emb, doc_emb)

#         # Cetak hasil
#         print("==== HASIL SIMILARITY ====")
#         print("Pertanyaan:", question)
#         print("VEKTOR PERTANYAAN:", q_emb)
#         print("VEKTOR DOKUMEN   :", doc_emb)
#         print("COSINE SIMILARITY:", cos_sim)
#         print("===========================\n")

#         return doc_text
#     else:
#         print("❌ Tidak ada hasil dari Pinecone.")
#         return ""
def retrieve_relevant_chunk(question: str):
    # Preprocessing query
    clean_q = clean_text(question)
    q_emb = embedding_model.encode(clean_q).tolist()

    # Query ke Pinecone untuk mencari dokumen yang relevan
    res = index.query(
        vector=q_emb,
        top_k=1,  # Ambil 1 hasil teratas
        include_metadata=True,
        include_values=True  # Mengambil vektor dokumen untuk perhitungan similarity
    )

    # Post-retrieval processing
    if res.matches:
        match = res.matches[0]  
        doc_text = match.metadata.get("text", "")
        doc_emb = match.values

        # Hitung cosine similarity antara query dan dokumen yang ditemukan
        cos_sim = cosine_similarity(q_emb, doc_emb)

        print("==== HASIL SIMILARITY ====")
        print("Pertanyaan:", question)
        print("VEKTOR PERTANYAAN:", q_emb)
        print("VEKTOR DOKUMEN   :", doc_emb)
        print("COSINE SIMILARITY:", cos_sim)
        print("===========================\n")

        # Post-processing: Jika cosine similarity cukup tinggi, gunakan dokumen tersebut
        if cos_sim > 0.5:  # Threshold relevansi
            return doc_text
        else:
            # Jika cosine similarity rendah, beri pesan default
            return "Maaf, saya tidak dapat menemukan informasi yang sangat relevan berdasarkan pertanyaan Anda. Apakah Anda ingin bertanya tentang hal lain?"
    else:
        # Jika tidak ada hasil dari Pinecone
        return "Maaf, saya tidak menemukan informasi yang relevan. Coba ajukan pertanyaan lain atau beri detail lebih lanjut."


def get_chatbot_response(question: str) -> str:
    relevant = retrieve_relevant_chunk(question)
    prompt = f"""
    Kamu adalah asisten informasi Pendaftaran Mahasiswa Baru (PMB)
    Fakultas Teknik Universitas Suryakancana Cianjur.

    Tugas kamu adalah membantu calon mahasiswa baru dengan memberikan
    informasi seputar PMB Fakultas Teknik UNSUR secara jelas, akurat,
    dan mudah dipahami.

    Aturan utama:
    1. Jika ditanya siapa kamu, jawab:
    "Saya CAKRA, asisten informasi Pendaftaran Mahasiswa Baru Fakultas Teknik Universitas Suryakancana Cianjur."
    2. Gunakan bahasa Indonesia yang sopan, ramah, dan profesional
    3. Fokus hanya pada informasi PMB Fakultas Teknik Universitas Suryakancana Cianjur
    4. Jawab berdasarkan informasi dari dokumen atau konteks yang tersedia
    5. Jika informasi tidak tersedia, sampaikan dengan jujur dan arahkan ke panitia PMB
    6. Jangan mengarang informasi
    7. Jangan membahas fakultas lain kecuali diminta secara umum

    Gaya komunikasi:
    - Ramah seperti staf akademik
    - Tidak kaku
    - Tidak menggunakan bahasa gaul berlebihan
    - Jawaban singkat, jelas, dan langsung ke poin

    {relevant}

    Pertanyaan: {question}
    Jawaban:
    """
    prompt += "\nJawaban harus sederhana, tanpa penggunaan penomoran atau bold."
    response = client.chat.completions.create(
        model="llama-3.3-70b-versatile",
        messages=[
            {
                "role": "system",
                "content": prompt
            }
        ],
        temperature=0.3,
        max_tokens=512
    )
    return post_process_answer(response.choices[0].message.content)

def post_process_answer(answer: str) -> str:
    if len(answer) > 500:
        answer = answer[:500] + "..."
    answer += "\n\nJika Anda memerlukan informasi lebih lanjut, silakan tanyakan lagi."
    return answer

@app.route("/query", methods=["POST"]) 
def route_query():
    data     = request.get_json(force=True)
    question = data.get("question")
    if not question:
        return jsonify({"error": "question wajib diberikan"}), 400

    answer = get_chatbot_response(question)
    return jsonify({"answer": answer})


# Jalankan Flask

if __name__ == "__main__":
    app.run(
        host="0.0.0.0",
        port=5000,
        debug=True,
        use_reloader=False
    )