#!/usr/bin/env python3
"""Reproduce the 2022 CFS state compilation, version 1.2, offline.

Python standard library only. Run from any directory:
  python build-freight-ledger-v1.2.py --output-dir ./rebuilt
The two source-input files must sit beside this script. They are a manual
transcription of published federal survey estimates, not new measurements.
This script checks inputs and arithmetic; it does not re-verify live sources.
"""
from __future__ import annotations
import argparse
import csv
import json
import statistics
import sys
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
from typing import Any

VERSION = '1.2'
VERIFIED = '2026-09-15'
SOURCE = 'https://www.census.gov/content/dam/Census/library/publications/2022/econ/e22tcf-us.pdf'
RAW = 'cfs-2022-state-source-transcription-v1.2.csv'
SUPPLEMENT = 'cfs-2022-supplementary-source-inputs-v1.2.json'
ESTIMATE_MAP = {
    'origin_value_musd':'origin_value_million_usd',
    'origin_weight_ktons':'origin_weight_thousand_tons',
    'origin_avg_gcd_miles':'origin_mean_gcd_miles',
    'dest_value_musd':'destination_value_million_usd',
    'dest_weight_ktons':'destination_weight_thousand_tons',
    'dest_avg_gcd_miles':'destination_mean_gcd_miles',
}
CV_MAP = {
    'origin_value_cv_percent':'origin_value_cv_percent',
    'origin_weight_cv_percent':'origin_weight_cv_percent',
    'origin_gcd_cv_percent':'origin_gcd_cv_percent',
    'dest_value_cv_percent':'destination_value_cv_percent',
    'dest_weight_cv_percent':'destination_weight_cv_percent',
    'dest_gcd_cv_percent':'destination_gcd_cv_percent',
}


def require(condition: bool, message: str) -> None:
    if not condition:
        raise ValueError(message)


def numeric(cell: str, integer: bool = True) -> int | float | None:
    if cell == 'S':
        return None
    require(bool(cell.strip()), 'A source cell is unexpectedly empty.')
    number = Decimal(cell.replace(',', ''))
    require(number.is_finite() and number >= 0, f'Invalid source value: {cell!r}')
    if integer:
        require(number == number.to_integral_value(), f'Expected integer: {cell!r}')
        return int(number)
    return float(number)


def divide(a: int | float | None, b: int | float | None,
           multiplier: int = 1) -> float | None:
    if a is None or b is None:
        return None
    require(b > 0, 'Denominator must be positive.')
    return float((Decimal(str(a)) / Decimal(str(b)) * multiplier).quantize(
        Decimal('0.000001'), rounding=ROUND_HALF_UP))


def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
    require(bool(rows), f'Cannot export empty table: {path.name}')
    fields = list(dict.fromkeys(key for row in rows for key in row))
    with path.open('w', newline='', encoding='utf-8') as stream:
        writer = csv.DictWriter(stream, fieldnames=fields, lineterminator='\n')
        writer.writeheader()
        writer.writerows(rows)


def write_json(path: Path, data: Any) -> None:
    path.write_text(json.dumps(data, indent=2, ensure_ascii=False, allow_nan=False)+'\n', encoding='utf-8')


