# -*- coding: utf-8 -*-
"""Rows of the Pro tebe page from a scored pool (docs/plan/pro-tebe/010 §6). Pure: no Kodi, no network.

A title appears once per page (Top 10 is the exception); rows are filled in display order, so the rows on top get
the best titles. `available(key)` is the Webshare check of phase P2 (everything is available until then).
"""
from wcs.foryou import features as F

ROW_SIZE = 15
FOR_YOU_SIZE = 20
TOP_SIZE = 10
MIN_ROW = 6
BECAUSE_ROWS = 3
MODES = ('all', 'movie', 'tv')
# display order; 'because' rows are numbered, the n-th one is used only when the seed exists
LAYOUT = ('for_you', 'top10', 'because#0', 'tonight', 'because#1', 'new', 'gems', 'because#2')
# fill order: Top 10 is not personal and goes first; a title with a strong tie to a seed stays in that seed's row
# the scarce rows (new, gems) pick before the big Pro tebe row, which has plenty to choose from
FILL = ('top10', 'because#0', 'because#1', 'because#2', 'new', 'gems', 'for_you', 'tonight')

# time of day -> (row title kind, allowed (media, min runtime, max runtime) ranges, genre bonus)
SLOTS = {
    'morning': ((('tv', 0, 35), ('movie', 0, 100)), {35, 16, 10751}),
    'afternoon': ((('tv', 0, 50), ('movie', 0, 115)), {35, 12, 10751, 14}),
    'evening': ((('movie', 85, 140), ('tv', 0, 65)), {28, 53, 80, 878, 9648, 18}),
    'night': ((('movie', 0, 115), ('tv', 0, 45)), {27, 53, 35}),
}


SLOT_MEDIA = {'morning': 'tv', 'afternoon': 'tv', 'evening': 'movie', 'night': 'movie'}


def slot_of(hour):
    if 5 <= hour < 11:
        return 'morning'
    if 11 <= hour < 17:
        return 'afternoon'
    if 17 <= hour < 22:
        return 'evening'
    return 'night'


KIDS = {10751, 16, 10762}


def _fits_slot(card, slot, kids_share=0.0, share=None):
    ranges, _bonus = SLOTS[slot]
    if slot in ('evening', 'night') and share and \
            max([share.get(g, 0.0) for g in F.folded_genres(card['genres'])] + [0.0]) < 0.4:
        return False                      # the evening is for the household's own genres, not for a random horror
    if slot in ('evening', 'night') and set(card['genres']) & KIDS and kids_share < 0.5:
        return False                      # the evening is for grown-ups unless the household watches family films
    runtime = card['runtime']
    if not runtime:
        return False
    return any(card['media'] == media and low <= runtime <= high for media, low, high in ranges)


def _slot_score(entry, slot):
    _ranges, bonus = SLOTS[slot]
    value = entry['score'] * (1.12 if set(entry['card']['genres']) & bonus else 1.0)
    if slot == 'evening' and entry['card']['media'] == 'movie':
        value *= 1.1                      # the evening belongs to films
    if slot in ('evening', 'night') and set(entry['card']['genres']) & {10751, 16, 10762}:
        value *= 0.6                      # ... and to grown-ups
    return value


def _linked(entry):
    parts = entry.get('parts') or {}
    return parts.get('cooc', 0) > 0 or parts.get('content', 0) >= 0.3 or bool(entry['kinds'] & {'maker', 'keyword', 'saga'})


def _seed_of(entry):
    reason = entry.get('reason') or {}
    return str(reason.get('seed') or reason.get('arg') or '')


MAX_SAME_MAKER = 2


def cap_makers(keys, pool):
    """At most MAX_SAME_MAKER titles of one creator per row ('Od tvůrce Chuck Lorre' three times is a list of his
    works, not a recommendation)."""
    seen, out = {}, []
    for k in keys:
        reason = pool[k].get('reason') or {}
        if reason.get('kind') == 'maker':
            seen[reason['arg']] = seen.get(reason['arg'], 0) + 1
            if seen[reason['arg']] > MAX_SAME_MAKER:
                continue
        out.append(k)
    return out


