# -*- coding: utf-8 -*-
"""Cache-only Vinyl widgets, using the existing Home slot and click contracts."""
import json
import os
import random
import tempfile
import threading
import time
from contextlib import contextmanager
from urllib.parse import quote, urlencode

import xbmc
import xbmcgui
import xbmcplugin

from wcs.skin import family
from wcs.vinyl import client

BASE = 'plugin://plugin.video.milionar/'
ROWS = {
    'vinyl_curator': ('curator', 'playlist', 32501),
    'vinyl_discover': ('discover', 'playlist', 32502),
    'vinyl_songs': ('songs', 'song', 32503),
    'vinyl_top_tracks': ('top_tracks', 'song', 32690),
    'vinyl_albums': ('albums', 'album', 32504),
    'vinyl_artists': ('artists', 'artist', 32505),
    'vinyl_playlists': ('playlists', 'playlist', 32506),
    'vinyl_genres': ('genres', 'genre', 32507),
    'vinyl_radios': ('radios', 'radio', 32508),
    'vinyl_favorite_songs': ('favorite_songs', 'song', 32510),
    'vinyl_favorite_playlists': ('favorite_playlists', 'playlist', 32511),
    'vinyl_favorite_albums': ('favorite_albums', 'album', 32512),
    'vinyl_favorite_artists': ('favorite_artists', 'artist', 32513),
}


_refresh_mode = threading.local()
FIELDS = ('id', 'album_id', 'name', 'title', 'artist', 'artists', 'primary_artist', 'description', 'duration',
          'total_duration', 'song_count', 'created_at', 'updated_at', 'cover_version', 'cover_mode', 'album',
          'cover_song_ids', 'cover_songs', 'playlist_id', 'playlist_name', 'playlist_cover_song_ids',
          'playlist_cover_mode', 'character_id', 'status', 'is_favorite', 'is_owner', 'user_id',
          'title_image_version', 'genre', '_genres', 'year')
SONG_SOURCES = ('songs', 'favorite_songs')
PLAYLIST_SOURCES = ('curator', 'discover', 'playlists', 'favorite_playlists')
GENRE_FETCHES = 60                       # playlist details read by one refresh; the rest follows in the next ones


def _limit(value):
    from wcs.skin import performance
    try:
        count = int(value or 20)
    except (ValueError, TypeError):
        count = 20
    return performance.limit(count if count in (10, 20, 30, 50) else 20)


@contextmanager
def defer_refresh():
    """Legacy context edits request worker jobs at the REST write, never fetch inline."""
    previous = getattr(_refresh_mode, 'deferred', False)
    _refresh_mode.deferred = True
    try:
        yield
    finally:
        _refresh_mode.deferred = previous


def configurations():
    """Read active slots without constructing dialog/context items."""
    from wcs.skin import slots
    result = {}
    initialized = xbmc.getCondVisibility('Skin.HasSetting(MilionarSlotsV1)')
    for section, (_label, _rows, defaults) in slots.SECTIONS.items():
        for number in range(1, slots.SLOTS + 1):
            prefix = 'Skin.String(mil.{}.{}.'.format(section, number)
            row = xbmc.getInfoLabel(prefix + 'type)')
            default = defaults[number - 1] if not initialized and number <= len(defaults) else None
            if not row and default:
                row = default[0]
            if row not in ROWS:
                continue
            count = xbmc.getInfoLabel(prefix + 'limit)') or (default[1] if default else '20')
            order = xbmc.getInfoLabel(prefix + 'sort)') or (default[2] if default else 'source')
            source = ROWS[row][0]
            config = (order if order in ('source', 'title', 'random') else 'source', _limit(count))
            if config not in result.setdefault(source, []):
                result[source].append(config)
    return result


def _plain(entries):
    result = []
    for entry in entries:
        item = {key: entry[key] for key in FIELDS if key in entry}
        if 'cover_songs' in item:
            item['cover_songs'] = [{k: song[k] for k in ('id', 'cover_version') if k in song}
                                   for song in item['cover_songs'] or []]
        result.append(item)
    return result


def _selected(data, source, order, count, user):
    entries = list(data.get('sorts', {}).get(order, data['items']))
    if order == 'title':
        entries.sort(key=lambda e: (e.get('name') or e.get('title') or e.get('playlist_name') or '').casefold())
    elif order == 'random':
        # Stable for unchanged data; a warm plugin invocation must not reshuffle the Home row.
        random.Random('{}:{}'.format(user, source)).shuffle(entries)
    return entries[:count]


