"""
berita_acara_service.py — Service layer untuk pemrosesan Berita Acara Sidang via LLM.
"""

import json
import logging
import time
import os
import re
from typing import Optional

from sqlalchemy.orm import Session

from app.models.berita_acara import BeritaAcara, BeritaAcaraPrompt
from app.models.transkripsi import TranskripsiTask
from app.utils.crypto_utils import decrypt_text, encrypt_text

logger = logging.getLogger(__name__)

# ── Prompt default (ringkas, hemat token, output konsisten) ───────────────────
# Target: < 200 token. Estimasi prompt ini ~115 token.
# Placeholder {transcript} WAJIB ada — akan diganti teks transkripsi saat runtime.
DEFAULT_PROMPT = """
Tugas: membaca dengan seksama seluruh teks terlebih dahulu dan menyusun untuk berita acara sidang agenda pemeriksaan saksi di persidangan dalam bentuk pertanyaan dan jawaban


OUTPUT:
JSON array VALID saja. Tidak ada teks lain.
Format:
[{"id":1,"question":"...","answer":"..."}]

ATURAN:
1. baca tiap kata dan kalimat dalam teks hasil transkripsi
2. tidak menghilangkan, mengubah mengurangi kata apapun agar fakta persidangan tidak hilang.
3. jawaban diparafrase dan normalisasi dengan tidak mengurangi dan mengubah kata apapun
4. jika ada kata tidak sesuai KBBI atau istilah asing maka buat perbaikan kata dalam tanda kurung ().

TRANSKRIPSI:
{transcript}
"""
# System message singkat — cukup satu kalimat eksplisit
_SYSTEM_MESSAGE = "Output: valid JSON array only. No prose, no markdown, no explanation."


# ── Prompt helpers ────────────────────────────────────────────────────────────

def get_active_prompt(db: Session) -> str:
    """Ambil prompt aktif dari DB. Fallback ke DEFAULT_PROMPT jika belum ada."""
    prompt_row = (
        db.query(BeritaAcaraPrompt)
        .filter(BeritaAcaraPrompt.is_active == True)
        .order_by(BeritaAcaraPrompt.updated_at.desc())
        .first()
    )
    return prompt_row.prompt_text if prompt_row and prompt_row.prompt_text else DEFAULT_PROMPT


def upsert_prompt(db: Session, prompt_text: str, admin_id: int) -> BeritaAcaraPrompt:
    """Simpan atau perbarui prompt di DB (hanya satu baris aktif bernama 'default')."""
    existing = db.query(BeritaAcaraPrompt).filter(BeritaAcaraPrompt.name == "default").first()
    if existing:
        existing.prompt_text = prompt_text
        existing.is_active   = True
        existing.updated_by  = admin_id
        db.commit()
        db.refresh(existing)
        return existing

    new_prompt = BeritaAcaraPrompt(
        name        = "default",
        prompt_text = prompt_text,
        is_active   = True,
        updated_by  = admin_id,
    )
    db.add(new_prompt)
    db.commit()
    db.refresh(new_prompt)
    return new_prompt


# ── Transcript helper ─────────────────────────────────────────────────────────

def get_transcript_text(task: TranskripsiTask) -> Optional[str]:
    """Dekripsi dan kembalikan teks transkripsi dari task."""
    if not task.result:
        return None
    return decrypt_text(task.result)


# ── Proses utama ──────────────────────────────────────────────────────────────

