# -*- coding: utf-8 -*-
import sys
import json
import re
import gzip
import os

import requests
import xbmc
import xbmcgui
import xbmcplugin
import xbmcaddon
import xbmcvfs

from modules import search_index_builder

ADDON      = xbmcaddon.Addon()
ADDON_ID   = ADDON.getAddonInfo("id")
HANDLE     = int(sys.argv[1]) if len(sys.argv) > 1 else -1

# Master list URL (same as builder)
MASTER_URL = "https://uck.st/splite_movies/master_list_1.json"

DATA_DIR = xbmcvfs.translatePath(
    "special://profile/addon_data/{0}/".format(ADDON_ID)
)
INDEX_PATH        = os.path.join(DATA_DIR, "search_index.json.gz")
LEGACY_INDEX_PATH = os.path.join(DATA_DIR, "search_index.json")  # old large file (if any)
SIGNATURE_PATH    = os.path.join(DATA_DIR, "master_signature.json")


# -----------------------------------------------------
# Basic tokenizer
# -----------------------------------------------------
def _tokenize(text):
    return [t for t in re.split(r"[^a-z0-9]+", text.lower()) if t]


# -----------------------------------------------------
# Fuzzy similarity helpers
# -----------------------------------------------------
def _levenshtein(a, b):
    if a == b:
        return 0
    la, lb = len(a), len(b)
    if la == 0:
        return lb
    if lb == 0:
        return la

    prev = list(range(lb + 1))
    for i in range(1, la + 1):
        ca = a[i - 1]
        curr = [i]
        for j in range(1, lb + 1):
            cb = b[j - 1]
            cost = 0 if ca == cb else 1
            curr.append(
                min(
                    prev[j] + 1,       # deletion
                    curr[j - 1] + 1,   # insertion
                    prev[j - 1] + cost # substitution
                )
            )
        prev = curr
    return prev[-1]


def _similarity(a, b):
    a = (a or "").strip().lower()
    b = (b or "").strip().lower()
    if not a or not b:
        return 0.0
    dist = _levenshtein(a, b)
    return 1.0 - float(dist) / float(max(len(a), len(b)))


# -----------------------------------------------------
# Direct master load (fallback, not primary path)
# -----------------------------------------------------
def _load_master_direct():
    xbmc.log("[DiamondSearch] Direct load master list from URL", xbmc.LOGINFO)
    try:
        r = requests.get(MASTER_URL, timeout=15)
        if not r.ok:
            xbmc.log("[DiamondSearch] HTTP {0}".format(r.status_code), xbmc.LOGERROR)
            return []
        data = r.json()
        if not isinstance(data, list):
            xbmc.log("[DiamondSearch] Master JSON not a list", xbmc.LOGERROR)
            return []
        return data
    except Exception as e:
        xbmc.log("[DiamondSearch] Error direct-loading master: {0}".format(e), xbmc.LOGERROR)
        return []


# -----------------------------------------------------
# Remote master signature (ETag / Last-Modified)
# -----------------------------------------------------
def _get_remote_master_signature():
    """
    Try a HEAD request to get ETag or Last-Modified.
    If neither is present or HEAD fails, returns None.
    """
    try:
        xbmc.log("[DiamondSearch] Checking remote master headers (HEAD)...", xbmc.LOGINFO)
        r = requests.head(MASTER_URL, timeout=8, allow_redirects=True)
        if not r.ok:
            xbmc.log("[DiamondSearch] HEAD HTTP {0}".format(r.status_code), xbmc.LOGWARNING)
            return None

        headers = r.headers or {}
        etag = headers.get("ETag") or headers.get("Etag") or headers.get("etag")
        lm   = headers.get("Last-Modified") or headers.get("Last-modified") or headers.get("last-modified")

        if not etag and not lm:
            xbmc.log("[DiamondSearch] No ETag/Last-Modified in headers.", xbmc.LOGINFO)
            return None

        sig = {
            "etag": etag or "",
            "last_modified": lm or ""
        }
        xbmc.log("[DiamondSearch] Remote signature: {0}".format(sig), xbmc.LOGINFO)
        return sig
    except Exception as e:
        xbmc.log("[DiamondSearch] Error checking remote headers: {0}".format(e), xbmc.LOGWARNING)
        return None


