#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Search/ranking harness: runs the addon's real Webshare search + sort + dialog
row building outside Kodi and judges the top result with oracle.py.

Webshare responses are cached in cache/ (read-only `search` calls, token from
the addon settings, never printed); reruns are offline and deterministic.

Usage:
    search_harness.py library [--per-show 4] [--movies-only|--series-only] [--show 5]
    search_harness.py episode "Přátelé" 1 2 [--original Friends] [--show 20]
    search_harness.py movie "Marťan" 2015 [--original "The Martian"] [--show 20]
    search_harness.py query "Marťan 2015"            # raw Webshare answer, no ranking
    search_harness.py diff out/report_A.json out/report_B.json   # what changed between two runs
Options: --refresh (refetch), --offline (cache only), --kraska (keep KRA.sk on),
         --set key=value (addon setting override, e.g. autoplay_preferred_quality=2)
"""

import argparse
import hashlib
import json
import os
import re
import sqlite3
import sys
import time
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import kodi_stubs  # noqa: E402
import oracle  # noqa: E402

CACHE_DIR = os.path.join(HERE, 'cache')
OUT_DIR = os.path.join(HERE, 'out')
DB_PATH = os.path.join(kodi_stubs.KODI_HOME, 'userdata/Database/MyVideos131.db')
API = 'https://webshare.cz/api/'
HEADERS = {'User-Agent': 'Mozilla/5.0', 'Referer': 'https://webshare.cz'}

MODE = {'refresh': False, 'offline': False}
STATS = {'live': 0, 'cached': 0, 'queries': []}


# ---------- Webshare with cache ----------

class _Response:
    def __init__(self, content):
        self.content = content
        self.status_code = 200


def _token():
    return kodi_stubs.Addon().getSetting('token')


def cached_api_call(fnct, data, addon_for_vip_check=None):
    """Drop-in for WebshareClient.api_call; caches everything except the token."""
    key_data = {k: v for k, v in sorted(data.items()) if k != 'wst'}
    if fnct == 'search':
        STATS['queries'].append(data.get('what'))
    key = hashlib.sha1(json.dumps([fnct, key_data], ensure_ascii=False).encode('utf-8')).hexdigest()[:20]
    path = os.path.join(CACHE_DIR, '{}_{}.xml'.format(fnct, key))
    if os.path.exists(path) and not MODE['refresh']:
        STATS['cached'] += 1
        with open(path, 'rb') as fh:
            return _Response(fh.read())
    if MODE['offline']:
        return _Response(b'<response><status>FATAL</status><message>offline cache miss</message></response>')
    body = dict(data, wst=data.get('wst') or _token())
    req = urllib.request.Request(API + fnct + '/', data=urllib.parse.urlencode(body).encode(), headers=HEADERS)
    content = urllib.request.urlopen(req, timeout=30).read()
    STATS['live'] += 1
    time.sleep(0.3)
    if fnct == 'search':
        os.makedirs(CACHE_DIR, exist_ok=True)
        with open(path, 'wb') as fh:
            fh.write(content)
        with open(os.path.join(CACHE_DIR, 'index.jsonl'), 'a', encoding='utf-8') as fh:
            fh.write(json.dumps({'file': os.path.basename(path), 'query': key_data}, ensure_ascii=False) + '\n')
    return _Response(content)


def load_addon(overrides):
    kodi_stubs.install(overrides)
    from wcs.webshare import WebshareClient
    WebshareClient.api_call = cached_api_call
    WebshareClient.revalidate = lambda addon, popinfo=None, *a, **k: _token() or 'offline'
    from wcs.ui.dialogs import DialogWcsSearch
    DialogWcsSearch.chip_strip = lambda values: json.dumps([v for v in values if v and v != 'N/A'],
                                                          ensure_ascii=False)
    DialogWcsSearch.SearchDialog._layout_info_column = lambda self: None
    if MODE['offline']:
        from wcs.search import title_aliases
        title_aliases._from_tmdb = lambda *a, **k: ''
    return DialogWcsSearch


QUALITY_SETTING = {1: '4K', 2: '1080p', 3: '720p', 4: '480p', 5: '360p', 6: 'SD'}


def preferred_quality():
    return QUALITY_SETTING.get(kodi_stubs.Addon().getSettingInt('autoplay_preferred_quality'))


# ---------- cases ----------

def _strm_params(path):
    try:
        url = open(path, encoding='utf-8').read().strip()
        return dict(urllib.parse.parse_qsl(urllib.parse.urlsplit(url).query))
    except OSError:
        return {}


def library_cases(per_show=4, movies=True, series=True):
    db = sqlite3.connect(DB_PATH)
    cases = []
    if movies:
        for title, original, premiered, path, fname in db.execute(
                'select c00, c16, premiered, strPath, strFileName from movie_view order by c00'):
            params = _strm_params(os.path.join(path, fname))
            cases.append({'type': 'movie', 'title': params.get('title', title), 'original_title': original,
                          'year': params.get('year') or (premiered or '')[:4]})
    if series:
        for show_id, title, original in db.execute('select idShow, c00, c09 from tvshow order by c00'):
            eps = db.execute('select c00, c12, c13, strPath, strFileName from episode_view where idShow=? '
                             'order by cast(c12 as int), cast(c13 as int)', (show_id,)).fetchall()
            if not eps:
                continue
            count = min(per_show, len(eps)) if per_show else len(eps)
            picks = sorted({round(i * (len(eps) - 1) / max(count - 1, 1)) for i in range(count)})
            for i in picks:
                ep_title, season, episode, path, fname = eps[i]
                params = _strm_params(os.path.join(path, fname))
                cases.append({'type': 'episode', 'title': params.get('series_name', title),
                              'original_title': original, 'season': int(season), 'episode': int(episode),
                              'episode_title': ep_title})
    return cases


def _library_original(kind, title):
    method = 'VideoLibrary.GetTVShows' if kind == 'episode' else 'VideoLibrary.GetMovies'
    answer = json.loads(kodi_stubs._json_rpc(json.dumps({'method': method, 'params': {'filter': {'value': title}}})))
    items = list((answer.get('result') or {}).values())
    return items[0][0].get('originaltitle', '') if items and items[0] else ''


def _library_original(kind, title):
    method = 'VideoLibrary.GetTVShows' if kind == 'episode' else 'VideoLibrary.GetMovies'
    answer = json.loads(kodi_stubs._json_rpc(json.dumps({'method': method, 'params': {'filter': {'value': title}}})))
    items = list((answer.get('result') or {}).values())
    return items[0][0].get('originaltitle', '') if items and items[0] else ''


def case_id(case):
    if case['type'] == 'episode':
        return '{} S{:02d}E{:02d}'.format(case['title'], case['season'], case['episode'])
    return '{} ({})'.format(case['title'], case.get('year', ''))


def dialog_kwargs(case):
    """Same arguments the library callers pass (LibraryManager / utils.search_and_select_movie_file)."""
    if case['type'] == 'episode':
        return dict(search_query='{} S{:02d}E{:02d}'.format(case['title'], case['season'], case['episode']),
                    movie_title=case['title'], is_episode=True, series_name=case['title'],
                    season_number=case['season'], episode_number=case['episode'],
                    episode_title=case.get('episode_title', ''), from_library=True)
    return dict(search_query='{} {}'.format(case['title'], case.get('year', '')).strip(),
                movie_title=case['title'], movie_year=str(case.get('year', '')), is_episode=False)


# ---------- pipelines ----------

def run_dialog(D, case):
    """What the dialog list shows (control 50 rows), in order."""
    dlg = D.SearchDialog('search/DialogWcsSearch.xml', kodi_stubs.ADDON_DIR, **dialog_kwargs(case))
    dlg.onInit()
    rows = []
    for item in dlg.getControl(50).items:
        rows.append({'name': item.getProperty('filename'), 'ident': item.getProperty('ident'),
                     'provider': item.getProperty('provider'),
                     'chips': json.loads(item.getProperty('chips_image') or '[]'),
                     'quality_chip': item.getProperty('quality_raw'), 'lang_chip': item.getProperty('language'),
                     'size': _size_of(dlg.search_results, item.getProperty('ident')),
                     'match': _match_of(dlg.search_results, item.getProperty('ident'))})
    return rows


def _size_of(results, ident):
    for r in results:
        if r.get('ident') == ident:
            return r.get('size')
    return 0


def _match_of(results, ident):
    for r in results:
        if r.get('ident') == ident:
            return r.get('match', '')
    return ''


def run_autoplay(D, case):
    """What autoplay / autoplay_first_item would play (no dialog)."""
    kwargs = dialog_kwargs(case)
    picked = D.show_search_dialog(autoplay_request=True, **kwargs)
    return picked or {}


def evaluate(D, case, show=0):
    STATS['queries'] = []
    rows = run_dialog(D, case)
    auto = run_autoplay(D, case)
    preferred = preferred_quality()
    verdict = oracle.judge(rows, case, preferred)
    auto_row = {'name': auto.get('name', ''), 'size': 0}
    for r in rows:
        if r['ident'] == auto.get('ident'):
            auto_row = r
    auto_verdict = oracle.judge([auto_row] + [r for r in rows if r is not auto_row], case, preferred) if auto else \
        {'verdict': 'EMPTY', 'why': 'autoplay picked nothing'}
    chip_issues = []
    for i, r in enumerate(rows[:10]):
        for issue in oracle.chip_mismatches(r['name'], r['size'], r['quality_chip'], r['lang_chip']):
            chip_issues.append('#{} {}'.format(i + 1, issue))
    result = {'id': case_id(case), 'case': case, 'queries': list(dict.fromkeys(STATS['queries'])),
              'rows': rows, 'verdict': verdict, 'autoplay': auto_row.get('name', ''),
              'autoplay_verdict': auto_verdict, 'autoplay_same': bool(rows) and auto.get('ident') == rows[0]['ident'],
              'chip_issues': chip_issues}
    print_result(result, show)
    return result


# ---------- output ----------

def _short(text, width):
    return text if len(text) <= width else text[:width - 1] + '…'


def print_result(res, show):
    v, a = res['verdict'], res['autoplay_verdict']
    top = res['rows'][0]['name'] if res['rows'] else '-'
    flag = '' if res['autoplay_same'] else '  AUTOPLAY≠TOP: ' + _short(res['autoplay'], 60)
    print('{:<8} {:<34} {:<70} {}{}'.format(v['verdict'], _short(res['id'], 34), _short(top, 70), v['why'], flag))
    if a['verdict'] != v['verdict'] and not res['autoplay_same']:
        print('         autoplay: {} {}'.format(a['verdict'], a['why']))
    if show:
        print('         queries: {} | rows: {} | valid: {}'.format(res['queries'], len(res['rows']), v.get('valid')))
        for i, r in enumerate(res['rows'][:show]):
            p = oracle.parse(r['name'], r['size'])
            ok, why = oracle.matches(p, res['case'])
            why = '{}{}'.format(why or 'ok', r.get('match', ''))
            print('   {:>3} {:<5} {:<6} {:<7} {:>6}MB {:<40} {}'.format(
                i + 1, why, p['quality'], p['lang'], p['size_mb'],
                _short(' · '.join(r['chips']), 40), _short(r['name'], 90)))
        for issue in res['chip_issues'][:show]:
            print('         chip: ' + issue)


def summary(results):
    counts = {}
    for r in results:
        counts[r['verdict']['verdict']] = counts.get(r['verdict']['verdict'], 0) + 1
    auto_diff = sum(1 for r in results if not r['autoplay_same'])
    chips = sum(len(r['chip_issues']) for r in results)
    fails = sum(1 for r in results if r['verdict']['verdict'] not in ('OK', 'SIZE'))
    print('\nFAIL {} / {} cases (SIZE = warning only)'.format(fails, len(results)))
    print('{} cases: {} | autoplay≠top: {} | chip mismatches (top10): {} | webshare live/cached: {}/{}'.format(
        len(results), ', '.join('{} {}'.format(k, v) for k, v in sorted(counts.items())), auto_diff, chips,
        STATS['live'], STATS['cached']))
    os.makedirs(OUT_DIR, exist_ok=True)
    path = os.path.join(OUT_DIR, 'report_{}.json'.format(time.strftime('%Y%m%d_%H%M%S')))
    with open(path, 'w', encoding='utf-8') as fh:
        json.dump(results, fh, ensure_ascii=False, indent=1)
    print('report: ' + os.path.relpath(path, kodi_stubs.ADDON_DIR))


def diff(old_path, new_path):
    old = {r['id']: r for r in json.load(open(old_path, encoding='utf-8'))}
    new = {r['id']: r for r in json.load(open(new_path, encoding='utf-8'))}
    changed = 0
    for cid, n in new.items():
        o = old.get(cid)
        if not o:
            continue
        o_top = o['rows'][0]['name'] if o['rows'] else '-'
        n_top = n['rows'][0]['name'] if n['rows'] else '-'
        if o_top == n_top and o['verdict']['verdict'] == n['verdict']['verdict']:
            continue
        changed += 1
        print('{:<34} {:>8} -> {:<8} {}'.format(_short(cid, 34), o['verdict']['verdict'], n['verdict']['verdict'],
                                               n['verdict']['why']))
        print('    was: ' + _short(o_top, 110))
        print('    now: ' + _short(n_top, 110))
    for label, data in (('old', old), ('new', new)):
        fails = sum(1 for r in data.values() if r['verdict']['verdict'] not in ('OK', 'SIZE'))
        chips = sum(len(r['chip_issues']) for r in data.values())
        print('{}: FAIL {} / {}, chip mismatches {}'.format(label, fails, len(data), chips))
    print('changed tops: {}'.format(changed))


def raw_query(text):
    kodi_stubs.install({})
    resp = cached_api_call('search', {'what': text, 'category': 'video', 'limit': 50, 'offset': 0})
    xml = ET.fromstring(resp.content)
    print('status {} total {}'.format(xml.findtext('status'), xml.findtext('total')))
    for f in xml.iter('file'):
        p = oracle.parse(f.findtext('name') or '', f.findtext('size'))
        print('{:>6}MB {:<6} {:<7} +{}/-{} {}'.format(p['size_mb'], p['quality'], p['lang'],
                                                       f.findtext('positive_votes'), f.findtext('negative_votes'),
                                                       f.findtext('name')))


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument('cmd', choices=['library', 'episode', 'movie', 'query', 'diff'])
    ap.add_argument('args', nargs='*')
    ap.add_argument('--original', default='')
    ap.add_argument('--per-show', type=int, default=4, help='episodes per show (0 = all)')
    ap.add_argument('--movies-only', action='store_true')
    ap.add_argument('--series-only', action='store_true')
    ap.add_argument('--show', type=int, default=0, help='print the top N rows per case')
    ap.add_argument('--refresh', action='store_true')
    ap.add_argument('--offline', action='store_true')
    ap.add_argument('--kraska', action='store_true', help='keep KRA.sk enabled (not cached, live SC calls)')
    ap.add_argument('--set', action='append', default=[], metavar='KEY=VALUE')
    opts = ap.parse_args()
    MODE.update(refresh=opts.refresh, offline=opts.offline)

    if opts.cmd == 'query':
        return raw_query(' '.join(opts.args))
    if opts.cmd == 'diff':
        return diff(*opts.args[:2])

    overrides = {'webshare_enabled': 'true', 'autoplay_enabled_global': 'true', 'autoplay_first_item': 'false'}
    if not opts.kraska:
        overrides['kraska_enabled'] = 'false'
    overrides.update(dict(s.split('=', 1) for s in opts.set))
    D = load_addon(overrides)
    if opts.cmd in ('episode', 'movie') and not opts.original:
        opts.original = _library_original(opts.cmd, opts.args[0])
    if opts.original:
        kodi_stubs.EXTRA_ORIGINAL_TITLES[opts.args[0]] = opts.original

    if opts.cmd == 'library':
        cases = library_cases(opts.per_show, not opts.series_only, not opts.movies_only)
    elif opts.cmd == 'episode':
        title, season, episode = opts.args[0], int(opts.args[1]), int(opts.args[2])
        cases = [{'type': 'episode', 'title': title, 'original_title': opts.original,
                  'season': season, 'episode': episode}]
    else:
        cases = [{'type': 'movie', 'title': opts.args[0], 'original_title': opts.original,
                  'year': opts.args[1] if len(opts.args) > 1 else ''}]
    show = opts.show or (20 if opts.cmd in ('episode', 'movie') else 0)
    results = [evaluate(D, case, show) for case in cases]
    summary(results)


if __name__ == '__main__':
    main()