def process_berita_acara(ba_id: int) -> None:
    """
    Entry point background job: ambil transkrip → kirim ke LLM → simpan hasil.
    Dipanggil dari background task FastAPI via run_in_threadpool.
    """
    from app.core.database import get_db_context

    # ── Tahap 1: validasi & ambil data ──────────────────────────────────────
    with get_db_context() as db:
        ba = db.query(BeritaAcara).filter(BeritaAcara.id == ba_id).first()
        if not ba:
            logger.warning("[BA] BeritaAcara ID %d tidak ditemukan.", ba_id)
            return

        task = db.query(TranskripsiTask).filter(TranskripsiTask.id == ba.task_id).first()
        if not task or task.status != "completed":
            _set_ba_failed(db, ba, "Task transkripsi belum selesai atau tidak ditemukan.")
            return

        transcript = get_transcript_text(task)
        if not transcript or not transcript.strip():
            _set_ba_failed(db, ba, "Teks transkripsi kosong, tidak bisa diproses.")
            return

        # Gunakan prompt snapshot yang tersimpan bersama BA, atau ambil prompt aktif saat ini
        prompt_template = ba.prompt_used or get_active_prompt(db)
        ba.status = "processing"
        db.commit()

    # ── Tahap 2: panggil LLM ────────────────────────────────────────────────
    try:
        qa_list, model_used, elapsed = _call_llm(transcript, prompt_template)
    except Exception as exc:
        logger.error("[BA] LLM error untuk BA ID %d: %s", ba_id, exc, exc_info=True)
        with get_db_context() as db:
            ba = db.query(BeritaAcara).filter(BeritaAcara.id == ba_id).first()
            if ba:
                _set_ba_failed(db, ba, f"LLM API error: {str(exc)[:300]}")
        return

    # ── Tahap 3: simpan hasil ────────────────────────────────────────────────
    with get_db_context() as db:
        ba = db.query(BeritaAcara).filter(BeritaAcara.id == ba_id).first()
        if not ba:
            return

        encrypted            = encrypt_text(json.dumps(qa_list, ensure_ascii=False))
        ba.qa_json_encrypted = encrypted
        ba.status            = "completed"
        ba.model_used        = model_used
        ba.processing_time   = str(round(elapsed, 2))
        ba.error_message     = None
        db.commit()

    logger.info("[BA] BeritaAcara ID %d selesai (%d Q&A, %.2fs).", ba_id, len(qa_list), elapsed)


# ── JSON repair utilities ─────────────────────────────────────────────────────

def _fix_truncated_json_advanced(raw: str) -> str:
    """
    Coba perbaiki JSON yang terpotong / tidak lengkap.
    Tiga teknik diterapkan berurutan; jika salah satu berhasil langsung dikembalikan.
    """
    raw = raw.strip()

    # Sudah valid → kembalikan langsung
    try:
        json.loads(raw)
        return raw
    except Exception:
        pass

    # Teknik 1: tambahkan kurung penutup yang hilang
    open_brackets  = raw.count('[')
    close_brackets = raw.count(']')
    open_braces    = raw.count('{')
    close_braces   = raw.count('}')

    candidate = raw
    if open_braces > close_braces:
        candidate += '}' * (open_braces - close_braces)
    if open_brackets > close_brackets:
        candidate += ']' * (open_brackets - close_brackets)

    try:
        json.loads(candidate)
        return candidate
    except Exception:
        pass

    # Teknik 2: ekstrak objek utuh via regex
    pattern = (
        r'\{\s*"id"\s*:\s*\d+\s*,\s*'
        r'"question"\s*:\s*"(?:[^"\\]|\\.)*"\s*,\s*'
        r'"answer"\s*:\s*"(?:[^"\\]|\\.)*"\s*\}'
    )
    matches = re.findall(pattern, raw, re.DOTALL)
    if matches:
        candidate = '[' + ','.join(matches) + ']'
        try:
            json.loads(candidate)
            return candidate
        except Exception:
            pass

    # Teknik 3: potong di koma terakhir lalu tutup array
    last_comma = raw.rfind(',')
    if last_comma != -1:
        candidate = raw[:last_comma] + ']'
        try:
            json.loads(candidate)
            return candidate
        except Exception:
            pass

    return raw


def _extract_qa_manually(raw: str) -> list:
    """
    Fallback terakhir: ekstrak pasangan tanya-jawab secara manual via regex
    ketika semua teknik perbaikan JSON gagal.
    """
    pattern_q = r'"question"\s*:\s*"((?:[^"\\]|\\.)*)"'
    pattern_a = r'"answer"\s*:\s*"((?:[^"\\]|\\.)*)"'

    questions = re.findall(pattern_q, raw, re.DOTALL)
    answers   = re.findall(pattern_a, raw, re.DOTALL)

    qa_pairs = []
    for i, (q, a) in enumerate(zip(questions, answers), start=1):
        q = q.replace('\\"', '"').replace('\\n', '\n')
        a = a.replace('\\"', '"').replace('\\n', '\n')
        qa_pairs.append({"id": i, "question": q, "answer": a})
    return qa_pairs


# ── LLM caller ───────────────────────────────────────────────────────────────

