# -*- coding: utf-8 -*-
"""Taste profile, match score, match percentage and reason (docs/plan/pro-tebe/010 §5). Pure: no Kodi, no network.

score = content similarity (taste profile vs. the title's features, IDF weighted)
      + co-occurrence (the title is in TMDb recommendations / similar of the seeds, by rank and seed weight)
      + quality (Bayesian rating), each normalised to 0..1 over the pool, minus penalties.
"""
import hashlib
import math

from wcs.foryou import features as F

W_CONTENT, W_COOC, W_QUALITY = 0.47, 0.28, 0.25
SIMILAR_FACTOR = 0.35
SAGA_BONUS = 1.15
MAKER_COOC, KEYWORD_COOC = 0.5, 0.3   # same creator / strongest taste keyword: as good as a mid-rank TMDb pick          # TMDb "similar" is keyword-based and noisy; "recommendations" is behavioural
NEG_FACTOR = 0.8
PRIOR_RATING, PRIOR_VOTES = 6.6, 250
HOME_LANGS = {'en', 'cs', 'sk'}


def _norm(vec):
    length = math.sqrt(sum(v * v for v in vec.values()))
    return {k: v / length for k, v in vec.items()} if length else {}


def profile(seed_features):
    """(positive, negative) taste vectors from [(seed, features)]; each seed counts by its weight, unit-normalised."""
    pos, neg = {}, {}
    for seed, feats in seed_features:
        target = pos if seed['weight'] > 0 else neg
        for name, value in _norm(feats).items():
            target[name] = target.get(name, 0.0) + abs(seed['weight']) * value
    return pos, neg


def idf(feature_sets):
    """Inverse document frequency over the candidate pool: 'Drama' says little, a director says a lot."""
    df = {}
    for feats in feature_sets:
        for name in feats:
            df[name] = df.get(name, 0) + 1
    n = max(1, len(feature_sets))
    return {name: math.log(1.0 + n / count) for name, count in df.items()}


def _cosine(a, b, weights):
    if not a or not b:
        return 0.0
    dot = sum(v * b.get(k, 0.0) * weights.get(k, 1.0) ** 2 for k, v in a.items())
    na = math.sqrt(sum((v * weights.get(k, 1.0)) ** 2 for k, v in a.items()))
    nb = math.sqrt(sum((v * weights.get(k, 1.0)) ** 2 for k, v in b.items()))
    return dot / (na * nb) if na and nb else 0.0


GENERIC = ('lang:', 'm:', 'oc:', 'dec:', 'cert:')      # shared by half the pool: no evidence of a real link


def similarity(a, b):
    """Cosine of two titles over their specific features (genres, keywords, people, studio, network, saga)."""
    def specific(vec):
        return {k: v for k, v in vec.items() if not k.startswith(GENERIC)}
    return _cosine(specific(a), specific(b), {})


def quality(rating, votes):
    """Bayesian rating mapped to 0..1 (5.0 -> 0, 9.0 -> 1)."""
    bayes = (votes * rating + PRIOR_VOTES * PRIOR_RATING) / (votes + PRIOR_VOTES) if votes else PRIOR_RATING - 0.6
    return max(0.0, min(1.0, (bayes - 5.0) / 4.0))


def cooccurrence(sources, seed_weight):
    """Sum over seeds of weight x rank decay; returns (value, best seed key, its contribution)."""
    total, best, best_value = 0.0, None, 0.0
    for (kind, seed_key), rank in sources.items():
        if kind not in ('rec', 'sim', 'kw'):
            continue
        weight = seed_weight.get(seed_key, 0.0)
        if weight <= 0:
            continue
        value = weight * (1.0 / (1.0 + rank / 6.0)) * (1.0 if kind == 'rec' else SIMILAR_FACTOR)
        total += value
        if value > best_value:
            best, best_value = seed_key, value
    return total, best, best_value


def taste_of(pos):
    """What the penalties need from the profile: genre shares (of the strongest genre) and decades."""
    genres = {int(n[2:]): v for n, v in pos.items() if n.startswith('g:')}
    top = max(genres.values()) if genres else 0.0
    decades = {int(n[4:]): v for n, v in pos.items() if n.startswith('dec:')}
    top_dec = max(decades.values()) if decades else 0.0
    return {'share': {g: v / top for g, v in genres.items()} if top else {},
            'decades': {d for d, v in decades.items() if top_dec and v / top_dec >= 0.2},
            'langs': {n[5:] for n in pos if n.startswith('lang:')}}


