# -*- coding: utf-8 -*-
"""Vinyl REST client. Account credentials stay in Kodi's addon profile."""
import hashlib
import json
import os
from urllib.parse import quote, urlencode, urlsplit

import xbmcaddon
import xbmcvfs

ADDON_ID = 'plugin.video.milionar'
BASE = 'https://hosting.pilsfree.net/chudy/proxy/4017/api/vinyl/'
PROFILE = xbmcvfs.translatePath('special://profile/addon_data/{}/'.format(ADDON_ID))
PUBLIC_SOURCES = ('curator', 'discover', 'radios', 'top_tracks')
SOURCES = PUBLIC_SOURCES + ('songs', 'favorite_songs', 'playlists', 'favorite_playlists',
                            'albums', 'artists', 'genres', 'favorite_albums', 'favorite_artists')
PAGE_SIZE = 20


def song_page(path, offset=0):
    """Read exactly one server page; never prefetch the rest of a collection."""
    separator = '&' if '?' in path else '?'
    result = request('{}{}limit={}&offset={}'.format(path, separator, PAGE_SIZE, offset))
    songs = result['songs']
    if not isinstance(songs, list) or len(songs) > PAGE_SIZE or not all(isinstance(s, dict) for s in songs):
        raise ValueError('Invalid Vinyl page')
    return songs, offset + len(songs) < result['total']


def collection_path(kind, identity, name='', artist=''):
    path = 'browse/{}/{}/songs'.format(kind, quote(name, safe=''))
    if kind == 'album':
        path += '?' + urlencode({'albumId': identity, 'artist': artist})
    return path


class LoginRequired(Exception):
    pass


def addon():
    return xbmcaddon.Addon(ADDON_ID)


def account():
    settings = addon()
    return settings.getSetting('vinyl_user_id') if settings.getSetting('vinyl_token') else ''


def scope():
    """In-memory session identity; never serialize the token into a cache or URL."""
    return account(), addon().getSetting('vinyl_token')


def favorite_value(value):
    if value in (True, 1, '1', 'true'):
        return True
    if value in (False, 0, '0', 'false'):
        return False
    return None


def _favorite_key(identity, session):
    return {'user': session[0], 'song': str(identity),
            'session': hashlib.sha256(session[1].encode('utf-8')).hexdigest()}


def favorite_state(identity):
    """Short-lived per-session delta/detail cache; None means no recent observation."""
    from wcs.metadata import TMDbCache
    return TMDbCache.get_from_cache('vinyl:song-favorite', _favorite_key(identity, scope()), expire_seconds=30)


def remember_favorite(identity, value, session, failed=False):
    from wcs.metadata import TMDbCache
    if session[0] and scope() == session:
        # An explicit unknown also suppresses stale source flags after a bodyless toggle.
        TMDbCache.set_to_cache('vinyl:song-favorite', _favorite_key(identity, session), {'state': value, 'failed': failed})


def request(path, method='GET', data=None, authenticated=True, timeout=(5, 25)):
    import requests
    session = scope()
    token = addon().getSetting('vinyl_token') if authenticated else ''
    response = requests.request(method, BASE + path.lstrip('/'), json=data,
                                headers={'Authorization': 'Bearer ' + (token or 'anonymous')}, timeout=timeout)
    if response.status_code == 401:
        raise LoginRequired(addon().getLocalizedString(32518))
    if not response.ok:
        # Do not log response bodies, headers or request objects (they can contain credentials).
        raise RuntimeError('Vinyl HTTP {}'.format(response.status_code))
    result = None if response.status_code == 204 else response.json()
    if method != 'GET':
        affected = mutation_sources(path, method)
        if affected:
            route = urlsplit(path).path.strip('/').split('/')
            if method == 'PATCH' and len(route) == 3 and route[0] == 'songs' and route[2] == 'favorite':
                remember_favorite(route[1], favorite_value(result.get('is_favorite')) if isinstance(result, dict) else None, session)
            from wcs.skin.refresh import request as request_refresh
            request_refresh(['vinyl_' + source for source in affected], force=True)
    return result