def _call_llm(transcript: str, prompt_template: str) -> tuple[list, str, float]:
    """
    Kirim transkrip ke LLM yang dikonfigurasi via env vars.
    Mendukung tiga provider: openai (default), openrouter, ollama.

    Returns
    -------
    tuple[list, str, float]
        qa_list      — daftar dict {id, question, answer}
        model_name   — label model yang digunakan
        elapsed      — waktu proses dalam detik
    """
    from openai import OpenAI

    provider       = os.getenv("LLM_PROVIDER", "openai").lower()
    openai_key     = os.getenv("OPENAI_API_KEY", "")
    openrouter_key = os.getenv("OPENROUTER_API_KEY", "")

    # ── Konfigurasi per-provider ─────────────────────────────────────────────
    if provider == "openrouter":
        if not openrouter_key:
            raise ValueError("OPENROUTER_API_KEY belum diset.")
        client = OpenAI(
            base_url   = "https://api.koboillm.com/v1",
            api_key    = openrouter_key,
            timeout    = 120.0,
            max_retries= 2,
        )
        model          = os.getenv("OPENROUTER_MODEL", "meta-llama/llama-3.2-3b-instruct:free")
        model_name     = f"OpenRouter/{model}"
        max_tokens_val = 8192

    elif provider == "ollama":
        base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
        client = OpenAI(
            base_url   = base_url,
            api_key    = "ollama",
            timeout    = 120.0,
            max_retries= 2,
        )
        model          = os.getenv("OLLAMA_MODEL", "gemma2:2b")
        model_name     = f"Ollama/{model}"
        max_tokens_val = 8192

    else:  # openai (default)
        if not openai_key:
            raise ValueError("OPENAI_API_KEY belum diset.")
        client = OpenAI(api_key=openai_key, timeout=120.0)
        model          = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
        model_name     = model
        max_tokens_val = 4096

    # ── Susun prompt ─────────────────────────────────────────────────────────
    # Batasi transkrip agar tidak meledak konteks (±80 000 karakter ≈ 20 000 token)
    transcript_trimmed = transcript[:80_000]
    final_prompt       = prompt_template.replace("{transcript}", transcript_trimmed)

    # ── Panggil API ──────────────────────────────────────────────────────────
    start = time.perf_counter()
    response = client.chat.completions.create(
        model    = model,
        messages = [
            {"role": "system", "content": _SYSTEM_MESSAGE},
            {"role": "user",   "content": final_prompt},
        ],
        temperature = 0.1,   # lebih deterministik → JSON lebih konsisten
        max_tokens  = max_tokens_val,
    )
    elapsed = time.perf_counter() - start

    raw_content = (response.choices[0].message.content or "").strip()

    # ── Bersihkan markdown fence jika model tetap menambahkannya ────────────
    if raw_content.startswith("```"):
        raw_content = re.sub(r'^```[a-zA-Z]*\n?', '', raw_content)
        raw_content = re.sub(r'\n?```$',          '', raw_content)
        raw_content = raw_content.strip()

    # ── Parse JSON ───────────────────────────────────────────────────────────
    fixed_json = _fix_truncated_json_advanced(raw_content)
    try:
        qa_list = json.loads(fixed_json)
    except json.JSONDecodeError:
        logger.error(
            "[BA] JSON tidak valid setelah repair. Raw (300 chars): %s",
            raw_content[:300],
        )
        qa_list = _extract_qa_manually(raw_content)
        if not qa_list:
            raise ValueError(f"Gagal memparse JSON dari LLM. Raw: {raw_content[:300]}")

    if not isinstance(qa_list, list):
        raise ValueError("LLM tidak mengembalikan array JSON.")

    # ── Normalisasi & bersihkan tiap item ────────────────────────────────────
    cleaned = []
    for i, item in enumerate(qa_list, start=1):
        if not isinstance(item, dict):
            continue
        cleaned.append({
            "id":       item.get("id", i),
            "question": str(item.get("question", "")).strip(),
            "answer":   str(item.get("answer",   "")).strip(),
        })

    return cleaned, model_name, elapsed


# ── DB helpers ────────────────────────────────────────────────────────────────

def _set_ba_failed(db: Session, ba: BeritaAcara, msg: str) -> None:
    """Set status BA menjadi 'failed' dan catat pesan error."""
    ba.status        = "failed"
    ba.error_message = msg
    db.commit()


def get_ba_qa_decrypted(ba: BeritaAcara) -> list:
    """Dekripsi dan kembalikan daftar Q&A dari BeritaAcara. Kembalikan [] jika kosong/error."""
    if not ba.qa_json_encrypted:
        return []
    try:
        raw     = decrypt_text(ba.qa_json_encrypted)
        parsed  = json.loads(raw)
        return parsed if isinstance(parsed, list) else []
    except Exception:
        return []


def save_qa_edits(db: Session, ba: BeritaAcara, qa_list: list) -> BeritaAcara:
    """Enkripsi dan simpan Q&A yang sudah diedit oleh user."""
    encrypted            = encrypt_text(json.dumps(qa_list, ensure_ascii=False))
    ba.qa_json_encrypted = encrypted
    db.commit()
    db.refresh(ba)
    return ba