#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
recommendation_cbf.py
=====================
Sistem Rekomendasi Produk — Content-Based Filtering (CBF)
Menggunakan TF-IDF + Cosine Similarity

═══════════════════════════════════════════════════════════════
CARA KERJA CBF (disesuaikan dengan data toko pertanian):

Langkah 1 — Baca Atribut Produk
   Setiap produk memiliki atribut:
     - Nama      → "Pupuk Urea 50 Kg"
     - Kategori  → "Pupuk"
     - Deskripsi → "mengandung nitrogen untuk padi, jagung, palawija"

Langkah 2 — Hitung Kemiripan Produk (TF-IDF + Cosine Similarity)
   Sistem menggabungkan atribut setiap produk menjadi satu
   "dokumen teks", lalu mengukur seberapa mirip dua produk
   berdasarkan kata-kata yang sama.

   Contoh dari data toko ini:
     User melihat → "Furadan 3GR" (Pestisida untuk padi)
     Sistem merekomendasikan:
       → "Plantomycin Plus 20WP"  (Pestisida untuk padi)
       → "KIREY PRIMA"            (Herbisida untuk padi)
       → "Pupuk Urea 50 Kg"       (Pupuk untuk padi)
     karena semua produk tersebut berbagi kata "padi"

Langkah 3 — Tampilkan Produk Paling Mirip
   Urutkan berdasarkan skor kemiripan tertinggi.

═══════════════════════════════════════════════════════════════
INPUT  (JSON via stdin dari PHP):
  {
    "target_ids": [8],
    "limit": 4,
    "products": [
      {"id": 1, "name": "Pupuk Urea 50 Kilo Gram",
       "category": "Pupuk",
       "description": "Pupuk Urea berkualitas tinggi...padi, jagung..."},
      ...
    ]
  }

OUTPUT (JSON via stdout ke PHP):
  {
    "recommended_ids": [11, 13, 1, 4],
    "similarities": {"11": 0.72, "13": 0.65, "1": 0.41, "4": 0.38},
    "engine": "sklearn"
  }
