#!/usr/bin/env python3
"""Rebuild one EPA efficiency/power graphic using only the archived release.

Requires existing local rsvg-convert and pdftotext for rendering/QA. No network,
browser, font download, install, government logo, or third-party service is used.
"""
from __future__ import annotations

import argparse
import csv
from decimal import Decimal
import hashlib
import html
import io
import json
from pathlib import Path
import shutil
import struct
import subprocess
import xml.etree.ElementTree as ET
import zipfile

OUT = Path(__file__).resolve().parent
DEFAULT_SOURCE = OUT / 'sources'
CSV_NAME = 'epa_all_vehicle_types_1975_2024.csv'
EXPECTED_CSV_SHA = '05c531ccbfd1c274086e4ad42bdf8baddc17244df626b399b33e4a14839f8c94'
EXPECTED_RAW_SHA = '2e6b98e9048eb5e96451ebaa8bc04bcba304c7aa9d5a4a88a7edb1dd42742507'
SOURCE_URL = 'https://www.epa.gov/automotive-trends/explore-automotive-trends-data'
DEFINITIONS_URL = 'https://www.epa.gov/automotive-trends/about-automotive-trends-data'
LICENSE_URL = 'https://edg.epa.gov/EPA_Data_License.htm'
REPORT_PDF_URL = 'https://www.epa.gov/system/files/documents/2026-02/420r26001.pdf'
MPGE_SUBTITLE = 'EVs + plug-in hybrids use gasoline equivalents.'
MPGE_NOTE = 'EPA includes battery-electric and plug-in hybrid vehicles using gasoline-equivalent energy (MPGe) in fleetwide MPG unless otherwise noted; 27.2 MPG is not a gasoline-only average.'
MPGE_CITATION = {'url': REPORT_PDF_URL, 'printedPage': 6, 'footnote': 1, 'pdfPage': 12, 'pdfPageIndex': 11, 'finding': MPGE_NOTE, 'verification': 'Official EPA PDF verified by parent /root; exact source location and finding supplied for this correction.'}
NS = 'http://www.w3.org/2000/svg'
INK, MUTED, BG = '#1d1d1f', '#626266', '#fafafa'
TEAL, GRAY, GRID = '#137c83', '#a4afb9', '#e3e5e8'


def digest(data):
    return hashlib.sha256(data).hexdigest()


def write_csv(name, rows):
    fields = ['model_year', 'regulatory_class', 'vehicle_type', 'production_share', 'real_world_mpg', 'horsepower']
    with (OUT / name).open('w', newline='') as handle:
        writer = csv.DictWriter(handle, fieldnames=fields, lineterminator='\n')
        writer.writeheader()
        writer.writerows({field: row[field] for field in fields} for row in rows)


