# -*- coding: utf-8 -*-
"""Reports of played games: report.md + summary.json per run, suite overview, comparison of two runs."""
import json
import os

LETTERS = 'ABCD'
ICON = {'error': 'ERR', 'warn': 'WARN', 'info': 'info'}


def _clip(text, size=160):
    text = ' '.join(str(text or '').split())
    return text if len(text) <= size else text[:size - 1] + '…'


def _options(question):
    options = question.get('options') or {}
    if isinstance(options, list):
        options = dict(zip(LETTERS, options))
    return options


def summary(meta, analysis, steps, outcome):
    totals = analysis['totals']
    waits = {s['step']: s['wait_ms'] for s in steps}
    return {
        'meta': meta, 'outcome': outcome, 'totals': totals,
        'verdict': 'FAIL' if totals['errors'] else ('WARN' if totals['warnings'] else 'OK'),
        'waits_ms': waits,
        'llm_ms_by_kind': _by_kind(analysis['llm']),
        'letters': analysis.get('letters', []),
        'fit': analysis.get('fit', {}),
        'issues': analysis['issues'],
    }


def _by_kind(llm):
    result = {}
    for entry in llm:
        result.setdefault(entry.get('kind') or entry.get('purpose'), []).append(entry.get('ms') or 0)
    return {kind: {'n': len(values), 'avg': int(sum(values) / len(values)), 'max': max(values)}
            for kind, values in result.items()}


def write(run_dir, meta, analysis, steps, outcome):
    data = summary(meta, analysis, steps, outcome)
    with open(os.path.join(run_dir, 'summary.json'), 'w', encoding='utf-8') as handle:
        json.dump(data, handle, ensure_ascii=False, indent=1)
    with open(os.path.join(run_dir, 'steps.json'), 'w', encoding='utf-8') as handle:
        json.dump(steps, handle, ensure_ascii=False, indent=1)
    with open(os.path.join(run_dir, 'report.md'), 'w', encoding='utf-8') as handle:
        handle.write(markdown(data, analysis, steps))
    return data


def markdown(data, analysis, steps):
    meta, totals = data['meta'], data['totals']
    lines = ['# Milionář — {} ({})'.format(meta.get('title'), meta.get('source')), '',
             '**{}** · {} · {} / {} · design {}'.format(data['verdict'], data['outcome'], meta.get('provider', '-'),
                                                      meta.get('model', '-'), meta.get('design', '-')),
             '',
             '| LLM volání | tokeny in/out (cache) | cena | čas LLM | chyby / varování |',
             '|---|---|---|---|---|',
             '| {} | {} / {} ({}) | ${:.4f} | {:.1f} s | {} / {} |'.format(
                 totals['llm_calls'], totals['tokens_in'], totals['tokens_out'], totals['tokens_cached'],
                 totals['cost_usd'], totals['llm_ms'] / 1000.0, totals['errors'], totals['warnings']), '']
    if meta.get('overrides'):
        lines += ['Prompt overrides: `{}`'.format(json.dumps(meta['overrides'], ensure_ascii=False)), '']
    if data['issues']:
        lines += ['## Nálezy', '']
        for item in data['issues']:
            where = 'kolo {} · '.format(item['round']) if item.get('round') else ''
            lines.append('- **{}** `{}` {}{}'.format(ICON[item['severity']], item['code'], where, item['message']))
        lines.append('')
    lines += ['## Čekání hráče', '', '| krok | obrazovka | ms |', '|---|---|---|']
    lines += ['| {} | {} → {} | {} |'.format(s['step'], s.get('from'), s.get('screen'), s['wait_ms']) for s in steps]
    lines += ['', '## LLM volání', '', '| # | druh | kolo | ms | in | out | cache | $ |', '|---|---|---|---|---|---|---|---|']
    for entry in analysis['llm']:
        lines.append('| {} | {} | {} | {} | {} | {} | {} | {:.4f} |'.format(
            entry['call'], entry.get('kind'), entry.get('round') or '', entry.get('ms', ''), entry.get('in', ''),
            entry.get('out', ''), entry.get('cached', ''), entry.get('cost', 0.0)) + (
            ' ERR {}'.format(_clip(entry['error'], 80)) if entry.get('error') else ''))
    welcome = analysis.get('welcome') or {}
    lines += ['', '## Průběh', '']
    if welcome:
        lines += ['**Uvítání:** {}'.format(_clip(welcome.get('moderator_welcome'), 400)),
                  '', '**Pravidla:** {}'.format(_clip(welcome.get('rules_summary'), 300)), '']
    for number in sorted(analysis['rounds']):
        entry = analysis['rounds'][number]
        question = entry.get('question') or {}
        options = _options(question)
        correct = str(question.get('correct', '')).upper()
        chosen = entry.get('chosen')
        lines.append('### Kolo {}{}'.format(number, '' if entry.get('used', True) else ' (předgenerováno, nehráno)'))
        if question.get('moderator_intro'):
            lines.append('_{}_'.format(_clip(question['moderator_intro'], 300)))
        lines.append('')
        lines.append('**{}**'.format(question.get('question', '')))
        for letter in LETTERS:
            marks = (' ✔' if letter == correct else '') + (' ← hráč' if chosen is not None and
                                                             LETTERS[chosen] == letter else '')
            lines.append('- {}: {}{}'.format(letter, options.get(letter, ''), marks))
        if question.get('explanation'):
            lines.append('')
            lines.append('Vysvětlení: {}'.format(_clip(question['explanation'], 400)))
        if entry.get('tip'):
            lines.append('Tip: {}'.format(_clip(entry['tip'], 300)))
        feedback = entry.get('feedback') or {}
        if feedback:
            lines.append('Výsledek: {} — {}'.format(_clip(feedback.get('result_announcement'), 300),
                                                    _clip(feedback.get('transition_text'), 200)))
        lines.append('')
    return '\n'.join(lines) + '\n'


