# -*- coding: utf-8 -*-
"""Service-side cache builders for the skin's network widgets (docs/plan/skin-milionar/060 §4).

Runs in a service thread. Keep last good snapshots on failure, and publish only
the named rows whose item content changed. The parent scheduler owns retries.
"""
import json
import os
import tempfile
import time

import xbmc
import xbmcaddon
import xbmcvfs

ADDON_ID = 'plugin.video.milionar'
PROFILE = xbmcvfs.translatePath('special://profile/addon_data/{}/'.format(ADDON_ID))
CACHE_DIR = os.path.join(PROFILE, 'skin_cache')
TMDB_IMG = 'https://image.tmdb.org/t/p/w500'
HOURS = 3600
SCHEDULE = {  # widget -> max age in seconds
    'widget_tmdb_popular_movies': 6 * HOURS,
    'widget_tmdb_popular_series': 6 * HOURS,
    'widget_kraska_root': 24 * HOURS,
    'widget_mycinema_recommended': 24 * HOURS,
}
SCHEDULE.update({'vinyl_' + source: 900 for source in (
    'curator', 'discover', 'top_tracks', 'songs', 'albums', 'artists', 'playlists', 'genres', 'radios',
    'favorite_songs', 'favorite_playlists', 'favorite_albums', 'favorite_artists')})


def active_rows():
    """Only configured network/Vinyl types, deduplicated across the Home slots; a section switched off needs nothing."""
    from wcs.skin import warm
    from wcs.skin.slots import SECTIONS, SLOTS
    return {row for section in SECTIONS if not warm.hidden(section) for number in range(1, SLOTS + 1)
            for row in [xbmc.getInfoLabel('Skin.String(mil.{}.{}.type)'.format(section, number))]
            if row in SCHEDULE}


def _log(msg, level=xbmc.LOGINFO):
    xbmc.log('[Milionář Skin] {}'.format(msg), level)


def cache_path(name):
    if name.startswith('vinyl_'):
        from wcs.vinyl import client
        return client.cache_path(name[len('vinyl_'):])
    return os.path.join(CACHE_DIR, name + '.json')


def cache_age(name):
    try:
        return time.time() - os.path.getmtime(cache_path(name))
    except OSError:
        return None


def write_cache(name, items):
    """Atomically replace a successful snapshot, including []; refresh only changed content."""
    if not isinstance(items, list) or not all(isinstance(item, dict) for item in items):
        raise ValueError('Invalid widget cache items')
    path = cache_path(name)
    previous = None
    try:
        with open(path, encoding='utf-8') as handle:
            previous = json.load(handle)['items']
    except (OSError, ValueError, KeyError, TypeError):
        pass
    os.makedirs(CACHE_DIR, exist_ok=True)
    temporary = None
    try:
        with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=CACHE_DIR, delete=False) as handle:
            temporary = handle.name
            json.dump({'built': int(time.time()), 'items': items}, handle, ensure_ascii=False)
        os.replace(temporary, path)
    finally:
        if temporary and os.path.exists(temporary):
            os.remove(temporary)
    different = previous != items
    if different:
        from wcs.skin.refresh import changed
        changed([name])
    return different


def _entry(row, media_type):
    title = row.get('title') or row.get('name') or ''
    date = row.get('release_date') or row.get('first_air_date') or ''
    tmdb_id = row.get('id')
    kind = 'movie' if media_type == 'movie' else 'tv'
    return {'title': title, 'year': date[:4], 'tmdb_id': tmdb_id,
            'poster': TMDB_IMG + row['poster_path'] if row.get('poster_path') else '',
            'plot': row.get('overview', ''), 'rating': row.get('vote_average'),
            'media_type': 'movie' if media_type == 'movie' else 'series',
            'run': 'action=show_wcs_details&media_type={}&tmdb_id={}'.format(kind, tmdb_id)}


def build_tmdb_popular(addon, media_type):
    from wcs.metadata import TMDbClient
    endpoint = 'movie/popular' if media_type == 'movie' else 'tv/popular'
    rows = TMDbClient.fetch_tmdb_items(media_type, endpoint, {'page': 1}, addon, strict=True)
    return [_entry(r, media_type) for r in rows if r.get('id')][:20]


