#!/usr/bin/env python3
"""Rebuild the published data from the included, verified source records.

Usage: python reproduce.py [--output DIRECTORY]
No external packages or network access are needed. This reproduces arithmetic;
it does not reverify the current contents of the cited websites.
"""
from __future__ import annotations
import argparse
import csv
import json
from collections import defaultdict
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
from typing import Any

D = Decimal

def number(value: Any) -> Decimal:
    return D(str(value))

def text(value: Decimal) -> str:
    result = format(value, 'f')
    return result.rstrip('0').rstrip('.') if '.' in result else result

def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
    if not rows:
        raise ValueError(f'No rows for {path.name}')
    with path.open('w', encoding='utf-8', newline='') as handle:
        writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)

def rebuild(source_path: Path, destination: Path) -> dict[str, int]:
    raw = json.loads(source_path.read_text(encoding='utf-8'))
    metadata, models, sources = raw['metadata'], raw['models'], raw['sources']
    date = metadata['verified_on']
    for model in models:
        basis = model['calculation_basis']
        if basis not in {'in', 'mm'}:
            raise ValueError(f'Unknown unit in {model["record_id"]}')
        factor = D('25.4') if basis == 'in' else D(1)
        a = number(model['published_side_1_' + basis]) * factor
        b = number(model['published_side_2_' + basis]) * factor
        if min(a, b) <= 0:
            raise ValueError('Footprint dimensions must be positive')
        model['normalized_long_side_mm'] = text(max(a, b))
        model['normalized_short_side_mm'] = text(min(a, b))
        model['arithmetic_footprint_area_m2'] = text(a * b / 1000000)
        model['verified_published_height_present'] = any(model['published_height_' + unit] is not None for unit in ('mm', 'in'))
    by_id = {model['record_id']: model for model in models}
    if len(by_id) != len(models):
        raise ValueError('Duplicate record IDs')
    groups: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
    for model in models:
        if model['publisher'] == 'EPAL':
            groups[(model['normalized_long_side_mm'], model['normalized_short_side_mm'])].append(model)
    audit = []
    for (a, b), rows in groups.items():
        heights = sorted({number(row['published_height_mm']) for row in rows})
        audit.append(dict(long_side_mm=a, short_side_mm=b, model_count=len(rows), model_ids=';'.join(row['record_id'] for row in rows), models_and_heights='; '.join(f"{row['model']}: {row['published_height_mm']} mm" for row in rows), distinct_height_count=len(heights), heights_mm=';'.join(text(h) for h in heights), height_span_mm=text(max(heights)-min(heights)), source_numbers=';'.join(str(row['source_number']) for row in rows), verified_on=date))
    audit.sort(key=lambda row: (-row['distinct_height_count'], -row['model_count'], -number(row['long_side_mm'])))
    reconciliation = []
    for model_id in ('chep-na', 'chep-au-10001', 'peco-48-40'):
        model = by_id[model_id]
        converted = number(model['published_height_in']) * D('25.4')
        reconciliation.append(dict(record_id=model_id, model=model['model'], published_height_in=model['published_height_in'], published_height_mm=model['published_height_mm'], inch_value_times_25_4_mm=text(converted), arithmetic_minus_published_mm=text(converted-number(model['published_height_mm'])), interpretation='Comparison of published representations; not a measured discrepancy, tolerance, or verified explanation of cause', source_number=model['source_number'], source_url=model['source_url'], verified_on=date))
    comparisons = []
    for aid, bid, label in [('chep-na','epal-euro','48 × 40 in versus 1200 × 800 mm'), ('chep-na','epal-2','48 × 40 in versus 1200 × 1000 mm'), ('epal-6','epal-euro','800 × 600 mm versus 1200 × 800 mm'), ('litco-24-40','chep-na','24 × 40 in versus 48 × 40 in')]:
        a, b = by_id[aid], by_id[bid]
        av, bv = number(a['arithmetic_footprint_area_m2']), number(b['arithmetic_footprint_area_m2'])
        comparisons.append(dict(comparison=label, a_record_id=aid, b_record_id=bid, a_rectangle_area_m2=text(av), b_rectangle_area_m2=text(bv), a_as_percent_of_b=text(av/bv*100), change_from_b_to_a_percent=text((av-bv)/bv*100), denominator='B rectangle area', source_numbers=f"{a['source_number']};{b['source_number']};2", verified_on=date))
    footprints, seen = [], set()
    for model in models:
        key = model['normalized_long_side_mm'], model['normalized_short_side_mm']
        if key in seen:
            continue
        seen.add(key)
        a, b = map(number, key)
        footprints.append(dict(long_side_mm=text(a), short_side_mm=text(b), long_side_cm=text(a/10), short_side_cm=text(b/10), long_side_in=text((a/D('25.4')).quantize(D('.01'), rounding=ROUND_HALF_UP)), short_side_in=text((b/D('25.4')).quantize(D('.01'), rounding=ROUND_HALF_UP)), rectangle_area_m2=text(a*b/1000000), example_record_id=model['record_id'], calculation_basis=model['calculation_basis'], verified_on=date))
    limits = raw['drawing_limits']
    for row in limits:
        if not row['lower_limit_mm'] <= row['nominal_mm'] <= row['upper_limit_mm']:
            raise ValueError('Invalid drawing limits')
    epal_limits = {row['dimension']: row for row in limits if row['model'] == 'EPAL Euro'}
    assert (epal_limits['long side']['lower_limit_mm'], epal_limits['long side']['upper_limit_mm']) == (1200-3, 1200+3)
    assert (epal_limits['short side']['lower_limit_mm'], epal_limits['short side']['upper_limit_mm']) == (800-3, 800+3)
    assert (epal_limits['overall height']['lower_limit_mm'], epal_limits['overall height']['upper_limit_mm']) == (144-0, 144+7)
    checks = dict(model_count=len(models), publisher_count=len({row['publisher'] for row in models}), unique_normalized_footprints=len(seen), models_with_published_height=sum(row['verified_published_height_present'] for row in models), epal_subset_model_count=sum(row['publisher']=='EPAL' for row in models), epal_subset_footprint_count=len(audit), epal_subset_footprints_with_multiple_heights=sum(row['distinct_height_count']>1 for row in audit))
    for field, actual in checks.items():
        if actual != metadata[field]:
            raise ValueError(f'Metadata mismatch for {field}: {actual} versus {metadata[field]}')
    assert number(comparisons[0]['change_from_b_to_a_percent']) == D('29.032')
    assert next(row for row in audit if (row['long_side_mm'], row['short_side_mm']) == ('1200', '1000'))['height_span_mm'] == '24'
    data = dict(metadata=metadata, models=models, epal_height_audit=audit, unit_reconciliation=reconciliation, drawing_limits=limits, area_comparisons=comparisons, footprint_conversions=footprints, local_evidence=raw['local_evidence'], sources=sources)
    destination.mkdir(parents=True, exist_ok=True)
    (destination/f'pallet-dimensions-{date}.json').write_text(json.dumps(data, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
    for prefix, rows in [('pallet-dimensions', models), ('epal-height-audit', audit), ('published-unit-reconciliation', reconciliation), ('published-drawing-limits', limits), ('pallet-footprint-comparisons', comparisons), ('pallet-footprint-conversions', footprints), ('ohio-toledo-source-notes', raw['local_evidence']), ('source-register', sources)]:
        write_csv(destination/f'{prefix}-{date}.csv', rows)
    return checks

if __name__ == '__main__':
    home = Path(__file__).resolve().parent
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--output', type=Path, default=home/'reproduced-data')
    args = parser.parse_args()
    try:
        checks = rebuild(home/'source-records.json', args.output)
    except (OSError, ValueError, KeyError, AssertionError) as exc:
        raise SystemExit(f'Reproduction failed: {exc}') from exc
    print(json.dumps({'status':'passed', **checks}, indent=2))