def console(data, run_dir):
    totals = data['totals']
    meta = data['meta']
    print('{verdict}  {outcome}  {title} [{source}] {provider}/{model}'.format(
        verdict=data['verdict'], outcome=data['outcome'], title=meta.get('title'), source=meta.get('source'),
        provider=meta.get('provider', '-'), model=meta.get('model', '-')))
    print('  LLM {n}× {s:.1f}s  tokens {i}/{o} (cache {c})  ${cost:.4f}  errors {e}  warnings {w}'.format(
        n=totals['llm_calls'], s=totals['llm_ms'] / 1000.0, i=totals['tokens_in'], o=totals['tokens_out'],
        c=totals['tokens_cached'], cost=totals['cost_usd'], e=totals['errors'], w=totals['warnings']))
    for item in data['issues']:
        if item['severity'] != 'info':
            print('  {} {}{}: {}'.format(ICON[item['severity']], 'r{} '.format(item['round']) if item.get('round')
                                         else '', item['code'], _clip(item['message'], 140)))
    print('  report: ' + os.path.join(run_dir, 'report.md'))


def load(run_dir):
    with open(os.path.join(run_dir, 'summary.json'), encoding='utf-8') as handle:
        return json.load(handle)


def suite(results, out_dir):
    """Overview of several runs (one per title)."""
    lines = ['# Milionář — sada {} her'.format(len(results)), '',
             '| titul | výsledek | verdikt | chyby | varování | LLM s | $ | report |', '|---|---|---|---|---|---|---|---|']
    codes = {}
    for run_dir, data in results:
        totals = data['totals']
        lines.append('| {} | {} | {} | {} | {} | {:.1f} | {:.4f} | {} |'.format(
            data['meta'].get('title'), data['outcome'], data['verdict'], totals['errors'], totals['warnings'],
            totals['llm_ms'] / 1000.0, totals['cost_usd'], os.path.relpath(os.path.join(run_dir, 'report.md'), out_dir)))
        for item in data['issues']:
            codes.setdefault((item['severity'], item['code']), 0)
            codes[(item['severity'], item['code'])] += 1
    total_cost = sum(d['totals']['cost_usd'] for _, d in results)
    lines += ['', 'Celkem ${:.4f}'.format(total_cost), '', '## Nálezy podle druhu', '']
    lines += ['- {} `{}` × {}'.format(ICON[s], c, n) for (s, c), n in sorted(codes.items(), key=lambda x: -x[1])]
    path = os.path.join(out_dir, 'suite.md')
    with open(path, 'w', encoding='utf-8') as handle:
        handle.write('\n'.join(lines) + '\n')
    return path


