# -*- coding: utf-8 -*-
"""Shared watch progress of the channels (MyTV / MyCinema) and the library (user decision 2026-10-06).

Settings `mytv_shared_progress` / `mycinema_shared_progress`, both on by default. With the switch on, library.db is
the only truth for the channels:

* reads: a channel's next episode, resume point and watched films come from the store (wcs/ai/ChannelHistory.py asks
  here), by the same rule as Continue watching (`next_up.next_up`);
* plays: a channel's play carries the tracker contract (`play.contract`, origin `channel`), so the service tracker saves
  resume / watched / the play log like for any library play; what comes next stays the channel's program
  (`follow` False) and the title joins My series / My movies;
* a manual progress edit in MyTV marks the episodes in the store (`set_pointer`).

The channel files (`mytv_history/<channel>.json`) are still written by the channel's own monitor. `sync` reconciles the
two worlds when the MyTV switch changes (service worker, the only place here that may use the network):
  on:  the further progress wins: the episodes up to a channel's pointer become watched in the store
  off: every channel takes over the store's current pointer and resume point
Until the first `sync` succeeded the channels keep reading their files (`active` is False), so a failed merge never
shows a series from its beginning.
"""
import json
import os
import time

import xbmc
import xbmcaddon
import xbmcvfs

from wcs.library2 import meta, store

ADDON_ID = 'plugin.video.milionar'
SETTINGS = {'tv': 'mytv_shared_progress', 'cinema': 'mycinema_shared_progress'}
FLAG = 'shared.tv.applied'                    # '1' the channels were merged into the store, '0' copied back
SYNC_JOB = 'shared:sync'                      # name of the service job (wcs/skin/service.py)
MYTV_ROWS = {'widget_mytv_next', 'widget_mytv_channels'}
BEFORE_FIRST = 99                             # (season - 1, 99): "the previous season is finished", as MyTV's edit dialog


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


def enabled(kind='tv'):
    """The user's switch; on unless it was turned off."""
    try:
        return xbmcaddon.Addon(ADDON_ID).getSetting(SETTINGS[kind]) != 'false'
    except Exception:
        return True


def active(kind='tv'):
    """The store speaks for the channels of `kind` now (MyTV: only after its channels were merged into the store)."""
    if not enabled(kind):
        return False
    if kind != 'tv':
        return True
    try:
        return store.get_flag(FLAG) == '1'
    except Exception:
        return False


def pending():
    """The MyTV switch changed and `sync` has work to do."""
    try:
        return store.get_flag(FLAG) != ('1' if enabled('tv') else '0')
    except Exception:
        return False


# --------------------------------------------------------------------------- reads

def _number(value):
    try:
        return int(value)
    except (TypeError, ValueError):
        return None


def _states(series_id):
    """{(season, episode): state} of the regular episodes of a series."""
    show = _number(series_id)
    if show is None:
        return {}
    return {(r['season'], r['episode']): r for r in store.get_episode_states(show) if r['season'] > 0}


def _resume_of(states):
    rows = [r for r in states.values() if float(r.get('resume_pos') or 0) > 0 and int(r.get('playcount') or 0) == 0]
    if not rows:
        return None
    row = max(rows, key=lambda r: r.get('last_played_at') or 0)
    return {'season': row['season'], 'episode': row['episode'], 'position': float(row['resume_pos']),
            'total': float(row.get('resume_total') or 0)}


def before(season, episode):
    """The channel pointer that makes (season, episode) the next episode."""
    if episode > 1:
        return season, episode - 1
    return (season - 1, BEFORE_FIRST) if season > 1 else (1, 0)


def _pointer_of(states):
    point = _resume_of(states)
    if point:
        return before(point['season'], point['episode'])
    watched = [key for key, r in states.items() if int(r.get('playcount') or 0) > 0]
    return max(watched) if watched else (1, 0)


def resume(series_id):
    """The episode in progress (newest play first): {'season', 'episode', 'position', 'total'} or None."""
    return _resume_of(_states(series_id))


