"""Map complete capture families to the unchanged 44-criterion screenshot specification.

Preparation never changes board acceptance or invents visual observations. Final reviewer
receipts are joined by native PNG SHA, retaining every exact-pixel alias context.
"""
from pathlib import Path
import hashlib, json, re, datetime

root = Path(__file__).resolve().parents[1]
plan = root.parents[1] / 'plans/rita-journey-20261001'
master = json.loads((root / 'evidence/inspection-post/inspection-manifest.json').read_text())
supplement = json.loads((root / 'evidence/luna-post-a-inputs/legacy-supplement.json').read_text())
combined = root / 'evidence/complete-capture-records.json'
records = (json.loads(combined.read_text())['records'] if combined.exists() else
           master['records'] + [r for r in supplement['records'] if r['source_kind'] == 'outer-marker-and-selected-customer'])
assert len(records) >= 12001
# A fresh complete baseline replaces that page/width's earlier baseline. Interactive
# states replace only the same state, retaining distinct unaffected edge-case proof.
def family(row):
    return (row['asset_id'], row['viewport']['width'], row['source_kind'],
            row.get('state') if row['source_kind'] == 'interactive' else None)
latest_round = {}
for row in records:
    latest_round[family(row)] = row.get('capture_round', 'post')
current_indices = {index for index, row in enumerate(records)
                   if row.get('capture_round', 'post') == latest_round[family(row)]}
spec = json.loads((plan / 'screenshot-evidence.json').read_text())

def ids(*values):
    return lambda r: r['asset_id'] in values

def prefix(*values):
    return lambda r: r['asset_id'].startswith(values)

def union(*conditions):
    return lambda r: any(condition(r) for condition in conditions)

def states(*values):
    return lambda r: r.get('state') in values

def both(*conditions):
    return lambda r: all(condition(r) for condition in conditions)

day = lambda r: re.fullmatch(r'day-\d{2}', r['asset_id']) is not None
expert = lambda r: re.fullmatch(r'rita-\d{2}', r['asset_id']) is not None
daily = union(day, expert)
landing = ids('free-a', 'free-b', 'challenge-a', 'challenge-b')
ads = prefix('ad-')
email = prefix('email-')
baseline = lambda r: r['source_kind'] == 'baseline'
interactive = lambda r: r['source_kind'] == 'interactive'
canvas = ids('canvas')
source = prefix('source-module-')
consultation = ids('consultation', 'app-consultation', 'day-14', 'rita-14')
checkout = union(prefix('checkout-'), ids('confirmation', 'app-index', 'app-overview', 'cta-alternatives'), landing)
flow = ids('free-upload', 'free-receipt', 'free-feedback', 'consent', 'confirmation')