def load_and_check(source):
    csv_bytes = (source / CSV_NAME).read_bytes()
    assert digest(csv_bytes) == EXPECTED_CSV_SHA, 'Frozen CSV hash changed; stop and review.'
    rows = list(csv.DictReader(io.StringIO(csv_bytes.decode())))
    assert len(rows) == 400
    assert len({(r['regulatory_class'], r['vehicle_type'], r['model_year']) for r in rows}) == 400
    assert 'co2' not in ','.join(rows[0]).lower(), 'Recheck any changed field inventory.'
    selected = [r for r in rows if r['regulatory_class'] == 'All' and r['vehicle_type'] == 'All']
    selected.sort(key=lambda row: int(row['model_year']))
    assert [int(r['model_year']) for r in selected] == list(range(1975, 2025))
    assert all(Decimal(r['production_share']) == 1 for r in selected)
    with zipfile.ZipFile(source / 'us-vehicle-weights.zip') as archive:
        assert archive.read(CSV_NAME) == csv_bytes, 'Loose and zipped CSV differ.'
        raw_bytes = archive.read('epa_raw_response.json')
        assert digest(raw_bytes) == EXPECTED_RAW_SHA, 'Archived raw response changed.'
        raw = json.loads(raw_bytes)
        terms = archive.read('SOURCE_TERMS.txt').decode()
    cube = raw['layout']['result']['qLayout']['qHyperCube']
    labels = [item['qFallbackTitle'] for item in cube['qMeasureInfo']]
    assert labels[1] == 'Real-world MPG' and labels[5] == 'Horsepower (HP)'
    matrices = [row for page in raw['data']['result']['qDataPages'] for row in page['qMatrix']]
    assert len(matrices) == 408
    archived = {(row[0]['qText'], row[1]['qText'], row[2]['qText']): row for row in matrices}
    comparisons = 0
    for row in selected:
        raw_row = archived[('All', 'All', row['model_year'])]
        for field, column in [('real_world_mpg', 4), ('horsepower', 8)]:
            assert Decimal(row[field]) == Decimal(str(raw_row[column]['qNum']))
            comparisons += 1
    assert comparisons == 100
    before, after = selected[0], selected[-1]
    assert before['real_world_mpg'] == '13.0597' and after['real_world_mpg'] == '27.24521'
    assert before['horsepower'] == '137.3346' and after['horsepower'] == '258.0114'
    facts = {}
    for field in ['real_world_mpg', 'horsepower']:
        a, b = Decimal(before[field]), Decimal(after[field])
        facts[field] = {'1975': str(a), '2024': str(b), 'ratio': str(b/a), 'percent_change': str((b/a - 1) * 100)}
    (OUT / 'SOURCE_TERMS.txt').write_text(terms)
    write_csv('chart_data_1975_2024.csv', selected)
    write_csv('plotted_endpoints.csv', [before, after])
    return before, after, facts, {
        'frozenCsvSha256': digest(csv_bytes),
        'frozenRawResponseSha256': digest(raw_bytes),
        'frozenZipSha256': digest((source / 'us-vehicle-weights.zip').read_bytes()),
        'allAnnualMpgAndHorsepowerValuesMatchArchivedQNum': comparisons,
        'looseCsvEqualsZippedCsv': True,
        'allSummaryRows': 400, 'selectedAllAllRows': 50,
        'oneRowPerFinalModelYear1975To2024': True,
        'allSelectedProductionSharesEqualOne': True,
        'overlappingAggregatesNotSummed': True,
        'noCo2FieldRetainedOrInvented': True,
        'sourceMeasureLabels': [labels[1], labels[5]],
    }