def penalty(card, taste):
    """Multiplier < 1 for titles the household is unlikely to want (outside its taste)."""
    factor = 1.0
    genres = set(card['genres'])
    if genres & F.BLOCKED_TV_GENRES:
        return 0.0
    share = taste['share']
    for gid in genres & F.NICHE_GENRES:
        if share.get(gid, 0.0) < 0.35:
            factor *= 0.5 if share.get(gid, 0.0) < 0.15 else 0.75
    for gid in genres & F.MOOD_GENRES:
        if share.get(gid, 0.0) < 0.1:
            factor *= 0.7                 # horror / crime / war / western for a household that never watches them
    if card['language'] and card['language'] not in HOME_LANGS and card['language'] not in taste['langs']:
        factor *= 0.6
    year = card['year']
    if year and year < 1960 and (year // 10 * 10) not in taste['decades']:
        return 0.0                        # Chaplin is a classic, not a recommendation for this household
    if year and year < 1985 and (year // 10 * 10) not in taste['decades']:
        factor *= 0.5 if year < 1975 else 0.7
    if card['votes'] < 40:
        factor *= 0.6
    elif card['votes'] < 150:
        factor *= 0.85                    # few votes: the rating and TMDb's recommendations are not reliable yet
    if card['votes'] >= 30 and card['rating'] < 6.4:
        factor *= 0.65 if card['rating'] < 6.0 else 0.82        # Geostorm-class filler loses to the real thing
    if not card['overview']:
        factor *= 0.7                     # no Czech overview on TMDb: hardly known (or dubbed) here
    elif card['language'] != 'cs' and card['original_title'] and card['title'] == card['original_title']:
        factor *= 0.88                    # no Czech title: probably never distributed here
    return factor


def jitter(day, item_key, amount=0.05):
    """Stable per-day noise, so the page moves a little every day without being random."""
    digest = hashlib.sha1('{}|{}'.format(day, item_key).encode('utf-8')).digest()
    return 1.0 + amount * (digest[0] / 255.0 - 0.5) * 2


def match_percent(score, best):
    """Netflix-like percentage: the best title of the day is ~98 %, an average fit ~75 %."""
    if best <= 0:
        return 0
    ratio = max(0.0, min(1.0, score / best))
    return int(round(58 + 40 * math.pow(ratio, 0.8)))


def taste_summary(pos, limit=3, prefix='g:'):
    return [name for name, _v in sorted(((k, v) for k, v in pos.items() if k.startswith(prefix)),
                                        key=lambda kv: -kv[1])[:limit]]


BROAD_GENRES = {18, 35, 28, 12, 10751, 53}     # "your genre: Drama" says nothing


def similar_seed(features, seed_vectors, media=None):
    """(seed key, cosine) of the positive seed most like the title; only seeds of the same kind (a film is not
    'like' a sitcom)."""
    best, value = None, 0.0
    for key, vec in seed_vectors.items():
        if media and not key.startswith(media + ':'):
            continue
        sim = _cosine(vec, features, {})
        if sim > value:
            best, value = key, sim
    return best, value


def reason(entry, seed_weight, seed_kind, seed_titles, pos, feature_names, seed_vectors=None):
    """{'kind', 'arg'}: why the title is recommended (rendered with localized templates)."""
    _total, best, best_value = cooccurrence(entry['sources'], seed_weight)
    if best and best_value >= 0.15:
        kind = {'watched': 'because_watched', 'like': 'because_liked', 'pick': 'because_liked',
                'list': 'because_list', 'tried': 'similar_to', 'started': 'similar_to'}.get(seed_kind.get(best),
                                                                                             'because_watched')
        return {'kind': kind, 'arg': seed_titles.get(best, ''), 'seed': best}
    feats = entry.get('features') or {}
    shared = [(pos.get(name, 0.0) * value, name) for name, value in feats.items() if name in pos]
    for prefix, kind in (('d:', 'maker'), ('col:', 'collection'), ('c:', 'actor')):
        hits = sorted((s, n) for s, n in shared if n.startswith(prefix) and feature_names.get(n))
        if hits and hits[-1][0] > 0.05:
            return {'kind': kind, 'arg': feature_names[hits[-1][1]]}
    seed, sim = similar_seed(feats, seed_vectors or {}, entry['card']['media'])
    shares_genre = seed and set(F.folded_genres(entry['card']['genres'])) & {
        int(n[2:]) for n in (seed_vectors or {}).get(seed, {}) if n.startswith('g:')}
    if seed and sim >= 0.42 and shares_genre:
        return {'kind': 'similar_to', 'arg': seed_titles.get(seed, ''), 'seed': seed}
    if entry['card']['rating'] >= 7.5 and entry['card']['votes'] >= 500:
        return {'kind': 'quality', 'arg': ''}            # "Vysoce hodnocené" says more than a genre of Forrest Gump
    genres = sorted((s, n) for s, n in shared if n.startswith('g:') and int(n[2:]) not in BROAD_GENRES)
    if genres:
        return {'kind': 'genre', 'arg': int(genres[-1][1][2:])}
    if 'trend' in entry['kinds'] or 'cz' in entry['kinds']:
        return {'kind': 'trending', 'arg': ''}
    return {'kind': 'quality', 'arg': ''}


def _percentile(values, q):
    positive = sorted(v for v in values if v > 0)
    if not positive:
        return 1e-9
    return positive[min(len(positive) - 1, int(q * len(positive)))]


def score_pool(pool, pos, neg, seed_weight, day):
    """Fill entry['score'] for every pool entry (in place). Pool entries: {'card', 'features', 'sources', 'kinds'}."""
    weights = idf([e['features'] for e in pool.values()])
    taste = taste_of(pos)
    raw = {}
    for k, e in pool.items():
        content = _cosine(pos, e['features'], weights) - NEG_FACTOR * _cosine(neg, e['features'], weights)
        cooc, _best, _value = cooccurrence(e['sources'], seed_weight)
        raw[k] = (max(0.0, content), cooc, quality(e['card']['rating'], e['card']['votes']))
    # normalised by the 95th percentile, not the maximum: one saga sequel must not flatten everything else
    max_content = _percentile([r[0] for r in raw.values()], 0.95)
    max_cooc = _percentile([r[1] for r in raw.values()], 0.95)
    for k, e in pool.items():
        content, cooc, qual = raw[k]
        content, cooc = min(1.0, content / max_content), min(1.0, cooc / max_cooc)
        if e['kinds'] & {'maker', 'keyword'}:
            cooc = max(cooc, MAKER_COOC if 'maker' in e['kinds'] else KEYWORD_COOC)
        value = W_CONTENT * content + W_COOC * cooc + W_QUALITY * qual
        e['parts'] = {'content': round(content, 3), 'cooc': round(cooc, 3),
                      'quality': round(qual, 3)}
        saga = SAGA_BONUS if 'saga' in e['kinds'] else 1.0      # the next part of a watched saga: the surest pick
        e['score'] = value * saga * penalty(e['card'], taste) * jitter(day, k)
    return pool