selectors = {
 'one-review-layer': {'outer-comment': union(prefix('legacy-outer-'), both(canvas, interactive), ids('free-a'))},
 'every-existing-asset': {'critique-context': lambda r: r['partition'] == 'legacy'},
 'common-meta-persona': {'persona-targeting': ids('persona', 'acquisition-proposal')},
 'step-cognitive-logic': {'reasoning-beside-asset': union(ids('reader-reasoning'), ads, landing, flow, ids('day-01','day-14','rita-01','rita-14','app-consultation'), prefix('checkout-'), ids('email-enrollment','email-feedback','email-weekly','email-promo-open'))},
 'all-stages-connected': {'journey-overview': union(canvas, ids('review-index','app-overview'))},
 'canvas-readable': {'canvas-control-states': canvas},
 'actual-canvas-template': {'template-output': ids('source-canvas','canvas')},
 'canvas-review-efficiency': {'review-path-alternatives': ids('canvas','review-index','cta-side-by-side')},
 'twenty-real-square-images': {'all-twenty-ads': ads},
 'independent-copy-check': {'ad-to-first-screen': union(ads,landing)},
 'testable-ad-hypotheses': {'ad-angle-comparison': union(ids('ad-test-guide'),ads)},
 'template-and-brand': {'four-pages-sections': union(landing,ids('layout-purpose-map','source-layout-kit','brand-source-reference'))},
 'landing-copy-converts': {'four-heroes': landing, 'six-cta-alternatives': ids('cta-alternatives','cta-side-by-side','free-upload','checkout-challenge-a-ready','checkout-challenge-b-ready')},
 'seven-generated-visuals': {'seven-visual-contexts': union(landing,ids('cta-alternatives'))},
 'brand-section-proof': {'brand-reference-comparison': union(landing,ids('brand-live-mobile','brand-source-reference','cta-alternatives','cta-side-by-side'))},
 'landing-quality-evidence': {'landing-mobile-full-coverage': landing, 'landing-fix-proof': landing},
 'source-wording': {'source-to-lesson': union(source,both(day,baseline))},
 'fifty-six-daily-images': {'four-options-per-day': both(day,states('infographic-a','infographic-b','image-a','image-b'))},
 'usable-mobile-tasks': {'day-task-flow': union(day,ids('app-index','app-overview'))},
 'day-inventory': {'overview-and-day-headers': both(union(day,ids('app-overview')),baseline)},
 'lesson-completeness': {'complete-daily-content': union(source,both(day,baseline))},
 'daily-ux-order': {'daily-sequence': union(both(day,baseline),both(day,states('previous-video-and-feedback','reflection-unsaved','submitted-waiting')))},
 'reflection-state': {'reflection-unanswered-filled': both(day,union(baseline,states('reflection-unsaved','reflection-saved-reloaded','unanswered-score-validation','storage-failed','failed-save-reloaded-unanswered','anna-reflection','boglarka-empty-reflection')))},
 'video-ux-states': {'video-selection-validation': both(day,states('initial-empty','initial-separate-submitted','empty-submit','unsupported-video','corrupt-video','playable-fixture','local-replacement','removed')), 'demo-submit-reload': both(day,states('submitted-waiting','submitted-reloaded'))},
 'participant-feedback-loop': {'participant-feedback-states': both(day,union(baseline,states('submitted-waiting','submitted-reloaded','received','reviewed','previous-video-and-feedback','anna-submitted-and-sent')))},
 'daily-visual-relevance': {'daily-visual-detail': both(day,union(baseline,states('infographic-a','infographic-b','image-a','image-b')))},
 'mobile-app-quality': {'participant-mobile-coverage': union(day,ids('app-index','app-overview'))},
 'day14-to-consultation': {'final-day-consultation': consultation},
 'expert-feedback-loop': {'paired-student-rita': union(daily,canvas)},
 'clear-feedback-copy': {'feedback-full-text': both(daily,union(baseline,states('preview','sent','received','reviewed')))},
 'rita-review-context': {'expert-context-switch': expert},
 'rita-write-send-flow': {'rita-editor-states': union(expert,both(day,states('received','reviewed')))},
 'rita-feedback-content': {'fourteen-feedback-examples': union(both(expert,baseline),source)},
 'rita-mobile-qa': {'expert-mobile-coverage': expert},
 'checkout-handoff': {'checkout-all-branches': checkout},
 'email-and-delivery-continuity': {'email-journey-sequence': union(email,ids('consent','reader-reasoning'))},
 'consultation-completes-journey': {'consultation-purpose': consultation},
 'checkout-demo-continuity': {'paid-cta-to-access': checkout},
 'email-preview-context': {'full-email-ab': union(email,ids('consent'))},
 'consultation-ui-completeness': {'consultation-frontend-states': consultation},
 'funnel-promise-consistency': {'promise-comparison': lambda r: r['partition'] != 'legacy' and r['asset_id'] not in ['source-canvas','source-layout-kit']},
 'complete-review-index': {'morning-index': ids('canvas','review-index','cta-side-by-side')},
 'rita-draft-specific': {'client-email-draft': ids('rita-email-draft')},
 'independent-mobile-qa': {'blind-luna-coverage': lambda r: r['partition'] != 'legacy', 'fresh-fix-pair': union(landing, daily, flow, email)},
}
assert set(selectors) == set(spec['criteria'])
groups = []
for criterion, brief in spec['criteria'].items():
    assert set(selectors[criterion]) == {slot['id'] for slot in brief['slots']}
    for slot in brief['slots']:
        indices = [index for index,row in enumerate(records)
                   if index in current_indices and selectors[criterion][slot['id']](row)]
        historical = [index for index,row in enumerate(records)
                      if index not in current_indices and selectors[criterion][slot['id']](row)]
        assert indices, (criterion,slot['id'])
        groups.append({'criterion':criterion,'slot':slot['id'],'title':slot['title'],
                       'required_coverage':slot['coverage'],'record_indices':indices,
                       'asset_ids':sorted({records[i]['asset_id'] for i in indices}),
                       'widths':sorted({records[i]['viewport']['width'] for i in indices}),
                       'states':sorted({records[i].get('state',records[i]['type']) for i in indices}),
                       'historical_record_indices':historical,
                       'actual_independent_observations_attached':False,
                       'before_after_required':slot['id'] in ['landing-fix-proof','fresh-fix-pair']})
assert len(groups) == 48
payload = {'generated_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),
           'criterion_count':44,'group_count':48,'record_count':len(records),
           'current_contexts':len(current_indices),'historical_contexts':len(records)-len(current_indices),
           'scope':'Capture mapping preparation only. No acceptance states or independent observations are changed.',
           'groups':groups}
out = root/'evidence/criterion-coverage-preparation.json'
out.write_text(json.dumps(payload,ensure_ascii=False,indent=2))
print(json.dumps({'criteria':44,'groups':48,'records':len(records),'criterion_states_changed':False,'output':str(out)},indent=2))