def compare(a_dir, b_dir):
    """Side by side metrics of two runs (or suites of runs: the folders' summary.json files)."""
    a, b = _collect(a_dir), _collect(b_dir)

    def metric(rows, key):
        values = [r['totals'][key] for r in rows]
        return sum(values) / float(len(values)) if values else 0

    def kind_avg(rows, kind):
        values = [r['llm_ms_by_kind'][kind]['avg'] for r in rows if kind in r.get('llm_ms_by_kind', {})]
        return sum(values) / float(len(values)) if values else 0

    def wait_avg(rows, prefix):
        values = [ms for r in rows for step, ms in r['waits_ms'].items() if step.startswith(prefix)
                  and step != 'question-1']
        return sum(values) / float(len(values)) if values else 0

    rows = [('běhů', len(a), len(b))]
    for key, label in (('errors', 'chyby / běh'), ('warnings', 'varování / běh'), ('cost_usd', '$ / běh'),
                       ('tokens_in', 'tokeny in / běh'), ('tokens_out', 'tokeny out / běh'),
                       ('llm_ms', 'LLM ms / běh')):
        rows.append((label, metric(a, key), metric(b, key)))
    for kind in ('welcome', 'question', 'feedback', 'tip'):
        rows.append(('LLM ms {} (průměr)'.format(kind), kind_avg(a, kind), kind_avg(b, kind)))
    rows.append(('čekání na otázku ms', wait_avg(a, 'question-'), wait_avg(b, 'question-')))
    rows.append(('čekání na výsledek ms', wait_avg(a, 'answer-'), wait_avg(b, 'answer-')))
    for kind in ('intro', 'feedback', 'explanation'):
        def avg_lines(rows, kind=kind):
            values = [n for r in rows for n in r.get('fit', {}).get(kind, [])]
            return sum(values) / float(len(values)) if values else 0
        rows.append(('řádků {} (průměr)'.format(kind), avg_lines(a), avg_lines(b)))
    shared = _overlap(a_dir, b_dir)
    if shared is not None:
        rows.append(('stejné správné odpovědi', shared, shared))
    print('{:<28} {:>14} {:>14} {:>9}'.format('', os.path.basename(a_dir.rstrip('/'))[:14],
                                             os.path.basename(b_dir.rstrip('/'))[:14], 'Δ'))
    for label, x, y in rows:
        delta = '' if not x else '{:+.0%}'.format((y - x) / float(x))
        print('{:<28} {:>14} {:>14} {:>9}'.format(label, _fmt(x), _fmt(y), delta))


def _answers(folder):
    """title -> set of normalized right answers of the run(s) in folder."""
    import json as _json
    result = {}
    for run_dir in _run_dirs(folder):
        title = load(run_dir)['meta'].get('title')
        with open(os.path.join(run_dir, 'trace.jsonl'), encoding='utf-8') as handle:
            for line in handle:
                event = _json.loads(line)
                if event['kind'] == 'content.result' and event.get('method') == 'question':
                    data = event.get('result') or {}
                    options = data.get('options') or {}
                    right = options.get(str(data.get('correct', '')).upper(), '') if isinstance(options, dict) else ''
                    result.setdefault(title, set()).add(' '.join(str(right).lower().split()))
    return result


def _overlap(a_dir, b_dir):
    """Share of B's right answers that A had for the same title (variety when a title is played again)."""
    a, b = _answers(a_dir), _answers(b_dir)
    common = [t for t in b if t in a]
    if not common:
        return None
    same = sum(len(a[t] & b[t]) for t in common)
    total = sum(len(b[t]) for t in common)
    return same / float(total) if total else None


def _fmt(value):
    if isinstance(value, float) and value < 1:
        return '{:.4f}'.format(value)
    return '{:.0f}'.format(value) if isinstance(value, float) else str(value)


def _run_dirs(folder):
    if os.path.exists(os.path.join(folder, 'summary.json')):
        return [folder]
    return [os.path.join(folder, n) for n in sorted(os.listdir(folder))
            if os.path.exists(os.path.join(folder, n, 'summary.json'))]


def _collect(folder):
    if os.path.exists(os.path.join(folder, 'summary.json')):
        return [load(folder)]
    rows = []
    for name in sorted(os.listdir(folder)):
        path = os.path.join(folder, name)
        if os.path.exists(os.path.join(path, 'summary.json')):
            rows.append(load(path))
    if not rows:
        raise SystemExit('no summary.json in ' + folder)
    return rows
