# Aggregate retained lantern assessment records without generating or resampling inputs.
import argparse
from collections import Counter
import hashlib
import json
from pathlib import Path

from hearth.quality_assessment import summary


def read(path: Path):
    """Read one explicitly named JSON assessment record."""
    return json.loads(path.read_text())


def write(path: Path, value):
    """Write a replayable derived report, retaining the original input records."""
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(value, indent=2) + '\n')


def measured(rows):
    """Separate finite input success, candidate acceptance and actual sitework costs."""
    result = summary(rows)
    valid = [row for row in rows if row['status'] == 'validated']
    result['rejected_candidates'] = result['candidate_attempts'] - result['accepted_candidates']
    result['input_success_rate'] = result['validated'] / len(rows) if rows else 0
    result['candidate_acceptance_rate'] = result['accepted_candidates'] / result['candidate_attempts'] if result['candidate_attempts'] else 0
    result['explicit_retries'] = sum(row['retries'] for row in rows)
    result['inputs_with_rejected_candidates'] = sum(row['attempts'] > row['accepted_candidates'] for row in rows)
    result['per_input_seconds_range'] = [min(row['seconds'] for row in rows), max(row['seconds'] for row in rows)] if rows else []
    for key in ('excavation', 'replaced_cells', 'new_sitework_cells', 'bearing_fill'):
        values = [row['contact'][key] for row in valid]
        result[key + '_range'] = [min(values), max(values)] if values else []
    result['contact_strategy_instances'] = dict(sorted(Counter(strategy for row in valid for strategy in row['contact']['strategies']).items()))
    return result


def run(seed: int, reports: Path, samples: Path, previews: Path):
    """Derive summaries and manifests from the complete predeclared release batches.

    Args:
        seed: Root seed used by the recorded batch commands.
        reports: Directory containing release batch records and auxiliary audits.
        samples: Directory in which to write the complete-pair replay manifest.
        previews: Directory containing the selected final view manifests.

    Returns:
        An aggregate containing separate supported, stress and visual evidence.
    """
    names = ('core', 'space', 'detail', 'holdout', 'craft', 'new')
    batches = {name: read(reports / 'release' / name / 'results.json') for name in names}
    supported = batches['core'][:88] + sum((batches[name] for name in names[1:]), [])
    stress = batches['core'][88:]
    clients = batches['core'][:24] + sum((batches[name][:8] for name in ('space', 'detail', 'craft', 'new')), [])
    groups = {name: batches['core'][i * 8:(i + 1) * 8] for i, name in enumerate(('home', 'hall', 'high'))}
    groups.update({name: batches[batch][:8] for name, batch in (('roof-home', 'space'), ('garden-house', 'detail'), ('hearth-house', 'craft'), ('lantern-house', 'new'))})
    result = {'version': '1.7.0', 'root_seed': seed, 'supported': measured(supported), 'stress': measured(stress), 'named_clients': measured(clients), 'per_client': {name: measured(rows) for name, rows in groups.items()}, 'batches': {name: measured(rows) for name, rows in batches.items()}}
    result['stress_failures'] = [{key: row[key] for key in ('label', 'inputs', 'status', 'diagnostic', 'attempts', 'candidates')} for row in stress if row['status'] != 'validated']
    observations = [row['ridge_lights'] for row in batches['new'] if row['status'] == 'validated']
    mounts = [mount for row in observations for mount in row['mounts']]
    result['new_ridge_observations'] = {'inputs': len(observations), 'actual_attachments': sum(row['lanterns'] for row in observations), 'observed_lengths': sorted({length for row in observations for length in row['lengths']}), 'current_cell_range': [min(row['current_cells'] for row in observations), max(row['current_cells'] for row in observations)], 'mounts': len(mounts), 'optional_omissions': sum(mount['optional_omission'] for mount in mounts), 'screening_reasons': dict(sorted(Counter(candidate['reason'] for mount in mounts for candidate in mount['screened_out']).items()))}
    selected = [{**row, 'batch': name} for name in names for row in batches[name] if 'artifact' in row]
    additional_path = reports / 'additional-preview.json'
    if additional_path.exists():
        additional = read(additional_path)
        original = next(row for row in batches['new'] if row['label'] == additional['label'])
        if additional['digest'] != original['digest']:
            raise ValueError('Additional visual sample differs from its assessed input')
        selected.append({**additional, 'batch': 'additional', 'duplicates_assessed_input': True})
    result['selected_pair_audits'] = {'pairs': len(selected), **{key: sum(row['audit'][key] for row in selected) for key in ('queries', 'inventories', 'nodes')}}
    write(samples / 'manifest.json', {'root_seed': seed, 'note': 'Additional visual sample repeats an already assessed input and is not added to input denominators.', 'pairs': selected})
    views = [read(path) for path in sorted(previews.glob('*/views.json'))]
    for entry in views:
        for view in entry['views']:
            if not Path(view['image']).is_file():
                raise ValueError('Missing rendered image: ' + view['image'])
    result['renders'] = {'buildings': len(views), 'images': sum(len(entry['views']) for entry in views), 'complete_exteriors': sum(bool(view.get('complete')) for entry in views for view in entry['views']), 'geometry_only_cutaways': sum(bool(view.get('geometry_only_cutaway')) for entry in views for view in entry['views'])}
    write(reports / 'render-manifest.json', views)
    for name in ('cli-audit', 'replay', 'portability'):
        result[name] = read(reports / (name + '.json'))
    baseline = read(reports / 'baseline-hashes.json')
    paths = set(baseline)
    for directory in ('hearth', 'hearth_extensions', 'extension_demo', 'examples', 'test'):
        paths.update(str(path) for path in Path(directory).rglob('*.py'))
    hashes = {path: hashlib.sha256(Path(path).read_bytes()).hexdigest() for path in sorted(paths) if Path(path).is_file()}
    removed = sorted(set(baseline) - set(hashes))
    changed = [path for path in baseline if path in hashes and baseline[path] != hashes[path]]
    old_tests = [path for path in baseline if path.startswith('test/')]
    delta = {'changed': sorted(changed), 'added': sorted(set(hashes) - set(baseline)), 'removed': removed, 'unchanged_existing_test_files': len([path for path in old_tests if hashes.get(path) == baseline[path]]), 'changed_existing_test_files': [path for path in old_tests if hashes.get(path) != baseline[path]], 'kernel_changed': sorted(path for path in changed if path.startswith('hearth/kernel/'))}
    write(reports / 'final-module-hashes.json', hashes)
    write(reports / 'module-delta.json', delta)
    result['source_delta'] = delta
    first = reports / 'first-assessment'
    if all((first / name / 'results.json').exists() for name in names):
        initial = {name: read(first / name / 'results.json') for name in names}
        result['retained_development_assessment'] = {'note': 'Prior to final mounting refinement. Excluded from release denominators.', 'supported': measured(initial['core'][:88] + sum((initial[name] for name in names[1:]), [])), 'stress': measured(initial['core'][88:])}
    write(reports / 'summary.json', result)
    return result


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--seed', type=int, default=0)
    parser.add_argument('--reports', type=Path, default=Path('reports/building-lantern'))
    parser.add_argument('--samples', type=Path, default=Path('samples/building-lantern/release'))
    parser.add_argument('--previews', type=Path, default=Path('previews/building-lantern/release'))
    args = parser.parse_args()
    observed = run(args.seed, args.reports, args.samples, args.previews)
    print(json.dumps({key: observed[key] for key in ('supported', 'stress', 'named_clients', 'selected_pair_audits', 'renders', 'new_ridge_observations', 'source_delta')}, indent=2))