def diversify(keys, pool, decay=0.82):
    """Greedy re-rank: the k-th title with the same reason (seed, maker, genre) counts decay**k, so one watched film
    does not fill a whole row."""
    left = list(keys)
    seen = {}
    out = []
    while left:
        best = max(left, key=lambda k: pool[k]['score'] * decay ** seen.get(_seed_of(pool[k]), 0))
        left.remove(best)
        out.append(best)
        seen[_seed_of(pool[best])] = seen.get(_seed_of(pool[best]), 0) + 1
    return out


def _is_gem(entry, this_year):
    """Underrated and not new: a gem is something the household missed, not this year's premiere."""
    card = entry['card']
    if not entry.get('gem_ok', True) or entry['kinds'] & {'new', 'maker', 'top'} or not card['year'] \
            or card['year'] >= this_year - 1:
        return False                      # famous makers' works and charts are not hidden
    limit = 6000 if card['media'] == 'movie' else 1500
    if card['rating'] < 7.0 or card['votes'] > limit:
        return False
    return 'gem' in entry['kinds'] or (card['rating'] >= 7.3 and card['votes'] >= 80)


def because_seeds(seeds, pool, limit=BECAUSE_ROWS * 2):
    """Seeds that get a 'Protože jsi viděl' row: recent / liked titles with enough recommendations in the pool."""
    ordered = sorted((s for s in seeds if s['weight'] > 0 and s['kind'] in ('watched', 'like', 'pick')),
                     key=lambda s: (-(s['at'] or 0) if s['kind'] == 'watched' else -10 ** 12, -s['weight']))
    chosen = []
    for seed in ordered:
        key = '{}:{}'.format(seed['media'], seed['id'])
        count = sum(1 for e in pool.values() if any(sk == key for (_kind, sk) in e['sources']))
        if count >= MIN_ROW:
            chosen.append(key)
        if len(chosen) == limit:
            break
    return chosen


BECAUSE_SIZE = 12


