#!/usr/bin/env python3
"""What 'This ad has multiple versions' is in a Meta ad-library response, per route. Reads the Meta
receipts written by scripts/run-ads-cohort.py (files named meta-*.json) in one or more run folders and,
per run and route, counts the distinct ads by display format, how many dynamic-creative (DCO) ads carry
their versions as snapshot cards and how many distinct texts, headlines, images and versions (text + headline + asset) those cards hold,
how many ads carry a collation count above 1 (several ad ids sharing one creative and text) and how
many of those ids came back in the same response. Across routes it compares the version and collation
counts for ad ids seen by more than one route, and across runs the version count of ad ids seen twice.

Usage: scripts/check-meta-versions.py --out evidence/ads-version-checks/YYYY-MM-DD RUN_DIR [RUN_DIR ...]
Writes summary.json and ads.csv (one row per distinct ad per run and route). No provider call is made."""
import argparse, collections, csv, glob, json, os

def ads_of(body):
    if isinstance(body, list): return body
    if isinstance(body, dict): return body.get('results') or body.get('searchResults') or []
    return []
def pick(d, *names):
    for k in names:
        v = d.get(k) if isinstance(d, dict) else None
        if v not in (None, ''): return v
    return None
def text(v): return (v.get('text') if isinstance(v, dict) else v) or ''
def media(c):
    return pick(c, 'original_image_url', 'originalImageUrl', 'resized_image_url', 'resizedImageUrl', 'video_hd_url', 'videoHdUrl', 'video_sd_url', 'videoSdUrl') or ''
def asset_id(url):  # the path segment is stable across fetches; the query string is a signed expiry
    return url.split('?')[0].rsplit('/', 1)[-1] if url else ''