def _load_local_signature():
    if not xbmcvfs.exists(SIGNATURE_PATH):
        return None
    try:
        with open(SIGNATURE_PATH, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception as e:
        xbmc.log("[DiamondSearch] Failed to read local master signature: {0}".format(e), xbmc.LOGWARNING)
        return None


def _save_local_signature(sig):
    if sig is None:
        return
    try:
        if not xbmcvfs.exists(DATA_DIR):
            xbmcvfs.mkdirs(DATA_DIR)
        with open(SIGNATURE_PATH, "w", encoding="utf-8") as f:
            json.dump(sig, f, ensure_ascii=False, indent=2)
        xbmc.log("[DiamondSearch] Saved master signature: {0}".format(SIGNATURE_PATH), xbmc.LOGINFO)
    except Exception as e:
        xbmc.log("[DiamondSearch] Failed to save master signature: {0}".format(e), xbmc.LOGWARNING)


# -----------------------------------------------------
# Ensure index exists and is up-to-date
# -----------------------------------------------------
def _ensure_index():
    # Clean old uncompressed index if present
    try:
        if xbmcvfs.exists(LEGACY_INDEX_PATH):
            xbmcvfs.delete(LEGACY_INDEX_PATH)
            xbmc.log("[DiamondSearch] Deleted legacy search_index.json", xbmc.LOGINFO)
    except Exception as e:
        xbmc.log("[DiamondSearch] Could not delete legacy index: {0}".format(e), xbmc.LOGWARNING)

    # Check remote signature (ETag / Last-Modified)
    remote_sig = _get_remote_master_signature()
    local_sig  = _load_local_signature()

    index_exists = xbmcvfs.exists(INDEX_PATH)

    need_rebuild = False

    if not index_exists:
        xbmc.log("[DiamondSearch] No index found - need initial build.", xbmc.LOGINFO)
        need_rebuild = True
    elif remote_sig and local_sig and remote_sig != local_sig:
        xbmc.log("[DiamondSearch] Remote master changed - rebuilding index.", xbmc.LOGINFO)
        need_rebuild = True
    else:
        xbmc.log("[DiamondSearch] Index exists and appears current.", xbmc.LOGINFO)

    if need_rebuild:
        dialog = xbmcgui.DialogProgress()
        dialog.create("Building Search Index", "Optimizing RD Player Search Database...")
        dialog.update(0)

        def progress_callback(pct):
            try:
                dialog.update(
                    max(min(int(pct), 100), 0),
                    "Optimizing RD Player Search Database..."
                )
            except Exception:
                pass

        ok = search_index_builder.build_index(
            progress_callback=progress_callback,
            force=True
        )
        dialog.close()

        if not ok or not xbmcvfs.exists(INDEX_PATH):
            xbmc.log("[DiamondSearch] Index build failed, falling back to direct master scan.", xbmc.LOGERROR)
            return None

        # Save remote signature (if we got one)
        if remote_sig:
            _save_local_signature(remote_sig)

    # Load compressed index
    try:
        with gzip.open(INDEX_PATH, "rb") as f:
            payload = json.loads(f.read().decode("utf-8"))
        movies = payload.get("movies") or []
        token_map = payload.get("token_map") or {}
        if not isinstance(movies, list) or not isinstance(token_map, dict):
            xbmc.log("[DiamondSearch] Invalid index payload, ignoring.", xbmc.LOGERROR)
            return None
        xbmc.log(
            "[DiamondSearch] Index loaded: movies={0}, tokens={1}".format(
                len(movies), len(token_map)
            ),
            xbmc.LOGINFO,
        )
        return movies, token_map
    except Exception as e:
        xbmc.log("[DiamondSearch] Failed to load compressed index: {0}".format(e), xbmc.LOGERROR)
        return None


# -----------------------------------------------------
# Candidate selection using index
# -----------------------------------------------------
def _get_candidates_from_index(terms, movies, token_map):
    """
    Use token_map to quickly narrow down a candidate movie set.
    AND-search across all terms.
    """
    all_tokens = list(token_map.keys())
    term_candidate_sets = []

    for t in terms:
        t = t.lower()
        cand_indices = set()

        for tok in all_tokens:
            # Exact token
            if tok == t:
                cand_indices.update(token_map.get(tok, []))
                continue

            # Substring in either direction (2160p, remux, hdr, etc.)
            if t in tok or tok in t:
                cand_indices.update(token_map.get(tok, []))
                continue

            # Fuzzy word match
            if len(t) >= 4 and len(tok) >= 3:
                sim = _similarity(t, tok)
                if sim >= 0.75:
                    cand_indices.update(token_map.get(tok, []))

        if not cand_indices:
            return []

        term_candidate_sets.append(cand_indices)

    if not term_candidate_sets:
        return []

    candidate_ids = term_candidate_sets[0]
    for s in term_candidate_sets[1:]:
        candidate_ids = candidate_ids.intersection(s)

    if not candidate_ids:
        return []

    results = [movies[i] for i in sorted(candidate_ids) if 0 <= i < len(movies)]
    xbmc.log(
        "[DiamondSearch] Index-based candidates: {0}".format(len(results)),
        xbmc.LOGINFO,
    )
    return results


# -----------------------------------------------------
# SMART SEARCH (Title-first, Plot fallback, weighted)
# -----------------------------------------------------
def _search_smart(movies, terms):
    scored_primary = []
    scored_fallback = []

    for mv in movies:
        title = (mv.get("title") or "").lower()
        plot  = (mv.get("plot") or "").lower()

        if not title and not plot:
            continue

        title_tokens = _tokenize(title)
        plot_tokens  = _tokenize(plot)

        matched_terms = 0
        score = 0
        has_title_hit = False

        for t in terms:
            term_matched = False

            # 1) TITLE: exact word
            if t in title_tokens:
                score += 60
                matched_terms += 1
                term_matched = True
                has_title_hit = True

            # 2) TITLE: substring (2160p, remux, hdr, etc.)
            elif t in title:
                score += 40
                matched_terms += 1
                term_matched = True
                has_title_hit = True

            else:
                # 3) TITLE: fuzzy
                best_sim = 0.0
                if len(t) >= 4 and title_tokens:
                    for tok in title_tokens:
                        sim = _similarity(t, tok)
                        if sim > best_sim:
                            best_sim = sim

                if best_sim >= 0.72:
                    score += int(35 * best_sim)
                    matched_terms += 1
                    term_matched = True
                    has_title_hit = True

            # If not matched in title, try plot
            if not term_matched:
                # 4) PLOT exact word
                if t in plot_tokens:
                    score += 20
                    matched_terms += 1
                    term_matched = True

                # 5) PLOT substring
                elif t in plot:
                    score += 10
                    matched_terms += 1
                    term_matched = True

                else:
                    # 6) PLOT fuzzy (weak)
                    best_sim = 0.0
                    if len(t) >= 5 and plot_tokens:
                        for tok in plot_tokens:
                            sim = _similarity(t, tok)
                            if sim > best_sim:
                                best_sim = sim

                    if best_sim >= 0.80:
                        score += int(10 * best_sim)
                        matched_terms += 1
                        term_matched = True

            if not term_matched:
                matched_terms = -1
                break

        if matched_terms == len(terms):
            if has_title_hit:
                scored_primary.append((score, mv))
            else:
                scored_fallback.append((score, mv))

    if scored_primary:
        scored_primary.sort(key=lambda x: -x[0])
        return [m for s, m in scored_primary]

    scored_fallback.sort(key=lambda x: -x[0])
    return [m for s, m in scored_fallback]


# -----------------------------------------------------
# SIMPLE multi-term (Title / Plot only)
# -----------------------------------------------------
def _search_simple_field(movies, terms, field_name):
    results = []
    for mv in movies:
        raw = mv.get(field_name) or ""
        text = raw.lower()
        if not text:
            continue

        tokens = _tokenize(raw)

        all_ok = True
        for t in terms:
            if t in text:
                continue

            matched = False
            if len(t) >= 4 and tokens:
                best_sim = 0.0
                for tok in tokens:
                    sim = _similarity(t, tok)
                    if sim > best_sim:
                        best_sim = sim
                if best_sim >= 0.75:
                    matched = True

            if not matched:
                all_ok = False
                break

        if all_ok:
            results.append(mv)

    return results


# -----------------------------------------------------
# Render results
# -----------------------------------------------------
def _render_results(movies, query, mode):
    try:
        from modules.rdmagnet_browser_v4_player import render_movie_list
    except Exception as e:
        xbmc.log("[DiamondSearch] Import render failed: {0}".format(e), xbmc.LOGERROR)
        xbmcgui.Dialog().ok("Diamond Search", "Render engine missing.")
        return

    if not movies:
        xbmcgui.Dialog().ok(
            "Diamond Search",
            "No results found for:\n[COLOR gold]{0}[/COLOR]\n({1})".format(query, mode)
        )
        return

    label = "Search: {0}  ({1})".format(query, mode)
    render_movie_list(movies, label)


# -----------------------------------------------------
# Main Search Handler
# -----------------------------------------------------
def run_text_search():

    mode_selector = xbmcgui.Dialog().select(
        "Diamond Search — Choose Search Type",
        [
            "Smart Search (Title First)",
            "Search by Title Only",
            "Search by Plot Only"
        ]
    )
    if mode_selector == -1:
        return

    if mode_selector == 0:
        search_mode = "smart"
        search_label = "Smart"
    elif mode_selector == 1:
        search_mode = "title"
        search_label = "Title"
    else:
        search_mode = "plot"
        search_label = "Plot"

    kb = xbmc.Keyboard("", "Search Movies by {0}".format(search_label))
    kb.doModal()
    if not kb.isConfirmed():
        return

    query = kb.getText().strip()
    if not query:
        xbmcgui.Dialog().ok("Diamond Search", "Nothing entered.")
        return

    search_q = query.lower()
    terms = [t.strip() for t in search_q.split() if t.strip()]
    if not terms:
        xbmcgui.Dialog().ok("Diamond Search", "Nothing entered.")
        return

    xbmc.log(
        "[DiamondSearch] Mode={0} Query='{1}' Terms={2}".format(
            search_mode, query, terms
        ),
        xbmc.LOGINFO,
    )

    idx = _ensure_index()

    if idx is None:
        # Fallback: direct master scan
        movies = _load_master_direct()
        token_map = None
        if not movies:
            xbmcgui.Dialog().ok("Diamond Search", "Failed to load master list.")
            return
    else:
        movies, token_map = idx

    if search_mode == "smart" and token_map is not None:
        candidates = _get_candidates_from_index(terms, movies, token_map)
        if not candidates:
            results = []
        else:
            results = _search_smart(candidates, terms)
    else:
        if search_mode == "smart":
            results = _search_smart(movies, terms)
        elif search_mode == "title":
            results = _search_simple_field(movies, terms, "title")
        else:
            results = _search_simple_field(movies, terms, "plot")

    xbmc.log("[DiamondSearch] Results found: {0}".format(len(results)), xbmc.LOGINFO)

    try:
        save_path = xbmcvfs.translatePath(
            "special://profile/addon_data/{0}/last_search_text.json".format(ADDON_ID)
        )
        with open(save_path, "w", encoding="utf-8") as f:
            json.dump(results, f, ensure_ascii=False, indent=2)
        xbmc.log("[DiamondSearch] Saved: {0}".format(save_path), xbmc.LOGINFO)
    except Exception as e:
        xbmc.log("[DiamondSearch] Save error: {0}".format(e), xbmc.LOGERROR)

    _render_results(results, query, search_label)