def mutation_sources(path, method):
    """Dependencies of successful favorite/playlist writes, including context.py callers."""
    path = urlsplit(path).path.strip('/')
    if method == 'PATCH' and path.endswith('/favorite'):
        if path.startswith('songs/'):
            return ('favorite_songs', 'songs')
        if path.startswith('playlists/'):
            return ('favorite_playlists', 'playlists')
        if path.startswith('browse/albums/'):
            return ('favorite_albums', 'albums')
        if path == 'browse/artists/favorite':
            return ('favorite_artists', 'artists')
    if path == 'playlists' and method == 'POST':
        return ('playlists',)
    if path.startswith('playlists/') and method in ('POST', 'PATCH', 'DELETE'):
        # Playlist title, count and first-song cover can appear in all these sources.
        return ('playlists', 'favorite_playlists', 'curator', 'discover', 'radios', 'top_tracks')
    return ()


def login(name, pin):
    result = request('auth/login', 'POST', {'name': name.strip(), 'pin': pin}, authenticated=False)
    settings = addon()
    settings.setSetting('vinyl_token', result['token'])
    settings.setSetting('vinyl_user_id', str(result['user']['id']))
    settings.setSetting('vinyl_status', result['user']['name'])
    return result['user']['name']


def logout():
    user = account()
    try:
        if account():
            request('auth/logout', 'POST')
    finally:
        for key in ('vinyl_token', 'vinyl_user_id', 'vinyl_status'):
            addon().setSetting(key, '')
        if user:
            for source in SOURCES:
                try:
                    os.remove(cache_path(source, user))
                except FileNotFoundError:
                    pass


def media(path):
    # Media is also readable with Vinyl's anonymous, read-only token. Never put a user's session in artwork/play URLs.
    return BASE + path.lstrip('/') + '|Authorization=Bearer%20anonymous'


def cover(entry, kind, size=600):
    version = entry.get('cover_version') or 0
    query = {'v': version}
    if size:
        query['size'] = size
    if kind in ('playlist', 'radio'):
        if kind == 'radio':
            entry = dict(entry, id=entry['playlist_id'], cover_mode=entry.get('playlist_cover_mode'))
            ids = entry.get('playlist_cover_song_ids') or []
            if isinstance(ids, str):
                ids = [int(i) for i in ids.split(',') if i]
        else:
            ids = entry.get('cover_song_ids') or []
        if entry.get('cover_mode') == 'custom':
            return media('playlists/{}/cover?{}'.format(entry['id'], urlencode(query)))
        refs = entry.get('cover_songs') or []
        if refs:
            return media('cover/{}?{}'.format(refs[0]['id'], urlencode(dict(query, v=refs[0].get('cover_version') or 0))))
        if ids:
            return media('cover/{}?{}'.format(ids[0], urlencode(query)))
    elif kind == 'song':
        return media('cover/{}?{}'.format(entry['id'], urlencode(query)))
    elif kind == 'album':
        query['albumId'] = entry.get('album_id') or entry.get('id') or ''
        artists = entry.get('artists') or []
        query['artist'] = entry.get('primary_artist') or (artists[0] if artists else '')
        return media('browse/albums/{}/cover?{}'.format(quote(entry['name'], safe=''), urlencode(query)))
    elif kind == 'artist':
        return media('browse/artists/cover?{}'.format(urlencode(dict(query, artist=entry['name']))))
    if kind == 'genre':
        refs = entry.get('cover_songs') or []
        ids = entry.get('cover_song_ids') or []
        if refs or ids:
            song = refs[0] if refs else {'id': ids[0]}
            return cover(song, 'song', size)
    return 'DefaultMusicGenres.png' if kind == 'genre' else 'DefaultAlbumCover.png'


def title_image(entry, kind, width=1200):
    if kind == 'playlist' and int(entry.get('title_image_version') or 0) > 0:
        return media('playlists/{}/title-image?{}'.format(entry['id'], urlencode(
            {'v': entry['title_image_version'], 'w': width})))
    return ''


def cache_path(source, user=None):
    return os.path.join(PROFILE, 'skin_cache', 'vinyl_{}_{}.json'.format((account() if user is None else user) or 'public', source))