def build(source_dir: Path, output_dir: Path) -> dict[str, Any]:
    with (source_dir/RAW).open(newline='', encoding='utf-8') as stream:
        raw = list(csv.DictReader(stream))
    supplementary = json.loads((source_dir/SUPPLEMENT).read_text(encoding='utf-8'))
    national = supplementary['national_2022']
    require(len(raw) == 51, 'Expected 50 states and District of Columbia.')
    require(len({r['state'] for r in raw}) == 51, 'Duplicate jurisdiction name.')
    require(sum(r['state'] == 'District of Columbia' for r in raw) == 1, 'DC must occur once.')
    require([r['state'] for r in raw] == sorted(r['state'] for r in raw), 'Expected alphabetical inputs.')
    records: list[dict[str, Any]] = []
    for original in raw:
        row: dict[str, Any] = {'state': original['state']}
        for target, key in ESTIMATE_MAP.items():
            row[target] = numeric(original[key])
        for target, key in CV_MAP.items():
            row[target] = numeric(original[key], integer=False)
        for side in ('origin', 'dest'):
            row[f'{side}_weight_status'] = 'S' if row[f'{side}_weight_ktons'] is None else 'published'
        row['origin_value_per_ton_usd'] = divide(row['origin_value_musd'], row['origin_weight_ktons'], 1000)
        row['dest_value_per_ton_usd'] = divide(row['dest_value_musd'], row['dest_weight_ktons'], 1000)
        row['out_in_value_ratio'] = divide(row['origin_value_musd'], row['dest_value_musd'])
        row['out_in_weight_ratio'] = divide(row['origin_weight_ktons'], row['dest_weight_ktons'])
        row['net_tons_ktons'] = (None if row['origin_weight_ktons'] is None or row['dest_weight_ktons'] is None
                                 else row['origin_weight_ktons']-row['dest_weight_ktons'])
        row['share_us_origin_value_pct'] = divide(row['origin_value_musd'], national['value_musd'], 100)
        row['share_us_origin_weight_pct'] = divide(row['origin_weight_ktons'], national['weight_ktons'], 100)
        records.append(row)
    states = [r for r in records if r['state'] != 'District of Columbia']
    matched = [r for r in states if r['origin_weight_ktons'] is not None]
    require(len(states) == 50 and len(matched) == 47, 'Unexpected state cohort size.')
    suppressed = [r['state'] for r in states if r['origin_weight_ktons'] is None]
    require(suppressed == ['Alaska','Arizona','New Mexico'], 'Unexpected suppressed-state set.')
    require(suppressed == [r['state'] for r in states if r['dest_weight_ktons'] is None], 'Origin and destination suppressions differ.')
    require(all(r['origin_value_musd'] is not None and r['dest_value_musd'] is not None for r in records), 'A value estimate is missing.')
    value_order = sorted(matched, key=lambda r: -r['origin_value_musd'])
    weight_order = sorted(matched, key=lambda r: -r['origin_weight_ktons'])
    require(len({r['origin_value_musd'] for r in matched}) == 47, 'Value ties need an explicit tie rule.')
    require(len({r['origin_weight_ktons'] for r in matched}) == 47, 'Weight ties need an explicit tie rule.')
    vr = {r['state']:i+1 for i,r in enumerate(value_order)}
    wr = {r['state']:i+1 for i,r in enumerate(weight_order)}
    for row in records:
        name = row['state']
        row['value_rank_47'] = vr.get(name)
        row['weight_rank_47'] = wr.get(name)
        row['rank_gap'] = None if name not in vr else wr[name]-vr[name]
        row['rank_cohort_n'] = 47 if name in vr else None
        row['data_year'] = 2022
        row['source_publication'] = 'January 2026'
        row['last_verified'] = VERIFIED
        row['verification_status'] = 'primary_tables_read_and_checked'
        row['estimate_tables'] = 'A7; A8 (printed pp. 19-20; PDF pp. 29-30)'
        row['reliability_tables'] = 'B-A7; B-A8 (printed pp. 92-93; PDF pp. 102-103)'
        row['source_url'] = SOURCE
        row['compilation_version'] = VERSION
    by_name = {r['state']:r for r in records}
    origin_sum = sum(r['origin_value_musd'] for r in records)
    dest_sum = sum(r['dest_value_musd'] for r in records)
    origin_w_sum = sum(r['origin_weight_ktons'] or 0 for r in records)
    dest_w_sum = sum(r['dest_weight_ktons'] or 0 for r in records)
    opposite = [r for r in matched if (r['origin_value_musd']-r['dest_value_musd'])*(r['origin_weight_ktons']-r['dest_weight_ktons']) < 0]
    both = [r for r in matched if r['origin_value_musd']>r['dest_value_musd'] and r['origin_weight_ktons']>r['dest_weight_ktons']]
    equal_ranks = [r['state'] for r in matched if r['rank_gap']==0]
    national_per_ton = divide(national['value_musd'],national['weight_ktons'],1000)
    top_v = sorted(states,key=lambda r:-r['origin_value_musd'])
    top_w = weight_order
    under250 = sum(r['weight_ktons'] for r in supplementary['distance_2022'] if r['lower_miles']<250)
    findings = {
        'version':VERSION, 'data_year':2022, 'last_verified':VERIFIED,
        'warning':'All classifications and ranks describe published survey point estimates, not tests of statistical significance. DC is displayed but excluded from state ranks and state-count findings.',
        'matched_state_count':47,
        'opposite_balance_state_count':len(opposite),
        'opposite_balance_states':[r['state'] for r in opposite],
        'more_origin_weight_state_count':sum(r['origin_weight_ktons']>r['dest_weight_ktons'] for r in matched),
        'more_origin_value_state_count':sum(r['origin_value_musd']>r['dest_value_musd'] for r in states),
        'both_origin_measures_higher_state_count':len(both),
        'both_origin_measures_higher_states':[r['state'] for r in both],
        'same_rank_state_count':len(equal_ranks),'same_rank_states':equal_ranks,
        'absolute_rank_gap_at_least_10_state_count':sum(abs(r['rank_gap'])>=10 for r in matched),
        'median_absolute_rank_gap':statistics.median(abs(r['rank_gap']) for r in matched),
        'states_above_national_origin_value_per_ton':sum(r['origin_value_musd']*national['weight_ktons']>national['value_musd']*r['origin_weight_ktons'] for r in matched),
        'national_origin_value_per_ton_usd':national_per_ton,
        'national_2017_value_per_ton_usd':divide(supplementary['national_2017_as_reprinted_2026']['value_musd'], supplementary['national_2017_as_reprinted_2026']['weight_ktons'], 1000),
        'california_texas_origin_value_musd':by_name['California']['origin_value_musd']+by_name['Texas']['origin_value_musd'],
        'california_texas_share_value_pct':divide(by_name['California']['origin_value_musd']+by_name['Texas']['origin_value_musd'],national['value_musd'],100),
        'texas_to_california_origin_weight_ratio':divide(by_name['Texas']['origin_weight_ktons'],by_name['California']['origin_weight_ktons']),
        'top_five_published_origin_weight_share_pct':divide(sum(r['origin_weight_ktons'] for r in top_w[:5]),national['weight_ktons'],100),
        'top_five_origin_value_share_pct':divide(sum(r['origin_value_musd'] for r in top_v[:5]),national['value_musd'],100),
        'top_ten_overlap_count':len({r['state'] for r in top_v[:10]} & {r['state'] for r in top_w[:10]}),
        'max_to_min_state_origin_value_per_ton_ratio':divide(by_name['Massachusetts']['origin_value_musd']*by_name['Wyoming']['origin_weight_ktons'],by_name['Massachusetts']['origin_weight_ktons']*by_name['Wyoming']['origin_value_musd']),
        'under_250_gcd_miles_weight_ktons':under250,
        'under_250_gcd_miles_weight_share_pct':divide(under250,national['weight_ktons'],100),
        'opposite_higher_origin_weight_count':sum(r['origin_weight_ktons']>r['dest_weight_ktons'] for r in opposite),
        'opposite_higher_origin_value_count':sum(r['origin_value_musd']>r['dest_value_musd'] for r in opposite),
        'checks':{
            'jurisdictions':len(records),'states':len(states),'weight_published_jurisdictions':sum(r['origin_weight_ktons'] is not None for r in records),'weight_published_states':len(matched),
            'origin_value_sum_musd':origin_sum,'origin_value_sum_minus_national_musd':origin_sum-national['value_musd'],
            'destination_value_sum_musd':dest_sum,'destination_value_sum_minus_national_musd':dest_sum-national['value_musd'],
            'published_origin_weight_sum_ktons':origin_w_sum,'published_destination_weight_sum_ktons':dest_w_sum,
            'national_minus_published_origin_weight_ktons':national['weight_ktons']-origin_w_sum,
            'national_minus_published_destination_weight_ktons':national['weight_ktons']-dest_w_sum,
            'origin_weight_reconciliation_gap_pct':divide(national['weight_ktons']-origin_w_sum,national['weight_ktons'],100),
            'destination_weight_reconciliation_gap_pct':divide(national['weight_ktons']-dest_w_sum,national['weight_ktons'],100),
            'reconciliation_warning':'Gaps are arithmetic checks, not recovered estimates for suppressed states. National totals and components are rounded; matching aggregate sums does not prove every cell was transcribed correctly.'
        }
    }
    require(findings['opposite_balance_state_count']==17,'Unexpected direction classification.')
    require(findings['both_origin_measures_higher_state_count']==14,'Unexpected both-higher classification.')
    require(equal_ranks==['Illinois','Pennsylvania'],'Unexpected equal ranks.')
    output_dir.mkdir(parents=True,exist_ok=True)
    write_csv(output_dir/'freight-by-state-2022-cfs-v1.2.csv',records)
    write_json(output_dir/'freight-by-state-2022-cfs-v1.2.json',{
        'name':'U.S. State Freight Ledger, 2022','version':VERSION,'data_year':2022,'last_verified':VERIFIED,
        'creator':'Des Moines Dock Door Repair Research','source_url':SOURCE,'source_publication':'January 2026',
        'scope':'2022 CFS-covered industries, 50 states plus DC; not all-sector U.S. freight.',
        'units':{'value':'millions of current 2022 U.S. dollars','weight':'thousands of short tons; one short ton is 2,000 pounds','distance':'mean great-circle miles per shipment, not routed distance','value_per_ton':'cargo value in dollars per short ton, not shipping cost','cv':'coefficient of variation in percent; includes noise-infusion variability','ratio':'dimensionless'},
        'suppression':'Source S is null in JSON and an empty numeric CSV cell; weight_status retains S. No missing estimate is treated as zero.',
        'ranks':'Both ranks compare the same 47 states with published origin value and weight; DC and three suppressed-weight states are excluded. rank_gap=weight_rank_47-value_rank_47. No significance tests were performed.',
        'precision':'Derived noninteger values are calculated from published source inputs and exported to six decimal places; display rounding is performed afterwards.',
        'national_controls':national,'records':records
    })
    write_json(output_dir/'freight-derived-findings-v1.2.json',findings)
    modes=[{**r,'data_year':2022,'source_table':'A1a','source_url':SOURCE,'last_verified':VERIFIED} for r in supplementary['selected_modes_2022']]
    distance=[{**r,'data_year':2022,'source_table':'A2a','source_url':SOURCE,'last_verified':VERIFIED} for r in supplementary['distance_2022']]
    write_csv(output_dir/'selected-freight-modes-cfs-2022-v1.2.csv',modes)
    write_csv(output_dir/'freight-distance-cfs-2022-v1.2.csv',distance)
    area=supplementary['county_geography']
    counties=[{'county':name,'state':'Iowa','cfs_area_code':area['cfs_area_code'],'cfs_area_name':area['cfs_area_name'],'data_year':2022,'source_location':area['source_location'],'source_url':area['source_url'],'last_verified':VERIFIED,'note':area['note']} for name in area['counties']]
    write_csv(output_dir/'des-moines-cfs-area-counties-2022-v1.2.csv',counties)
    return findings


def main() -> int:
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--output-dir',type=Path,default=Path(__file__).resolve().parent)
    args=parser.parse_args()
    try:
        findings=build(Path(__file__).resolve().parent,args.output_dir.resolve())
    except (OSError,ValueError,KeyError,csv.Error) as exc:
        print(f'Build failed: {exc}',file=sys.stderr)
        return 1
    print(json.dumps(findings['checks'],indent=2))
    print(f'Built v{VERSION}: 51 jurisdictions; 47-state matched cohort; 17 opposite directions.')
    return 0

if __name__=='__main__':
    raise SystemExit(main())