def pointer(series_id):
    """(season, episode) a channel counts as its last watched one: its next episode is the first after it. The same
    rule as Continue watching: an episode in progress comes first, else the one after the latest watched by order."""
    return _pointer_of(_states(series_id))


def movie_watched(movie_id):
    movie = _number(movie_id)
    state = store.get_state('movie', movie) if movie is not None else None
    return bool(state and int(state.get('playcount') or 0) > 0)


def movie_resume(movie_id):
    """{'position', 'total'} of a film in progress, or None."""
    movie = _number(movie_id)
    state = store.get_state('movie', movie) if movie is not None else None
    if not state or float(state.get('resume_pos') or 0) <= 0 or int(state.get('playcount') or 0) > 0:
        return None
    return {'season': 0, 'episode': 0, 'position': float(state['resume_pos']), 'total': float(state.get('resume_total') or 0)}


# --------------------------------------------------------------------------- writes

def _aired(series_id, network=False):
    """[(season, episode)] of the aired regular episodes from the metadata cache; `network`: fetch what is missing."""
    from wcs.library2 import jobs
    targets = jobs.cached_targets('tv', series_id)
    if targets is None and network:
        from wcs.library2 import tmdb
        try:
            tmdb.refresh_show(series_id)
        except Exception as exc:
            _log('metadata of series {} unavailable: {}'.format(series_id, type(exc).__name__), xbmc.LOGWARNING)
            return None
        targets = jobs.cached_targets('tv', series_id)
    return targets


def set_pointer(series_id, season, episode, network=True):
    """Manual edit in MyTV: every aired episode up to (season, episode) is watched, every later one is not.
    Returns False when the episode list of the series is not available."""
    show = _number(series_id)
    targets = _aired(show, network) if show is not None else None
    if targets is None:
        return False
    point = (int(season), int(episode))
    known = set(targets) | set(_states(show))
    store.mark_many('tv', show, [t for t in targets if t <= point], True)
    store.mark_many('tv', show, [t for t in known if t > point], False)
    return True


def list_snapshot(media, tmdb_id, data=None, title=''):
    """What a title stores when it joins My movies / My series (shown until its TMDb detail is cached)."""
    data = data or {}
    name = (data.get('title') if media == 'movie' else data.get('name')) or title or str(tmdb_id)
    date_key = 'release_date' if media == 'movie' else 'first_air_date'
    return {'id': tmdb_id, ('title' if media == 'movie' else 'name'): name,
            'year': (data.get(date_key) or '')[:4], 'overview': data.get('overview', ''),
            'poster_path': data.get('poster_path', ''), 'backdrop_path': data.get('backdrop_path', ''),
            'vote_average': data.get('vote_average', 0)}


def ensure_listed(media, tmdb_id, title=''):
    """A title played from a channel belongs to My movies / My series. Cache only (runs beside the playback loop)."""
    tmdb_id = int(tmdb_id)
    if store.is_in_list(media, tmdb_id):
        return False
    data = meta.get(meta.key_movie(tmdb_id) if media == 'movie' else meta.key_tv(tmdb_id))[0]
    store.set_in_list(media, tmdb_id, True, list_snapshot(media, tmdb_id, data, title))
    return True


def contract(media, tmdb_id, stream, selected, season=None, episode=None, title='', show_title='', follow=False):
    """Tracker contract of a channel's playlist item (its stream is resolved outside the canonical resolver)."""
    from wcs.library2 import play
    info = {'type': 'movie' if media == 'movie' else 'episode', 'tmdb': int(tmdb_id),
            's': None if media == 'movie' else int(season), 'e': None if media == 'movie' else int(episode)}
    return play.contract(info, stream, selected, title=title, show_title=show_title,
                         origin=play.ORIGIN_CHANNEL, follow=follow)


# --------------------------------------------------------------------------- the switch changed

def _profile():
    return xbmcvfs.translatePath('special://profile/addon_data/{}/'.format(ADDON_ID))


def _channels():
    try:
        with open(os.path.join(_profile(), 'mytv_channels.json'), 'r', encoding='utf-8') as handle:
            data = json.load(handle)
    except (OSError, ValueError):
        return []
    return [c for c in data if isinstance(c, dict) and c.get('id')] if isinstance(data, list) else []