def main():
    ap = argparse.ArgumentParser(); ap.add_argument('--out', required=True); ap.add_argument('run_dirs', nargs='+')
    a = ap.parse_args(); os.makedirs(a.out, exist_ok=True)
    rows = []
    for run_dir in a.run_dirs:
        run = os.path.basename(run_dir.rstrip('/')); seen = set()
        for f in sorted(glob.glob(os.path.join(run_dir, 'meta-*.json'))):
            r = json.load(open(f)); route = r['_run']['label'].rsplit('-', 1)[0]; adv = r['_run']['adv']
            ads = ads_of(r['response'])
            in_file = collections.Counter(pick(ad, 'collation_id', 'collationId') for ad in ads)
            for ad in ads:
                aid = pick(ad, 'ad_archive_id', 'adArchiveID', 'adArchiveId')
                if not aid or (route, aid) in seen: continue
                seen.add((route, aid)); snap = ad.get('snapshot') or {}; cards = snap.get('cards') or []
                cid = pick(ad, 'collation_id', 'collationId'); cc = pick(ad, 'collation_count', 'collationCount')
                rows.append({'run': run, 'route': route, 'advertiser': adv, 'ad_id': aid, 'display_format': pick(snap, 'display_format', 'displayFormat') or '',
                             'cards': len(cards), 'distinct_card_texts': len({text(c.get('body')).strip() for c in cards if text(c.get('body')).strip()}),
                             'distinct_card_headlines': len({(c.get('title') or '').strip() for c in cards if (c.get('title') or '').strip()}),
                             'distinct_card_media': len({asset_id(media(c)) for c in cards if media(c)}),
                             'distinct_card_versions': len({(text(c.get('body')).strip(), (c.get('title') or '').strip(), asset_id(media(c))) for c in cards}),
                             'collation_id': cid or '', 'collation_count': '' if cc is None else cc, 'collation_ids_in_same_response': in_file[cid] if cid else ''})
    summary = {'runs': [os.path.basename(d.rstrip('/')) for d in a.run_dirs], 'by_run_route': [], 'cross_route': {}, 'across_runs': {}}
    groups = collections.defaultdict(list)
    for x in rows: groups[(x['run'], x['route'])].append(x)
    for (run, route), xs in sorted(groups.items()):
        dco = [x for x in xs if x['display_format'] == 'DCO']; multi = [x for x in xs if isinstance(x['collation_count'], int) and x['collation_count'] > 1]
        summary['by_run_route'].append({
            'run': run, 'route': route, 'distinct_ads': len(xs), 'by_display_format': dict(collections.Counter(x['display_format'] or 'none' for x in xs).most_common()),
            'dco_ads': len(dco), 'dco_with_cards': sum(1 for x in dco if x['cards']), 'dco_cards_median': sorted(x['cards'] for x in dco)[len(dco) // 2] if dco else 0,
            'dco_cards_max': max((x['cards'] for x in dco), default=0), 'dco_cards_distribution': dict(sorted(collections.Counter(x['cards'] for x in dco).items())),
            'dco_with_more_than_one_text': sum(1 for x in dco if x['distinct_card_texts'] > 1), 'dco_with_more_than_one_headline': sum(1 for x in dco if x['distinct_card_headlines'] > 1),
            'dco_with_more_than_one_media': sum(1 for x in dco if x['distinct_card_media'] > 1),
            'dco_distinct_versions_median': sorted(x['distinct_card_versions'] for x in dco)[len(dco) // 2] if dco else 0,
            'dco_distinct_versions_distribution': dict(sorted(collections.Counter(x['distinct_card_versions'] for x in dco).items())),
            'dco_with_repeated_cards': sum(1 for x in dco if x['distinct_card_versions'] < x['cards']),
            'dco_cards_total': sum(x['cards'] for x in dco), 'dco_distinct_versions_total': sum(x['distinct_card_versions'] for x in dco),
            'non_dco_with_cards': dict(collections.Counter(x['display_format'] for x in xs if x['display_format'] != 'DCO' and x['cards'])),
            'collation_count_missing': sum(1 for x in xs if x['collation_count'] == ''), 'collation_count_1': sum(1 for x in xs if x['collation_count'] == 1),
            'collation_count_above_1': len(multi), 'collation_count_max': max((x['collation_count'] for x in multi), default=0),
            'dco_with_collation_above_1': sum(1 for x in multi if x['display_format'] == 'DCO'),
            'multi_ads_whose_group_came_back_whole': sum(1 for x in multi if x['collation_ids_in_same_response'] >= x['collation_count']),
            'multi_ads_alone_in_response': sum(1 for x in multi if x['collation_ids_in_same_response'] == 1)})
    for run in summary['runs']:  # same ad id through more than one route on the same run
        by_id = collections.defaultdict(dict)
        for x in rows:
            if x['run'] == run: by_id[x['ad_id']][x['route']] = x
        both = {k: v for k, v in by_id.items() if len(v) > 1}
        summary['cross_route'][run] = {'ad_ids_on_more_than_one_route': len(both),
                                       'same_display_format': sum(1 for v in both.values() if len({x['display_format'] for x in v.values()}) == 1),
                                       'same_card_count': sum(1 for v in both.values() if len({x['cards'] for x in v.values()}) == 1),
                                       'same_collation_count': sum(1 for v in both.values() if len({x['collation_count'] for x in v.values()}) == 1)}
    first, last = summary['runs'][0], summary['runs'][-1]
    a_ = {(x['route'], x['ad_id']): x for x in rows if x['run'] == first}; b_ = {(x['route'], x['ad_id']): x for x in rows if x['run'] == last}
    dco_both = [(a_[k], b_[k]) for k in a_.keys() & b_.keys() if a_[k]['display_format'] == 'DCO' and b_[k]['display_format'] == 'DCO']
    summary['across_runs'] = {'from': first, 'to': last, 'dco_ads_seen_on_both': len(dco_both), 'same_card_count': sum(1 for p, q in dco_both if p['cards'] == q['cards']),
                              'more_cards_later': sum(1 for p, q in dco_both if q['cards'] > p['cards']), 'fewer_cards_later': sum(1 for p, q in dco_both if q['cards'] < p['cards'])}
    json.dump(summary, open(os.path.join(a.out, 'summary.json'), 'w'), indent=1)
    with open(os.path.join(a.out, 'ads.csv'), 'w', newline='') as fh:
        w = csv.DictWriter(fh, fieldnames=list(rows[0].keys())); w.writeheader(); w.writerows(rows)
    for r in summary['by_run_route']: print(r)
    print(summary['cross_route']); print(summary['across_runs'])
if __name__ == '__main__': main()