def _display(data, source, configs, user):
    kind = ROWS['vinyl_' + source][1]
    result = []
    for order, count in configs:
        cards = []
        for entry in _selected(data, source, order, count, user):
            cards.append((query(entry, kind), client.cover(entry, kind), client.title_image(entry, kind),
                          entry.get('description') or '', entry.get('artist') or '',
                          entry.get('song_count') or 0, entry.get('duration') or entry.get('total_duration') or 0,
                          entry.get('album') or '', entry.get('album_id') or '',
                          entry.get('genre') or entry.get('_genres') or '', entry.get('is_favorite'),
                          entry.get('created_at') or ''))
        result.append(cards)
    return result


def _genre_cache():
    return os.path.join(os.path.dirname(client.cache_path('x', 'public')), 'vinyl_playlist_genres.json')


def _top_genres(songs, count=3):
    totals = {}
    for song in songs:
        for name in str(song.get('genre') or '').split(','):
            if name.strip():
                totals[name.strip()] = totals.get(name.strip(), 0) + 1
    return ','.join(sorted(totals, key=lambda name: (-totals[name], name))[:count])


def _playlist_genres(documents, configs, user):
    """Service job only: a playlist list has no genres, so the shown playlists get the most frequent genres of their
    tracks (`_genres`). One detail request per new or changed playlist, remembered by its `updated_at`."""
    path = _genre_cache()
    try:
        with open(path, encoding='utf-8') as handle:
            known = json.load(handle)
    except (OSError, ValueError):
        known = {}
    budget, dirty = GENRE_FETCHES, False
    for source in PLAYLIST_SOURCES:
        if source not in documents:
            continue
        shown = [entry for order, count in configs[source] for entry in _selected(documents[source], source, order, count, user)]
        for entry in shown + documents[source]['items']:
            key, stamp = str(entry.get('id')), entry.get('updated_at') or ''
            saved = known.get(key)
            if (not saved or saved[0] != stamp) and budget > 0 and any(entry is e for e in shown):
                budget -= 1
                try:
                    songs = client.request('playlists/{}'.format(int(entry['id'])), timeout=(3, 8)).get('songs', [])
                except Exception:
                    continue
                saved = known[key] = [stamp, _top_genres(songs)]
                dirty = True
            if saved and saved[1]:
                entry['_genres'] = saved[1]
    if dirty:
        try:
            os.makedirs(os.path.dirname(path), exist_ok=True)
            with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=os.path.dirname(path), delete=False) as handle:
                json.dump(known, handle)
            os.replace(handle.name, path)
        except OSError:
            pass


def refresh(sources=None):
    """Service job: publish each successful source atomically; False asks the scheduler to retry."""
    if getattr(_refresh_mode, 'deferred', False):
        return True
    from wcs.skin import performance
    from wcs.skin.refresh import changed
    configs = configurations()
    selected = set(configs if sources is None else ([sources] if isinstance(sources, str) else sources))
    selected.intersection_update(client.SOURCES)
    if not selected:
        return True
    session = client.scope()
    user = session[0]
    for source in selected:
        configs.setdefault(source, [('source', _limit(20))])
    try:
        data = client.sources(selected.difference(SONG_SOURCES), strict=False)
        documents = {source: {'items': _plain(entries), 'sorts': {}} for source, entries in data.items()}
        for source in selected.intersection(SONG_SOURCES):
            count = max(count for _sort, count in configs[source])
            random_pool = any(order == 'random' for order, _count in configs[source])
            pool = max(count, 50 if performance.enabled() else 200) if random_pool else count
            entries = client.sources([source], limit=pool, strict=False)
            if source not in entries:
                continue
            document = {'items': _plain(entries[source]), 'sorts': {}, 'limits': {'source': pool}}
            if any(order == 'title' for order, _count in configs[source]):
                title_count = max(count for order, count in configs[source] if order == 'title')
                titles = client.sources([source], limit=title_count, sort='title', strict=False)
                if source not in titles:
                    continue  # Never replace a coherent previous source with a partial sort generation.
                document['sorts']['title'] = _plain(titles[source])
                document['limits']['title'] = title_count
            documents[source] = document
        _playlist_genres(documents, configs, user)
    except Exception:
        return False
    if client.scope() != session:
        return False
    success = selected.issubset(documents)
    updated = []
    for source, document in documents.items():
        if client.scope() != session:
            return False
        previous = client.snapshot(source, user)
        path = client.cache_path(source, user)
        temporary = None
        try:
            different = previous is None or _display(previous, source, configs[source], user) != _display(
                document, source, configs[source], user)
            document.update(built=time.time(), version=2, presentation=1)
            os.makedirs(os.path.dirname(path), exist_ok=True)
            with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=os.path.dirname(path), delete=False) as handle:
                temporary = handle.name
                json.dump(document, handle)
            if client.scope() != session:
                return False
            os.replace(temporary, path)
            if different:
                updated.append('vinyl_' + source)
        except Exception:
            success = False
        finally:
            if temporary and os.path.exists(temporary):
                os.remove(temporary)
    if updated:
        changed(updated)
    return success