def build_svg(before, after):
    parts = [f'<svg xmlns="{NS}" viewBox="0 0 390 700" width="390" height="700" role="img" aria-labelledby="title description">',
        '<title id="title">More horsepower. More miles per gallon.</title>',
        f'<desc id="description">Two separate zero-based comparisons of EPA production-weighted averages for new light-duty vehicles delivered for sale in the United States. Model year 1975 versus final model year 2024: reported horsepower rises from 137.3346 to 258.0114 HP; EPA estimated real-world fuel economy rises from 13.0597 to 27.24521 MPG. {MPGE_NOTE} Labels round to one decimal. The mix of models changes over time; this is not a comparison of the same vehicles or the on-road fleet. Both metrics come directly from the frozen All/All EPA summary. The separate panels use their own units and scales. No emissions, literal fuel-use, technology-adjusted, causal or continuous-growth inference is made. Source: US EPA 2025 Automotive Trends Report, printed page 6 footnote 1 (PDF page 12); summary retrieved 13 October 2026; preliminary 2025 excluded.</desc>',
        f'<rect width="390" height="700" fill="{BG}"/>',
        '<g font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Arial,sans-serif" font-variant-numeric="tabular-nums">']

    def text(x, y, value, size=14.25, color=MUTED, weight=400, anchor='start', role='label'):
        parts.append(f'<text x="{x:.3f}" y="{y:.3f}" fill="{color}" font-size="{size}" font-weight="{weight}" text-anchor="{anchor}" data-role="{role}">{html.escape(str(value))}</text>')

    def line(x1, y1, x2, y2):
        parts.append(f'<line x1="{x1:.3f}" y1="{y1:.3f}" x2="{x2:.3f}" y2="{y2:.3f}" stroke="{GRID}" stroke-width="1"/>')

    text(24, 50, 'More horsepower.', 28, INK, 600, role='headline')
    text(24, 84, 'More miles per gallon.', 28, INK, 600, role='headline')
    text(24, 117, 'New US light-duty vehicles · 1975–2024', 14.25)
    x0, x1 = 83, 309
    panels = [
        ('horsepower', 'Horsepower', 171, [200, 249], 300, [0, 150, 300], 308),
        ('real_world_mpg', 'EPA estimated real-world MPG', 370, [421, 470], 30, [0, 15, 30], 529),
    ]
    for field, heading, heading_y, bar_y, maximum, ticks, axis_y in panels:
        text(24, heading_y, heading, 20 if field == 'horsepower' else 18, INK, 600)
        if field == 'real_world_mpg':
            text(24, heading_y + 23, MPGE_SUBTITLE, 14.25)
        for tick in ticks:
            xx = x0 + tick / maximum * (x1 - x0)
            line(xx, bar_y[0] - 8, xx, bar_y[-1] + 36)
            text(xx, axis_y, tick, 14.25, anchor='middle')
        for row, yy, color in zip([before, after], bar_y, [GRAY, TEAL]):
            value = float(row[field])
            width = value / maximum * (x1 - x0)
            text(24, yy + 21, row['model_year'], 15, INK)
            parts.append(f'<rect x="{x0}" y="{yy}" width="{width:.6f}" height="30" rx="2" fill="{color}" data-metric="{field}" data-year="{row["model_year"]}" data-exact-value="{row[field]}"/>')
            text(x0 + width + 9, yy + 22, f'{value:.1f}', 20, TEAL if row['model_year'] == '2024' else INK, 500, role='value')
    text(24, 574, 'Production-weighted new-vehicle averages.', 14.25)
    text(24, 596, 'Model mix changes; not the same vehicles.', 14.25)
    text(24, 618, 'Final model years; 2025 excluded.', 14.25)
    text(24, 656, 'Source: US EPA · 2025 Automotive Trends Report', 11, role='source')
    text(24, 675, 'Frozen summary · retrieved 13 Oct 2026 · meplar.com', 11, role='source')
    parts.extend(['</g>', '</svg>'])
    return '\n'.join(parts) + '\n'


def render_and_check():
    rsvg, pdftotext = shutil.which('rsvg-convert'), shutil.which('pdftotext')
    assert rsvg and pdftotext, 'Needs existing local librsvg and Poppler; do not install/fetch.'
    svg = OUT / 'chart.svg'
    primary, mobile = OUT / 'chart.png', OUT / 'chart_mobile390.png'
    qa = OUT / '.qa'
    qa.mkdir(exist_ok=True)
    pdf = qa / 'chart_native390.pdf'
    subprocess.run([rsvg, '--width', '1440', '--output', str(primary), str(svg)], check=True)
    subprocess.run([rsvg, '--width', '390', '--output', str(mobile), str(svg)], check=True)
    subprocess.run([rsvg, '--format', 'pdf', '--output', str(pdf), str(svg)], check=True)
    primary_size = struct.unpack('>II', primary.read_bytes()[16:24])
    mobile_size = struct.unpack('>II', mobile.read_bytes()[16:24])
    assert primary_size[0] == 1440 and mobile_size == (390, 700)
    root = ET.parse(svg).getroot()
    assert root.attrib['role'] == 'img' and root.attrib['aria-labelledby'] == 'title description'
    bars = [el for el in root.iter() if el.tag.endswith('rect') and el.attrib.get('data-metric')]
    assert len(bars) == 4
    for node in root.iter():
        if not node.tag.endswith('text'):
            continue
        size = float(node.attrib['font-size'])
        role = node.attrib['data-role']
        assert size >= (16 if role == 'value' else 10 if role == 'source' else 14)
        if role == 'source':
            assert size <= 11
    # Use actual shaped-font boxes from the native librsvg PDF, not character-count estimates.
    doc = ET.fromstring(subprocess.check_output([pdftotext, '-bbox', str(pdf), '-']))
    page = next(el for el in doc.iter() if el.tag.endswith('page'))
    pw, ph = float(page.attrib['width']), float(page.attrib['height'])
    words = [(word.text, tuple(float(word.attrib[k]) for k in ['xMin', 'yMin', 'xMax', 'yMax'])) for word in doc.iter() if word.tag.endswith('word')]
    assert words
    for word, box in words:
        assert box[0] >= 12 and box[2] <= pw - 12, ('Clipped text', word, box)
        assert box[1] >= 12 and box[3] <= ph - 12, ('Clipped text', word, box)
    for i, (aw, a) in enumerate(words):
        for bw, b in words[i + 1:]:
            overlap_x, overlap_y = min(a[2], b[2]) - max(a[0], b[0]), min(a[3], b[3]) - max(a[1], b[1])
            assert overlap_x <= .25 or overlap_y <= .25, ('Overlapping text', aw, bw)
    return {
        'primaryPngPixels': list(primary_size), 'actualMobileQaPixels': list(mobile_size),
        'essentialLabelMinimumPxAt390': 14.25, 'numericValuePxAt390': 20,
        'sourceCreditPxAt390': 11, 'essentialLabelMinimumPxAt1440': 14.25 * 1440/390,
        'numericValuePxAt1440': 20 * 1440/390, 'sourceCreditPxAt1440': 11 * 1440/390,
        'zeroBasedBars': True, 'separateNativeUnitScales': True,
        'renderedTextInsideCanvasWithMargin': True, 'renderedTextDoesNotOverlap': True,
        'renderer': 'Local librsvg / rsvg-convert and Poppler pdftotext',
        'browserUsed': False, 'networkUsed': False, 'additionalCost': 0,
    }


