# -*- coding: utf-8 -*-
"""Taste signals from the addon library (docs/plan/pro-tebe/010 §2).

`seeds(...)` is pure (unit-tested with plain dicts); `load()` reads library.db. A seed is a title the household
showed interest in, with a weight (negative = dislike) and the kind of interest, which picks the reason text.
"""
import math
import time

DAY = 86400
HALF_LIFE_DAYS = 240          # interest from watching fades; explicit feedback does not
W_LIKE, W_PICK, W_DISLIKE = 2.0, 1.6, -2.0
W_MOVIE_WATCHED, W_MOVIE_STARTED, W_SHORT = 1.0, 0.55, 0.12
WATCHED_SECONDS = 15 * 60       # a film watched this long counts as "seen" (a row "Protože jsi viděl" may follow)
W_STARTED = 0.3                 # opened and left early: a hint of taste, never a row of its own
W_LIST = 0.25                 # only in the list: a wish, not yet a taste


def _decay(at, now):
    if not at:
        return 0.6
    days = max(0.0, (now - at) / DAY)
    return 0.35 + 0.65 * math.pow(0.5, days / HALF_LIFE_DAYS)


def seeds(titles, movie_states, episode_counts, short_plays, feedback, now=None, watched_seconds=None):
    """Weighted seeds and the titles never to recommend.

    titles          {(media, id): {'in_list', 'last_activity_at'}}
    movie_states    {id: {'playcount', 'resume_pos', 'last_played_at'}}
    episode_counts  {show_id: (watched_episodes, started_episodes, last_played_at)}
    short_plays     {(media, id): last_started_at} plays that ended under the resume threshold
    feedback        {(media, id): (value, at)} like / dislike / hide / pick
    watched_seconds {(media, id): seconds really played} from the play log; a resume point alone can come from a
                    seek, so a film counts as seen by what was played (titles without a log keep their resume point)

    Returns (seeds, exclude): seeds = [{'media', 'id', 'weight', 'kind', 'at'}] sorted by weight,
    exclude = {(media, id)} (watched, in progress, in the lists, disliked, hidden).
    """
    now = now or time.time()
    watched_seconds = watched_seconds or {}
    found = {}
    exclude = set()

    def add(media, tmdb_id, weight, kind, at):
        k = (media, int(tmdb_id))
        old = found.get(k)
        if old is None or abs(weight) > abs(old['weight']):
            found[k] = {'media': media, 'id': int(tmdb_id), 'weight': weight, 'kind': kind, 'at': at or 0}

    for (media, tmdb_id), row in titles.items():
        if row.get('in_list') or row.get('last_activity_at'):
            exclude.add((media, int(tmdb_id)))
        if row.get('in_list'):
            add(media, tmdb_id, W_LIST, 'list', row.get('list_added_at'))
    for tmdb_id, st in movie_states.items():
        at = st.get('last_played_at')
        if int(st.get('playcount') or 0) > 0:
            add('movie', tmdb_id, W_MOVIE_WATCHED * _decay(at, now) * (1.15 if int(st['playcount']) > 1 else 1.0),
                'watched', at)
            exclude.add(('movie', int(tmdb_id)))
        elif watched_seconds.get(('movie', int(tmdb_id)), float(st.get('resume_pos') or 0)) >= WATCHED_SECONDS:
            add('movie', tmdb_id, W_MOVIE_STARTED * _decay(at, now), 'watched', at)
            exclude.add(('movie', int(tmdb_id)))
        elif float(st.get('resume_pos') or 0) > 0:
            add('movie', tmdb_id, W_STARTED * _decay(at, now), 'started', at)
            exclude.add(('movie', int(tmdb_id)))
    for show_id, (watched, started, at) in episode_counts.items():
        if watched:
            weight = min(1.6, 0.45 + 0.12 * watched + 0.05 * started) * _decay(at, now)
            add('tv', show_id, weight, 'watched', at)
            exclude.add(('tv', int(show_id)))
        elif started:                   # only an episode in progress: tried, not yet a favourite
            add('tv', show_id, W_STARTED * _decay(at, now), 'started', at)
            exclude.add(('tv', int(show_id)))
    for (media, tmdb_id), at in short_plays.items():
        add(media, tmdb_id, W_SHORT * _decay(at, now), 'tried', at)
    for (media, tmdb_id), (value, at) in feedback.items():
        k = (media, int(tmdb_id))
        if value == 'hide':
            exclude.add(k)
            found.pop(k, None)
            continue
        exclude.add(k)
        weight = {'like': W_LIKE, 'pick': W_PICK, 'dislike': W_DISLIKE}[value]
        found[k] = {'media': media, 'id': int(tmdb_id), 'weight': weight, 'kind': value, 'at': at or 0}
    result = sorted(found.values(), key=lambda s: (-s['weight'], -s['at'], s['id']))
    return result, exclude


def load(now=None):
    """seeds(...) over library.db."""
    from wcs.library2 import store
    conn = store.connect()
    try:
        titles = {(r['media_type'], r['tmdb_id']): dict(r) for r in conn.execute('SELECT * FROM titles')}
        movie_states = {r['tmdb_id']: dict(r) for r in conn.execute('SELECT * FROM movie_state')}
        episodes = {}
        for r in conn.execute('SELECT show_id, SUM(playcount > 0) w, SUM(playcount = 0 AND resume_pos > 0) s, '
                              'MAX(last_played_at) at FROM episode_state GROUP BY show_id'):
            episodes[r['show_id']] = (int(r['w'] or 0), int(r['s'] or 0), r['at'])
        short = {}
        for r in conn.execute("SELECT media_type, tmdb_id, MAX(started_at) at FROM play_log WHERE outcome='short' "
                              "GROUP BY media_type, tmdb_id"):
            short[(r['media_type'], r['tmdb_id'])] = r['at']
        # what was really played: per session at most its wall time (a resume or a seek jumps, the clock does not)
        played = {}
        for r in conn.execute('SELECT media_type, tmdb_id, SUM(MIN(COALESCE(position, 0), '
                              'MAX(0, COALESCE(ended_at, started_at) - started_at))) s FROM play_log '
                              'GROUP BY media_type, tmdb_id'):
            played[(r['media_type'], r['tmdb_id'])] = float(r['s'] or 0)
    finally:
        conn.close()
    return seeds(titles, movie_states, episodes, short, store.get_feedback(), now, played)