def build_kraska_root(addon):
    from wcs.streamcinema import SCClient
    # browse_root() collapses failed responses into []; inspect the transport result instead.
    response = SCClient._api_get('/', params={}, auth_token=addon.getSetting('sc_auth_token'))
    if not isinstance(response, dict) or not isinstance(response.get('menu'), list):
        raise ValueError('Invalid Kraska root response')
    entries = []
    for item in response['menu']:
        if not isinstance(item, dict):
            raise ValueError('Invalid Kraska root item')
        if item.get('type') not in ('dir', 'ldir') or (item.get('visible') or '').startswith('sc://'):
            continue
        i18n = item.get('i18n_info', {})
        title = i18n.get('cs', {}).get('title') or i18n.get('en', {}).get('title') or item.get('title', '')
        if title:
            entries.append({'title': title, 'path': item.get('url', ''),
                            'icon': (item.get('art') or {}).get('icon', '')})
    return entries


def build_mycinema_recommended(addon):
    from wcs.metadata import TMDbCache, TMDbClient
    cinemas = _load('mycinema_channels.json')
    owned = {str(int(movie['id'])) for cinema in cinemas for movie in (cinema.get('media_list') or [])
             if movie.get('id') is not None}
    votes = {}
    for movie_id in sorted(owned):
        endpoint = 'movie/{}/recommendations'.format(movie_id)
        params = {'language': 'cs-CZ'}
        rows = TMDbCache.get_from_cache(endpoint, params, expire_seconds=24 * HOURS)
        missing = rows is None
        if missing:
            # Failed strict fetches leave both the stale per-movie response and the Home snapshot intact.
            rows = TMDbClient.fetch_tmdb_items('movie', endpoint, params, addon, strict=True)
        if not isinstance(rows, list) or not all(isinstance(row, dict) and row.get('id') is not None for row in rows):
            raise ValueError('Invalid cached TMDb recommendations')
        if missing:
            TMDbCache.set_to_cache(endpoint, params, rows)
        seen = set()
        for row in rows:
            rid = str(row['id'])
            if rid in owned or rid in seen or not row.get('poster_path'):
                continue
            seen.add(rid)
            count, _row = votes.get(rid, (0, row))
            votes[rid] = (count + 1, row)
    ranked = sorted(votes.values(), key=lambda pair: (pair[0], pair[1].get('vote_average') or 0), reverse=True)
    return [_entry(row, 'movie') for _count, row in ranked[:20]]


def _load(name):
    try:
        with open(os.path.join(PROFILE, name), 'r', encoding='utf-8') as handle:
            return json.load(handle)
    except FileNotFoundError:
        return []


def refresh(addon, name):
    """Build one widget cache now; providers that are not configured are skipped silently."""
    try:
        if name == 'vinyl' or name in SCHEDULE and name.startswith('vinyl_'):
            from wcs.vinyl import widgets as vinyl_widgets
            return vinyl_widgets.refresh(sources=None if name == 'vinyl' else [name[len('vinyl_'):]])
        elif name == 'widget_kraska_root':
            if not addon.getSetting('sc_auth_token'):
                return False
            items = build_kraska_root(addon)
        else:
            if name == 'widget_tmdb_popular_movies':
                items = build_tmdb_popular(addon, 'movie')
            elif name == 'widget_tmdb_popular_series':
                items = build_tmdb_popular(addon, 'tv')
            elif name == 'widget_mycinema_recommended':
                items = build_mycinema_recommended(addon)
            else:
                return False
        write_cache(name, items)
        _log('{}: cached {} items'.format(name, len(items)))
        return True
    except Exception as exc:
        _log('{} refresh failed: {}'.format(name, type(exc).__name__), xbmc.LOGWARNING)
        return False


def refresh_due(force=()):
    """Compatibility worker: due active rows plus explicitly forced names; no retry loops."""
    addon = xbmcaddon.Addon(ADDON_ID)
    active = active_rows()
    forced = set(force)
    if 'vinyl' in forced:
        forced.update(name for name in active if name.startswith('vinyl_'))
    selected = active | (forced & set(SCHEDULE))
    playing = xbmc.getCondVisibility('Player.HasVideo')
    result = {}
    for name in sorted(selected):
        if playing and name not in forced:
            continue
        age = cache_age(name)
        if name in forced or age is None or age >= SCHEDULE[name]:
            result[name] = refresh(addon, name)
    return result