def snapshot(source, user=None):
    if user is None:
        user = account()
    if not user and source not in PUBLIC_SOURCES:
        return None
    try:
        with open(cache_path(source, user), encoding='utf-8') as handle:
            data = json.load(handle)
        return data if isinstance(data, dict) and isinstance(data.get('items'), list) else None
    except (OSError, ValueError):
        return None


def cached(source, sort='source'):
    data = snapshot(source)
    if data is None:
        return []
    return data.get('sorts', {}).get(sort, data['items'])


def sources(sources=None, limit=50, sort='source', all_items=False, strict=True):
    """Fetch only selected sources; Home songs are bounded, interactive lists can page fully.

    With strict=False failed sources are omitted, so an empty list always denotes success.
    Non-song endpoints return their metadata collection in one request (no song expansion).
    """
    selected = set(SOURCES if sources is None else ([sources] if isinstance(sources, str) else sources))
    selected.intersection_update(SOURCES)
    session = scope()
    result = {}

    def fetch(names, load):
        names = selected.intersection(names)
        if not names:
            return
        if scope() != session:
            raise RuntimeError('Vinyl account changed')
        try:
            data = load(names)
            for name in names:
                entries = data[name]
                if not isinstance(entries, list) or not all(isinstance(e, dict) for e in entries):
                    raise ValueError('Invalid Vinyl source')
            result.update({name: data[name] for name in names})
        except Exception:
            if strict:
                raise

    def featured(names):
        data = request('featured?slim=1&curatorLimit=80')
        keys = {'curator': 'curatorPlaylists', 'discover': 'playlists', 'top_tracks': 'topTracks'}
        return {name: data[keys[name]] for name in names}

    fetch(('curator', 'discover', 'top_tracks'), featured)
    if session[0]:
        def playlists(names):
            entries = request('playlists')['playlists']
            return {name: [p for p in entries if p.get('is_owner' if name == 'playlists' else 'is_favorite')]
                    for name in names}
        fetch(('playlists', 'favorite_playlists'), playlists)
        for source, path in (('songs', 'songs'), ('favorite_songs', 'songs/favorites')):
            path += '?sort={}&order={}'.format('title' if sort == 'title' else 'created_at',
                                             'ASC' if sort == 'title' else 'DESC')
            fetch((source,), lambda names, path=path, source=source: {
                source: paged_songs(path, all_items=all_items, limit=200 if all_items else limit)})
        for source, path, key in (
                ('albums', 'browse/albums', 'albums'), ('artists', 'browse/artists', 'artists'),
                ('genres', 'browse/genres', 'genres'), ('favorite_albums', 'browse/favorites/albums', 'albums'),
                ('favorite_artists', 'browse/favorites/artists', 'artists')):
            fetch((source,), lambda names, source=source, path=path, key=key: {source: request(path)[key]})
    else:
        result.update({name: [] for name in selected if name not in PUBLIC_SOURCES})
    fetch(('radios',), lambda names: {'radios': request('radio/sessions/all')['sessions']})
    if scope() != session:
        raise RuntimeError('Vinyl account changed')
    return result


def paged_songs(path, all_items=False, limit=200):
    items, offset = [], 0
    session = scope()
    limit = min(200, max(1, int(limit)))
    while True:
        if scope() != session:
            raise RuntimeError('Vinyl account changed')
        separator = '' if path.endswith(('?', '&')) else '&' if '?' in path else '?'
        result = request('{}{}limit={}&offset={}'.format(path, separator, limit, offset))
        chunk = result['songs']
        if not isinstance(chunk, list) or not all(isinstance(e, dict) for e in chunk):
            raise ValueError('Invalid Vinyl songs')
        if scope() != session:
            raise RuntimeError('Vinyl account changed')
        if not all_items:
            return chunk[:limit]
        items.extend(chunk)
        offset += len(chunk)
        if not chunk or offset >= result.get('total', offset):
            return items


def collection(kind, identity, name='', artist=''):
    if kind == 'playlist':
        return request('playlists/{}'.format(int(identity))).get('songs', [])
    if kind == 'radio':
        timeline = request('radio/sessions/{}/timeline'.format(int(identity)))
        return collection('playlist', timeline['session']['playlist_id'])
    path = collection_path(kind, identity, name, artist)
    return paged_songs(path + ('&' if '?' in path else '?'), all_items=True)