═══════════════════════════════════════════════════════════════
"""

import sys
import json
import re
import math


# ─── Cek scikit-learn ─────────────────────────────────────────────────────────
try:
    from sklearn.feature_extraction.text import TfidfVectorizer
    from sklearn.metrics.pairwise import cosine_similarity as _cosine
    import numpy as np
    SKLEARN_AVAILABLE = True
except ImportError:
    SKLEARN_AVAILABLE = False


# ─── Stopwords (kata tidak bermakna yang diabaikan) ───────────────────────────
# Catatan: kata-kata yang RELEVAN untuk produk pertanian seperti
# "padi", "jagung", "hama", "pupuk", "pestisida" TIDAK dimasukkan
# ke stopwords agar tetap digunakan sebagai fitur kemiripan.
STOPWORDS = {
    # Kata umum Bahasa Indonesia
    'dan', 'di', 'ke', 'dari', 'yang', 'untuk', 'dengan', 'atau', 'ini',
    'itu', 'pada', 'adalah', 'oleh', 'dalam', 'juga', 'akan', 'bisa',
    'dapat', 'karena', 'ada', 'kami', 'kita', 'saya', 'anda', 'tidak',
    'sudah', 'telah', 'belum', 'sangat', 'lebih', 'paling', 'hanya',
    'maka', 'lalu', 'serta', 'agar', 'jika', 'namun', 'tetapi', 'saja',
    'bahwa', 'atas', 'sampai', 'antara', 'seperti', 'ketika', 'setelah',
    'sebelum', 'selama', 'setiap', 'semua', 'beberapa', 'banyak',
    # Kata umum Bahasa Inggris
    'the', 'a', 'an', 'is', 'are', 'of', 'in', 'on', 'at', 'to', 'for',
    'and', 'or', 'not', 'with', 'as', 'it', 'by', 'was', 'be', 'has',
    # Kata umum yang tidak membedakan produk
    'kilo', 'gram', 'kg', 'liter', 'ml', 'cc', 'gr',  # satuan
    'berkualitas', 'tinggi', 'mudah', 'efektif', 'optimal', 'kuat',
    'baik', 'bagus', 'cocok', 'dirancang', 'digunakan', 'memiliki',
    'mengandung', 'meningkatkan', 'mencegah', 'mengendalikan',
    'tahan', 'lama', 'ringan', 'besar', 'kecil',
}


# ─── Langkah 1: Baca dan siapkan atribut produk ───────────────────────────────

def ekstrak_fitur(produk: dict) -> str:
    """
    Gabungkan atribut produk menjadi satu teks fitur untuk TF-IDF.

    Strategi pembobotan:
      - Nama produk  → diulang 4x (paling penting, identitas utama produk)
      - Kategori     → diulang 3x (penting: Pupuk, Pestisida, Bibit, dll.)
      - Deskripsi    → diulang 1x (konteks tambahan)

    Mengapa diulang? Pengulangan meningkatkan bobot TF (term frequency)
    sehingga kata dari nama/kategori lebih dominan dalam vektor TF-IDF.

    Contoh hasil:
      Produk: "Furadan 3GR" | Kategori: "Pestisida"
      → "furadan gr furadan gr furadan gr furadan gr pestisida pestisida
         pestisida insektisida nematoda penggerek padi..."
    """
    nama      = str(produk.get('name', ''))
    kategori  = str(produk.get('category', ''))
    deskripsi = str(produk.get('description', ''))

    # Bobot: nama 4x, kategori 3x, deskripsi 1x
    teks = ' '.join([
        (nama + ' ') * 4,
        (kategori + ' ') * 3,
        deskripsi
    ])

    return bersihkan(teks)


def bersihkan(teks: str) -> str:
    """
    Normalisasi teks:
    1. Huruf kecil semua
    2. Hapus angka dan karakter khusus (kecuali spasi)
    3. Hapus stopwords
    4. Hapus kata terlalu pendek (≤ 2 huruf)
    """
    teks = teks.lower()
    teks = re.sub(r'[^a-z\s]', ' ', teks)  # hanya huruf dan spasi
    kata = teks.split()
    kata_valid = [
        k for k in kata
        if k not in STOPWORDS and len(k) > 2
    ]
    return ' '.join(kata_valid)


# ─── Langkah 2A: Hitung kemiripan dengan scikit-learn ─────────────────────────

def rekomendasikan_sklearn(produk_list: list, target_ids: list, limit: int) -> dict:
    """
    TF-IDF + Cosine Similarity menggunakan scikit-learn.

    TF-IDF:
    - TF  = seberapa sering sebuah kata muncul dalam deskripsi satu produk
    - IDF = kebalikan dari seberapa umum kata itu di semua produk
    - Kata unik untuk satu produk → skor TF-IDF tinggi → pembeda kuat

    Cosine Similarity:
    - Hitung sudut antara dua vektor produk
    - Nilai mendekati 1.0 = sangat mirip, mendekati 0.0 = tidak mirip
    """
    daftar_id   = [int(p['id']) for p in produk_list]
    id_ke_idx   = {pid: i for i, pid in enumerate(daftar_id)}

    # Buat corpus teks fitur
    corpus = [ekstrak_fitur(p) for p in produk_list]

    # Buat matriks TF-IDF
    # ngram_range=(1,2): gunakan kata tunggal ("padi") DAN pasangan kata ("pupuk padi")
    # Ini penting agar frasa seperti "tanaman padi" dianggap satu fitur tersendiri
    vectorizer = TfidfVectorizer(
        ngram_range=(1, 2),
        min_df=1,
        sublinear_tf=True,  # log(1+tf) mencegah kata yang terlalu sering mendominasi
        norm='l2'
    )
    matriks = vectorizer.fit_transform(corpus)

    # Vektor profil = rata-rata vektor TF-IDF dari semua produk target
    idx_target = [id_ke_idx[tid] for tid in target_ids if tid in id_ke_idx]
    if not idx_target:
        idx_target = [0]

    profil = np.asarray(matriks[idx_target].mean(axis=0))

    # Hitung cosine similarity profil vs semua produk
    skor = _cosine(profil, matriks).flatten()

    # Kumpulkan hasil, abaikan produk target
    set_target = set(target_ids)
    hasil = [
        (daftar_id[i], float(skor[i]))
        for i in range(len(daftar_id))
        if daftar_id[i] not in set_target
    ]
    hasil.sort(key=lambda x: x[1], reverse=True)

    top = hasil[:limit]
    return {
        "recommended_ids": [pid for pid, _ in top],
        "similarities":    {str(pid): round(s, 4) for pid, s in top},
        "engine":          "sklearn"
    }


# ─── Langkah 2B: Hitung kemiripan secara manual (fallback) ───────────────────

def _tf(tokens: list) -> dict:
    """Term Frequency = jumlah_kemunculan / total_kata"""
    if not tokens:
        return {}
    freq = {}
    for t in tokens:
        freq[t] = freq.get(t, 0) + 1
    n = len(tokens)
    return {k: v / n for k, v in freq.items()}


def _idf(semua_tokens: list) -> dict:
    """
    Smooth IDF = log((N+1)/(df+1)) + 1
    N  = jumlah dokumen (produk)
    df = jumlah dokumen yang mengandung kata tersebut
    """
    N  = len(semua_tokens)
    df = {}
    for tokens in semua_tokens:
        for t in set(tokens):
            df[t] = df.get(t, 0) + 1
    return {t: math.log((N + 1) / (c + 1)) + 1 for t, c in df.items()}


def _tfidf(tokens: list, idf: dict) -> dict:
    tf = _tf(tokens)
    return {t: v * idf.get(t, 1.0) for t, v in tf.items()}


def _cosine_manual(v1: dict, v2: dict) -> float:
    dot  = sum(v1.get(t, 0.0) * v2.get(t, 0.0) for t in v1)
    mag1 = math.sqrt(sum(x * x for x in v1.values()))
    mag2 = math.sqrt(sum(x * x for x in v2.values()))
    denom = mag1 * mag2
    return dot / denom if denom > 0 else 0.0


def rekomendasikan_manual(produk_list: list, target_ids: list, limit: int) -> dict:
    """TF-IDF + Cosine Similarity implementasi manual (tanpa scikit-learn)."""
    daftar_id    = [int(p['id']) for p in produk_list]
    tokens_list  = [ekstrak_fitur(p).split() for p in produk_list]
    idf          = _idf(tokens_list)

    vektor = {
        daftar_id[i]: _tfidf(tokens_list[i], idf)
        for i in range(len(daftar_id))
    }

    # Profil = rata-rata vektor target
    valid = [tid for tid in target_ids if tid in vektor]
    if not valid:
        valid = [daftar_id[0]]
    n       = len(valid)
    profil  = {}
    for tid in valid:
        for t, w in vektor[tid].items():
            profil[t] = profil.get(t, 0.0) + w / n

    set_target = set(target_ids)
    hasil = [
        (pid, _cosine_manual(profil, vektor.get(pid, {})))
        for pid in daftar_id if pid not in set_target
    ]
    hasil.sort(key=lambda x: x[1], reverse=True)

    top = hasil[:limit]
    return {
        "recommended_ids": [pid for pid, _ in top],
        "similarities":    {str(pid): round(s, 4) for pid, s in top},
        "engine":          "manual"
    }


# ─── Langkah 3: Entry point — dipanggil PHP via proc_open ─────────────────────

def main():
    """
    Alur utama:
      1. Terima data produk JSON dari stdin (dikirim PHP)
      2. Baca atribut produk (nama, kategori, deskripsi)
      3. Hitung kemiripan dengan TF-IDF + Cosine Similarity
      4. Kirim ID produk paling mirip ke stdout (dibaca PHP)
    """
    try:
        data_raw = sys.stdin.read().strip()
        if not data_raw:
            _error("Tidak ada data masuk dari PHP")
            return

        data = json.loads(data_raw)

        produk_list = data.get('products', [])
        target_ids  = [int(x) for x in data.get('target_ids', [])]
        limit       = int(data.get('limit', 4))

        if not produk_list:
            _error("Daftar produk kosong")
            return

        if not target_ids:
            target_ids = [int(produk_list[0]['id'])]

        # Pilih engine
        if SKLEARN_AVAILABLE:
            hasil = rekomendasikan_sklearn(produk_list, target_ids, limit)
        else:
            hasil = rekomendasikan_manual(produk_list, target_ids, limit)

        print(json.dumps(hasil, ensure_ascii=False))

    except json.JSONDecodeError as e:
        _error(f"JSON tidak valid: {e}")
        sys.exit(1)
    except Exception as e:
        _error(str(e))
        sys.exit(1)


def _error(pesan: str):
    print(json.dumps({
        "recommended_ids": [],
        "similarities":    {},
        "error":           pesan
    }))


if __name__ == '__main__':
    main()