def _series_of(channel):
    return [s for s in channel.get('media_list') or channel.get('series_list') or [] if _number(s.get('id')) is not None]


def _history_time(channel_id):
    try:
        return int(os.path.getmtime(os.path.join(_profile(), 'mytv_history', '{}.json'.format(channel_id))))
    except OSError:
        return int(time.time())


def _resume_threshold():
    from wcs.library2 import policy
    try:
        return float(xbmcaddon.Addon(ADDON_ID).getSetting('lib_resume_min_seconds') or policy.DEFAULT_RESUME_MIN_SECONDS)
    except ValueError:
        return float(policy.DEFAULT_RESUME_MIN_SECONDS)


def merge_into_store(network=True):
    """Switch on: the further progress wins. A series' furthest channel pointer marks its episodes watched in the
    store, a channel's resume point of an unwatched episode is kept, and the series joins My series.
    Returns False when a series could not be merged yet (its episode list is missing)."""
    from wcs.ai import ChannelHistory
    furthest = {}
    for channel in _channels():
        history = ChannelHistory.load_channel_history(channel['id'])
        when = _history_time(channel['id'])
        for series in _series_of(channel):
            entry = history.get(str(series['id'])) or {}
            point = (_number(entry.get('season')) or 1, _number(entry.get('episode')) or 0)
            resume_point = None
            if float(entry.get('resume_position') or 0) > 0:
                resume_point = (_number(entry.get('resume_s')) or 1, _number(entry.get('resume_e')) or 1,
                                float(entry['resume_position']), float(entry.get('resume_total') or 0))
            if point == (1, 0) and not resume_point:
                continue                                           # never played in this channel
            best = furthest.get(int(series['id']))
            if not best or point > best['point']:
                furthest[int(series['id'])] = {'point': point, 'resume': resume_point, 'when': when,
                                              'name': series.get('name') or series.get('title') or ''}
    complete, minimum = True, _resume_threshold()
    for show, best in furthest.items():
        targets = _aired(show, network)
        if targets is None:
            complete = False
            continue
        store.mark_many('tv', show, [t for t in targets if t <= best['point']], True, best['when'])
        states = _states(show)
        if best['resume'] and not _resume_of(states):
            season, episode, position, total = best['resume']
            state = states.get((season, episode)) or {}
            if position >= minimum and total > position and int(state.get('playcount') or 0) == 0:
                store.save_resume('tv', show, position, total, season, episode, when=best['when'])
        ensure_listed('tv', show, best['name'])
    _log('merged {} series of the channels into the store{}'.format(len(furthest), '' if complete else ' (some are waiting for metadata)'))
    return complete


def copy_to_channels():
    """Switch off: every channel continues from the store's current pointer and resume point."""
    from wcs.ai import ChannelHistory
    for channel in _channels():
        history = dict(ChannelHistory.load_channel_history(channel['id']))
        for series in _series_of(channel):
            states = _states(series['id'])
            if not states:
                continue
            season, episode = _pointer_of(states)
            entry = {'season': season, 'episode': episode}
            point = _resume_of(states)
            if point:
                entry.update(resume_position=round(point['position'], 1), resume_total=round(point['total'], 1),
                             resume_s=point['season'], resume_e=point['episode'])
            history[str(series['id'])] = entry
        ChannelHistory.save_channel_history(channel['id'], history)
    return True


def sync(network=True):
    """Bring the channels and the store together after the MyTV switch changed. True when nothing is left to do."""
    want = '1' if enabled('tv') else '0'
    current = store.get_flag(FLAG)
    if current == want:
        return True
    if want == '1':
        done = merge_into_store(network)
    else:
        done = copy_to_channels() if current == '1' else True       # never shared before: the files are already right
    if done:
        store.set_flag(FLAG, want)
        from wcs.skin import refresh
        refresh.changed(refresh.SERIES_ROWS | MYTV_ROWS)             # the rows read the other world now
    return done