def query(entry, kind, row='', click='open'):
    artists = entry.get('artists') or []
    return {'action': 'vinyl_item', 'kind': kind, 'id': (entry.get('album_id') or entry.get('id', '')) if kind == 'album' else entry.get('id', ''),
            'name': entry.get('name') or entry.get('title') or entry.get('playlist_name') or '',
            'artist': entry.get('primary_artist') or entry.get('artist') or (artists[0] if artists else ''),
            'row': row, 'click': click}


def item(entry, kind, row='', click='open', listing=False):
    from wcs.skin import performance
    from wcs.vinyl import presentation
    title = entry.get('name') or entry.get('title') or entry.get('playlist_name') or ''
    params = query(entry, kind, row, click)
    if entry.get('_queue'):
        params['ids'] = entry['_queue']
    if entry.get('_parent_playlist'):
        params.update(parent_playlist=str(entry['_parent_playlist']), owner='1' if entry.get('_owner') else '')
    if kind == 'song':
        params.update(album=entry.get('album', ''), album_id=entry.get('album_id', ''))
    card = not listing and (click == 'detail' or (click == 'open' and family.active()))
    if card:
        params['action'] = 'vinyl_detail'
    path = BASE + '?' + urlencode(params)
    folder = click == 'open' and not card
    art = performance.artwork(client.cover(entry, kind))
    li = xbmcgui.ListItem(title, label2=entry.get('artist') or presentation.tracks(entry.get('song_count')), path=path,
                          offscreen=True)
    li.setArt({'thumb': art, 'poster': art, 'landscape': art, 'fanart': art,
               'icon': art})
    plot = entry.get('description') or entry.get('artist') or ''
    if kind != 'song':
        plot = '{}\n{}'.format(presentation.tracks(entry.get('song_count')), plot).strip()
    # Home uses Estuary's video cards, the library listings use Kodi's native music metadata/window.
    if listing:
        tag = li.getMusicInfoTag()
        tag.setTitle(title)
        tag.setArtist(entry.get('artist') or entry.get('primary_artist') or '')
        tag.setDuration(int(entry.get('duration') or entry.get('total_duration') or 0))
        if kind == 'song':
            tag.setMediaType('song')
    else:
        tag = li.getVideoInfoTag()
        tag.setTitle(title)
        tag.setPlot(plot)
        tag.setDuration(int(entry.get('duration') or entry.get('total_duration') or 0))
    if folder:
        li.setProperty('milionar.folder', '1')
        command = 'ActivateWindow(Music,{},{})'.format(path, 'return')
    else:
        li.setProperty('IsPlayable', 'true')
        command = 'RunPlugin({})'.format(path)
    li.setProperty('milionar.exec', command)
    if not listing:
        from wcs.vinyl import presentation
        presentation.apply(li, entry, kind, params)
    if listing and kind == 'song':
        li.setPath(client.media('stream/{}'.format(entry['id'])))
        li.setProperty('IsPlayable', 'true')
    li.setProperty('milionar.ctxdata', quote(urlencode(dict(params, type='vinyl', title=title,
                                                        path=path, is_favorite=entry.get('is_favorite', ''),
                                                        folder='1' if folder else '')), safe=''))
    from wcs.skin import context
    li.addContextMenuItems(context.entry(context.unpack(li.getProperty('milionar.ctxdata'))), True)
    return li.getPath(), li, folder


def build(params):
    from wcs.library2 import clicks
    from wcs.skin.refresh import request
    row = params.get('row', '')
    source, kind, _title = ROWS[row]
    order = params.get('sort', 'source')
    limit = _limit(params.get('limit'))
    user = client.account()
    data = client.snapshot(source, user)
    if data is None:
        if user or source in client.PUBLIC_SOURCES:
            request([row])
        return []
    if data.get('presentation') != 1 or (source in SONG_SOURCES and (data.get('version') != 2 or
            data.get('limits', {}).get('title' if order == 'title' else 'source', 0) < limit)):
        request([row])
    click = clicks.resolve(row, params.get('click'))
    entries = _selected(data, source, order, limit, user)
    if kind == 'song':
        queue = ','.join(str(e['id']) for e in entries)
        entries = [dict(e, _queue=queue) for e in entries]
    return [item(e, kind, row, click) for e in entries]


def run(handle, params):
    xbmcplugin.setContent(handle, 'albums')
    items = build(params)
    xbmcplugin.addDirectoryItems(handle, items, len(items))
    xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