def rows(pool, seeds, top_order, slot, available=None, mode='all', taste_share=None, dubbed=None, seed_genres=None,
         seed_audience=None):
    """[{'id', 'kind', 'items': [key, ...], 'seed'?, 'slot'?}] for one chip mode. `available(key)`: playable
    (the Webshare check); `dubbed(key)`: with Czech dubbing (the 'Nově s CZ dabingem' row)."""
    available = available or (lambda key: True)
    dubbed = dubbed or (lambda key: True)

    def ok(key):
        entry = pool[key]
        return entry['score'] > 0 and (mode == 'all' or entry['card']['media'] == mode) and available(key)

    ranked = sorted((k for k in pool if ok(k)), key=lambda k: -pool[k]['score'])
    # personal rows want a real link to the household: a seed, a creator / keyword, or a content match
    personal = [k for k in ranked if _linked(pool[k])] if any(s['weight'] > 0 for s in seeds) else ranked
    seeds_for_rows = because_seeds(seeds, pool)
    kids_share = max([taste_share.get(g, 0.0) for g in KIDS] + [0.0]) if taste_share else 0.0
    seed_kind = {'{}:{}'.format(s['media'], s['id']): s['kind'] for s in seeds}
    used = set()
    used_titles = set()
    out = []

    def title_of(key):
        card = pool[key]['card']                      # Flash the film and Flash the series: one of them per page
        return (card['original_title'] or card['title']).strip().lower()

    def take(candidates, size):
        """Unused titles; a remake / second version of a title already on the page is skipped too."""
        picked, names = [], set()
        for k in candidates:
            name = title_of(k)
            if k in used or name in used_titles or name in names:
                continue
            picked.append(k)
            names.add(name)
            if len(picked) == size:
                break
        return picked

    def use(items):
        used.update(items)
        used_titles.update(title_of(k) for k in items)

    built = {}
    for slot_id in FILL:
        row = None
        if slot_id == 'for_you':
            items = take(cap_makers(diversify(personal[:FOR_YOU_SIZE * 4], pool, 0.72), pool), FOR_YOU_SIZE)
            row = {'id': 'for_you', 'kind': 'for_you', 'items': items}
        elif slot_id == 'top10':
            items = [k for k in top_order if k in pool and ok(k) and pool[k]['sane']][:TOP_SIZE]
            if len(items) >= MIN_ROW:
                built[slot_id] = {'id': 'top10', 'kind': 'top10', 'items': items, 'reason': {'kind': 'trending'}}
                use(items)               # Top 10 is shown once; no other row repeats it
            continue
        elif slot_id.startswith('because#'):
            # the next seed whose row still has enough titles after the rows above took theirs
            while seeds_for_rows:
                seed = seeds_for_rows.pop(0)
                # TMDb recommendations of the seed first; its keyword-based "similar" titles only fill up
                # ... and only titles of the seed's main genre: TMDb also recommends Čarodějky for Dva a půl chlapa
                main = set((seed_genres or {}).get(seed, [])[:1])

                level = (seed_audience or {}).get(seed, -1)

                def kin(k):
                    if 'saga' in pool[k]['kinds']:
                        return True
                    card = pool[k]['card']
                    if level >= 0 and card.get('audience', -1) > level + 1:
                        return False              # a family sitcom's row has no TV-MA shows
                    return not main or bool(main & set(F.folded_genres(card['genres'])))
                # TMDb recommendations first, then the titles of the seed's own keywords; TMDb "similar" (shared
                # tags only: Čarodějky for Dva a půl chlapa) never fills a row
                recs = [k for k in ranked if ('rec', seed) in pool[k]['sources'] and kin(k)]
                kws = [k for k in ranked if ('kw', seed) in pool[k]['sources'] and k not in recs and kin(k)]
                items = take(recs, BECAUSE_SIZE)
                if len(items) < MIN_ROW:
                    items = take(recs + kws, BECAUSE_SIZE)
                if len(items) >= MIN_ROW:
                    break
            else:
                continue
            # inside the row every title is there because of the row's seed, whatever its own best reason is
            row = {'id': 'because:' + seed, 'kind': 'because', 'seed': seed, 'items': items,
                   'reason': {'kind': 'because_' + ('liked' if seed_kind.get(seed) in ('like', 'pick') else 'watched'),
                              'seed': seed}}
        elif slot_id == 'tonight':
            # one format per slot on the mixed page: episodes in the day, a film in the evening and at night
            # (the Filmy / Seriály chips keep their own)
            fitting = sorted((k for k in personal if _fits_slot(pool[k]['card'], slot, kids_share, taste_share)
                              and (mode != 'all' or pool[k]['card']['media'] == SLOT_MEDIA[slot])),
                             key=lambda k: -_slot_score(pool[k], slot))
            row = {'id': 'tonight', 'kind': 'tonight', 'slot': slot, 'items': take(cap_makers(diversify(fitting, pool), pool), ROW_SIZE)}
        elif slot_id == 'new':
            # new and in the taste: a link to a seed or a real content match, not just "popular this month"
            fresh = [k for k in personal if 'new' in pool[k]['kinds'] and k not in used and dubbed(k)]
            if len(fresh) < MIN_ROW:      # too few new titles with a link: the best new ones in the household's genres
                fresh = [k for k in ranked if 'new' in pool[k]['kinds'] and dubbed(k)]
            row = {'id': 'new', 'kind': 'new', 'items': take(cap_makers(diversify(fresh, pool), pool), ROW_SIZE)}
        elif slot_id == 'gems':
            this_year = max([pool[k]['card']['year'] for k in pool] + [0])
            popularity = sorted(pool[k]['card']['popularity'] for k in pool)
            ceiling = popularity[int(len(popularity) * 0.7)] if popularity else 0   # less known than most of the pool
            gems = [k for k in ranked if _is_gem(pool[k], this_year) and pool[k]['card']['popularity'] <= ceiling]
            row = {'id': 'gems', 'kind': 'gems', 'items': take(cap_makers(diversify(gems, pool), pool), ROW_SIZE)}
        if row and len(row['items']) >= MIN_ROW:
            use(row['items'])
            # diversity (and the time of day) pick the titles, the row shows them best match first
            # (the next part of the seed's saga leads its 'because' row)
            row['items'].sort(key=lambda k: (-(row['kind'] == 'because' and 'saga' in pool[k]['kinds']),
                                             -pool[k]['score']))
            built[slot_id] = row
    return [built[slot_id] for slot_id in LAYOUT if slot_id in built]