def main(source):
    before, after, facts, source_checks = load_and_check(source)
    (OUT / 'chart.svg').write_text(build_svg(before, after))
    render_checks = render_and_check()
    validation = {'numericChecksPassed': True, 'sourceChecks': source_checks, 'exactEndpointFacts': facts, 'renderChecks': render_checks,
        'independentContentQa': {'reviewer': '/root/content_qa', 'result': 'PASS', 'scope': 'Independently matched all 100 annual MPG/HP values to archived qMatrix qNum; checked final-year selection, source labels, endpoints and percent changes.', 'limitation': 'EPA report printed page 6 footnote 1 verifies the BEV/PHEV gasoline-equivalent convention. This chart does not reproduce the full adjustment formula or derive literal gasoline use, CO2 or other emissions.'},
        'mpgDefinitionVerification': MPGE_CITATION}
    validation['deliverableSha256'] = {p.name: digest(p.read_bytes()) for p in [OUT / 'chart.svg', OUT / 'chart.png', OUT / 'chart_mobile390.png', OUT / 'chart_data_1975_2024.csv', OUT / 'plotted_endpoints.csv']}
    (OUT / 'VALIDATION.json').write_text(json.dumps(validation, indent=2) + '\n')
    (OUT / 'SOURCE_REFERENCE.json').write_text(json.dumps({'sourceTitle': 'US EPA, 2025 Automotive Trends Report', 'landingPage': SOURCE_URL, 'definitions': DEFINITIONS_URL, 'reportPdf': REPORT_PDF_URL, 'mpgDefinitionCitation': MPGE_CITATION, 'mpgInterpretation': MPGE_NOTE, 'dataLicense': LICENSE_URL, 'retrieved': '2026-10-13', 'sourceDirectoryReadOnly': str(source), 'frozenSummaryFile': CSV_NAME, 'frozenArchive': 'us-vehicle-weights.zip', 'selectedEntity': {'regulatory_class': 'All', 'vehicle_type': 'All'}, 'transformations': ['Select final model years 1975–2024 from the already-cleaned frozen summary.', 'Use the All / All EPA aggregate directly; do not sum overlapping category aggregates.', 'Retain exact source numeric strings in both CSV outputs.', 'Plot only the two endpoint years, with zero-based bars and separate native-unit scales.', 'Display one decimal, matching the original EPA qText precision for these endpoint measures.']}, indent=2) + '\n')
    (OUT / 'README.md').write_text(f'''# More horsepower. More miles per gallon.

An original Meplar comparison using only the already-licensed frozen EPA summary. This replaces the previous pickup/sedan **weight** comparison with a distinct power/fuel-economy story.

## Deliverables

- `chart.png`: 1440-pixel primary PNG.
- `chart.svg`: self-contained vector chart; native viewBox 390 × 700.
- `chart_mobile390.png`: actual 390 × 700 native raster for visual QA.
- `plotted_endpoints.csv`: the two exact endpoint rows plotted.
- `chart_data_1975_2024.csv`: all 50 annual All/All rows, exact retained source precision.
- `VALIDATION.json`: endpoint arithmetic, archived-source checks, output hashes, font-size and rendered-text checks.
- `SOURCE_REFERENCE.json` / `SOURCE_TERMS.txt`: retrieval, scope, transformations and frozen licence terms.
- `build_chart.py`: deterministic local rebuild; no network or paid dependencies.

## Exact facts and display rounding

| Metric | 1975 | 2024 | Percent change |
|---|---:|---:|---:|
| EPA estimated real-world MPG | 13.0597 | 27.24521 | +108.6204889852% |
| Horsepower (HP) | 137.3346 | 258.0114 | +87.8706458533% |

The chart shows 13.1 / 27.2 MPG and 137.3 / 258.0 HP. MPG more than doubles; horsepower **nearly**, not fully, doubles. Percent changes are documented here rather than repeated as additional oversized statistics on the graphic.

## Scope and limitations

US new light-duty vehicles delivered for sale, production-weighted category averages by EPA **model year**, final years 1975–2024. This is the source's existing `All / All` aggregate, not an unweighted average over vehicle models or a sum of overlapping aggregates. Preliminary 2025 stays excluded.

The vehicle mix changes over time; these are not the same vehicles tracked longitudinally and not the existing on-road fleet. The two panels have separate native units/scales and both start at zero. The graphic compares endpoints and does not imply uninterrupted improvement, a technology-adjusted effect or causality. The full 50-year selected series is retained alongside the graphic for inspection.

The frozen measure labels are exactly `Real-world MPG` and `Horsepower (HP)`. {MPGE_NOTE} The official EPA report documents this convention on printed page 6, footnote 1 (PDF page 12, zero-based index 11): {REPORT_PDF_URL}#page=12 . This chart retains the reported metric and does not reproduce EPA's full adjustment formula. No CO₂ field is available here. No emissions, literal gasoline-use, EV-share or mechanical-efficiency estimates are invented or derived.

## Source and reuse

US EPA, **2025 Automotive Trends Report**, public summary retrieved **13 October 2026**. Source: {SOURCE_URL}

Definitions: {DEFINITIONS_URL}

MPG/MPGe convention: official report, printed page 6 footnote 1 (PDF page 12): {REPORT_PDF_URL}#page=12

EPA Standard Open Data License: {LICENSE_URL}

Frozen terms state that EPA-produced data are generally US public-domain material unless otherwise specified; no contrary data-specific restriction was found in this archived source. Preserve attribution and transformation notes. The visualization is original Meplar work. No EPA logo or endorsement is implied.

## Reproduce and verify

Run `python3 build_chart.py` in this directory. To use a relocated copy of the **same frozen** release, pass `--source /path/to/us-vehicle-weights/v1`. SHA-256 guards reject changed input. Uses only Python standard library plus the existing local `rsvg-convert` and `pdftotext`; do not install or fetch dependencies.

The builder checks that loose/zipped CSV bytes match, all 50 All/All final years exist once, production share is 1, and all 100 annual MPG/HP values equal the archived EPA Qlik `qNum` values. Content QA independently repeated these checks. Native renders are tested for actual text clipping and text overlap. `chart_mobile390.png` must also be visually inspected at native size; this is a static local render, not browser QA.

No shared release files, deployment, browser, Reddit, external post or production assets were touched. The `.qa/` PDF is local rendering evidence and is not intended for publishing.
''')
    print(json.dumps({'status': 'PASS', 'directory': str(OUT), 'primaryPngPixels': render_checks['primaryPngPixels'], 'mobileQaPixels': render_checks['actualMobileQaPixels'], 'allAnnualValuesChecked': 100, 'files': sorted(p.name for p in OUT.iterdir() if p.is_file())}, indent=2))


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--source', type=Path, default=DEFAULT_SOURCE)
    args = parser.parse_args()
    main(args.source)
