"""
recommender.py
Modul inference untuk sistem rekomendasi manga (Content-Based Filtering).
Semua artifact di-load sekali saat server start, lalu fungsi-fungsi di bawah
dipanggil berulang kali oleh app.py setiap ada request.
"""
import os
import joblib
import pandas as pd

ARTIFACT_DIR = os.path.dirname(os.path.abspath(__file__))

_df = joblib.load(os.path.join(ARTIFACT_DIR, "df_manga.pkl"))
_tfidf_matrix = joblib.load(os.path.join(ARTIFACT_DIR, "tfidf_matrix.pkl"))
_cbf_model = joblib.load(os.path.join(ARTIFACT_DIR, "cbf_model.pkl"))
_title_to_idx = joblib.load(os.path.join(ARTIFACT_DIR, "title_to_idx.pkl"))

# Kolom yang ditampilkan di kartu hasil rekomendasi / pencarian
DISPLAY_COLS = ["title", "genres", "themes", "score", "type", "status", "main_picture"]

# Kolom lengkap untuk halaman detail
DETAIL_COLS = [
    "title", "type", "score", "scored_by", "status", "volumes", "chapters",
    "members", "favorites", "genres", "themes", "demographics", "synopsis",
    "main_picture", "url",
]


def find_title_key(title: str):
    """Cari judul persis atau judul yang paling mendekati (substring match)."""
    key = title.strip().lower()
    if key in _title_to_idx:
        return key
    matches = [k for k in _title_to_idx if key in k]
    return matches[0] if matches else None


def get_recommendations(title: str, top_n: int = 10, pool_factor: int = 5):
    """Kembalikan list of dict rekomendasi, atau None jika judul tidak ditemukan."""
    key = find_title_key(title)
    if key is None:
        return None

    idx = _title_to_idx[key]
    n_neighbors = min(top_n * pool_factor + 1, _tfidf_matrix.shape[0])
    distances, indices = _cbf_model.kneighbors(_tfidf_matrix[idx], n_neighbors=n_neighbors)
    sims = 1 - distances.flatten()
    idxs = indices.flatten()

    mask = idxs != idx
    idxs, sims = idxs[mask], sims[mask]

    order = sims.argsort()[::-1][:top_n]
    idxs, sims = idxs[order], sims[order]

    result = _df.iloc[idxs][DISPLAY_COLS].copy()
    result["cosine_similarity"] = (sims.round(4) * 100).round(1)  # tampil sbg persen
    result.insert(0, "rank", range(1, len(result) + 1))
    return result.to_dict(orient="records")


def get_manga_detail(title: str):
    """Kembalikan metadata lengkap satu manga, atau None jika tidak ditemukan."""
    key = find_title_key(title)
    if key is None:
        return None
    row = _df.iloc[_title_to_idx[key]][DETAIL_COLS].to_dict()
    return row


def search_titles(query: str, limit: int = 10):
    """Cari judul manga untuk autocomplete. Judul yang diawali query muncul duluan,
    baru disusul judul yang mengandung query di posisi lain."""
    q = query.strip().lower()
    if not q:
        return []

    lower_titles = _df["title"].str.lower()

    starts_with = _df[lower_titles.str.startswith(q, na=False)]
    contains = _df[lower_titles.str.contains(q, na=False, regex=False) & ~lower_titles.str.startswith(q, na=False)]

    combined = pd.concat([starts_with, contains]).head(limit)
    return combined[["title", "genres", "score", "main_picture"]].to_dict(orient="records")


def search_manga(query: str, limit: int = 24):
    """Cari manga berdasarkan judul yang mengandung query (bukan rekomendasi kemiripan).
    Judul yang diawali query muncul lebih dulu, baru judul yang mengandung query
    di posisi lain. Kembalikan None jika tidak ada judul yang cocok sama sekali."""
    q = query.strip().lower()
    if not q:
        return None

    lower_titles = _df["title"].str.lower()
    mask = lower_titles.str.contains(q, na=False, regex=False)
    if not mask.any():
        return None

    starts_with = _df[lower_titles.str.startswith(q, na=False)]
    contains = _df[mask & ~lower_titles.str.startswith(q, na=False)]

    combined = pd.concat([starts_with, contains]).head(limit)
    result = combined[DISPLAY_COLS].copy()
    result.insert(0, "rank", range(1, len(result) + 1))
    return result.to_dict(orient="records")


def get_popular(limit: int = 12):
    """Kembalikan manga dengan skor tertinggi (untuk ditampilkan di homepage)."""
    pool = _df[_df["scored_by"] > 1000].sort_values("score", ascending=False).head(limit)
    return pool[DISPLAY_COLS].to_dict(orient="records")
