#!/usr/bin/env python3
"""Is your carton costing you money? Measure it, then price the repack.

Three parts, in this order:

  PART 1  Zero network cost. Downloads the open Parcel Price Index CSV and
          shows, for every carton in the newest issue, what the scale says
          and what the carrier actually bills once the box is measured.
  PART 2  Two live quotes (only with --box and --to). Prices the carton you
          ship in today against the carton you think the item fits in, and
          turns the difference into a per-parcel and a per-month number.
  PART 3  Zero network cost. The honesty check: proves a carton-to-carton
          comparison from the Part 1 table is legitimate before quoting a
          saving from it, and refuses to state one when it is not.

Standard library only. Python 3.9+. No install, no signup, no key, no cost.

  python3 repack.py
  python3 repack.py --box 24x18x18 --into 16x16x16 --weight 12 --to 98101
  python3 repack.py --box 24x18x18 --into 16x16x16 --weight 12 --to 98101 --dry-run

Data: Parcel Price Index, CC BY 4.0, https://smklog.com/lp/parcel-price-index
"""

import argparse
import csv
import io
import json
import math
import socket
import sys
import urllib.error
import urllib.request

CSV_URL = 'https://smklog.com/downloads/parcel-price-index.csv'
QUOTE_URL = 'https://quote-api.smklog.com/quote'

# Both hosts answer HTTP 403 to Python's default urllib User-Agent
# ("Python-urllib/3.x"). This is not a fluke and not rate limiting: send a
# real User-Agent on EVERY request and both return 200. If you fork this
# script, keep the header. Reproduced 2026-09-02 on the CSV host and the
# quote host.
USER_AGENT = 'smklog-repack-example/1.0 (+https://smklog.com/lp/parcel-price-index)'

DEFAULT_ITEM = 'packed carton of ceramic dinner plates'

CSV_TIMEOUT = 30      # seconds; the file is about 33 KB
QUOTE_TIMEOUT = 60    # seconds; a live carrier round trip took 5.1 s and 1.9 s

IN_TO_CM = 2.54
LB_TO_KG = 0.45359237
CUBIC_FOOT_IN3 = 1728  # 12 x 12 x 12

# Dimensional divisors. The lower the divisor, the more the box costs.
DIVISOR_STANDARD = 139   # what UPS and FedEx bill retail domestic ground on
DIVISOR_USPS_GA = 166    # USPS Ground Advantage, and only above 1 cubic foot

# Every no-rates answer is still HTTP 200 with an empty rates array, so the
# branch below is on the response "mode" field and never on the status code.
MODE_MESSAGES = {
    'freight_manager':
        'No online rates: this shipment is out of parcel range (too big, too '
        'heavy, or more than one box) and is priced by a person instead.',
    'parcel_restricted':
        'No rates: carrier rules do not allow this shipment as described, so '
        'nothing was priced.',
    'international_unsupported_online':
        'No rates: that destination country is not sold online, so retrying '
        'with a different carton will not help.',
    'parcel_rates_unavailable':
        'No rates: the carriers returned no purchasable service for this box '
        'on this lane. Try again shortly, or change the carton.',
}

# A rejected request answers 4xx with an error code and a message. The message
# is written for the checkout, where a card is involved, so quoting it verbatim
# to somebody who is only pricing boxes says things like "payment is blocked"
# on a script that never charges anything. Say what went wrong instead, and
# fall through to the server text only for codes not named here.
ERROR_MESSAGES = {
    'invalid_us_zip':
        'That is not a US ZIP this endpoint can price. Check --to (and '
        '--from-zip): both have to be real 5-digit US ZIP codes.',
    'zip_city_state_mismatch':
        'That ZIP did not resolve to a real US city and state. Check --to and '
        '--from-zip.',
    'missing_required_fields':
        'The request was missing a field. If you edited quote_payload, put the '
        'product, quantity and both postal codes back.',
    'product_not_recognized':
        'The text in --item was not read as a product. Describe the thing in '
        'the box in plain words, e.g. "packed carton of ceramic dinner plates".',
    'product_url_not_detected':
        'No product could be read from that URL. Pass --item with a plain-words '
        'description instead.',
    'rate_limited':
        'You have used the quotes this endpoint allows one client in an hour '
        '(80). Wait for the hour to turn over; nothing is broken.',
    'invalid_json':
        'The request body was not valid JSON. If you edited quote_payload, that '
        'is where to look.',
}


# ---------------------------------------------------------------- networking

def http_get(url, timeout):
    """GET with an explicit User-Agent. See the USER_AGENT note above."""
    request = urllib.request.Request(url, headers={'User-Agent': USER_AGENT})
    return urllib.request.urlopen(request, timeout=timeout).read()


def http_post_json(url, payload, timeout):
    """POST JSON with an explicit User-Agent, and return (status, parsed body).

    A 4xx from this API still carries a JSON body naming what was wrong, so
    the body is parsed and handed back for both success and failure.
    """
    body = json.dumps(payload).encode('utf-8')
    request = urllib.request.Request(
        url,
        data=body,
        headers={
            'User-Agent': USER_AGENT,
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        },
        method='POST',
    )
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            return response.status, json.loads(response.read().decode('utf-8'))
    except urllib.error.HTTPError as error:
        if error.code == 403:
            # Not a business error: this is the missing User-Agent. Let it out
            # so explain_network_error can name the header instead of printing
            # an empty rejection body.
            raise
        raw = error.read().decode('utf-8', 'replace')
        try:
            return error.code, json.loads(raw)
        except ValueError:
            return error.code, {'error': 'non_json_response', 'message': raw[:400]}


def explain_network_error(error, what):
    """One clear line for the failure modes a reader will actually hit."""
    if isinstance(error, urllib.error.HTTPError) and error.code == 403:
        return ('%s was refused with HTTP 403. That is the missing User-Agent '
                'header: this host rejects the default Python urllib '
                'User-Agent. Send a User-Agent request header (this script '
                'sets USER_AGENT on every call) and it returns 200.' % what)
    if isinstance(error, urllib.error.HTTPError):
        return '%s failed with HTTP %s: %s' % (what, error.code, error.reason)
    if isinstance(error, socket.timeout):
        return '%s timed out. Nothing was charged and nothing was booked; run it again.' % what
    if isinstance(error, urllib.error.URLError):
        reason = getattr(error, 'reason', error)
        if isinstance(reason, socket.timeout):
            return '%s timed out. Nothing was charged and nothing was booked; run it again.' % what
        return '%s could not connect: %s' % (what, reason)
    return '%s failed: %s' % (what, error)


# ------------------------------------------------------------------ measuring

def dim_weight_lb(volume_in3, divisor):
    """Dimensional weight in whole pounds. Carriers round up, and never to 0."""
    return max(1, int(math.ceil(volume_in3 / float(divisor))))


def billed_weight_lb(scale_lb, volume_in3, divisor, only_above_cubic_foot=False):
    """What the carrier bills: the greater of scale weight and dimensional weight.

    USPS Ground Advantage applies its dimensional weight only to boxes over one
    cubic foot (1728 cubic inches). Below that, the scale weight stands however
    empty the box is. That single exception is why the two counts in Part 1
    differ, so it is a parameter here rather than an afterthought.
    """
    if only_above_cubic_foot and volume_in3 <= CUBIC_FOOT_IN3:
        return scale_lb
    return max(scale_lb, dim_weight_lb(volume_in3, divisor))


def parse_box(text):
    """'24x18x18' -> (24.0, 18.0, 18.0) inches. A 'cm' suffix converts."""
    raw = str(text).strip().lower().replace(' ', '')
    units = 'in'
    for suffix in ('inches', 'inch', 'in', '"', 'cm'):
        if raw.endswith(suffix):
            units = 'cm' if suffix == 'cm' else 'in'
            raw = raw[:-len(suffix)]
            break
    parts = raw.replace('*', 'x').replace('X', 'x').split('x')
    if len(parts) != 3:
        raise argparse.ArgumentTypeError(
            'box must look like 24x18x18 (inches) or 61x46x46cm, got %r' % text)
    try:
        dims = [float(p) for p in parts]
    except ValueError:
        raise argparse.ArgumentTypeError('box sides must be numbers, got %r' % text)
    if min(dims) <= 0:
        raise argparse.ArgumentTypeError('box sides must be greater than zero, got %r' % text)
    if units == 'cm':
        dims = [d / IN_TO_CM for d in dims]
    return tuple(dims)


def parse_weight(text):
    """'12' or '12lb' -> 12.0 pounds. A 'kg' suffix converts."""
    raw = str(text).strip().lower().replace(' ', '')
    units = 'lb'
    for suffix in ('pounds', 'pound', 'lbs', 'lb', 'kgs', 'kg'):
        if raw.endswith(suffix):
            units = 'kg' if suffix.startswith('kg') else 'lb'
            raw = raw[:-len(suffix)]
            break
    try:
        value = float(raw)
    except ValueError:
        raise argparse.ArgumentTypeError('weight must be a number, got %r' % text)
    if value <= 0:
        raise argparse.ArgumentTypeError('weight must be greater than zero, got %r' % text)
    return value / LB_TO_KG if units == 'kg' else value


# --------------------------------------------------------- part 1: the index

def load_index(url=CSV_URL):
    """Download the open price index and return (issue, captured_on, cartons)."""
    text = http_get(url, CSV_TIMEOUT).decode('utf-8')
    rows = list(csv.DictReader(io.StringIO(text)))
    if not rows:
        raise ValueError('the price index CSV came back empty')

    issue = max(row['issue'] for row in rows)
    rows = [row for row in rows if row['issue'] == issue]
    captured_on = sorted({row['captured_on'] for row in rows})[-1]

    cartons = {}
    for row in rows:
        name = row['carton']
        carton = cartons.get(name)
        if carton is None:
            length, width, height = parse_box(row['box_in'].split(' ')[0])
            carton = {
                'name': name,
                'box_in': row['box_in'],
                'origin': '%s (%s)' % (row.get('origin_zip', ''), row.get('origin', '')),
                'dims_in': (length, width, height),
                'volume_in3': length * width * height,
                'scale_lb': float(row['weight_lb']),
                'lanes': {},
            }
            cartons[name] = carton
        lane = row['destination_zip']
        price = float(row['customer_price_usd'])
        best = carton['lanes'].get(lane)
        if best is None or price < best['price']:
            carton['lanes'][lane] = {'price': price, 'city': row['destination']}
        worst = carton.setdefault('lane_dearest', {}).get(lane)
        if worst is None or price > worst['price']:
            carton['lane_dearest'][lane] = {'price': price, 'city': row['destination']}

    for carton in cartons.values():
        prices = [lane['price'] for lane in carton['lanes'].values()]
        carton['lane_count'] = len(prices)
        carton['avg_cheapest'] = sum(prices) / len(prices)
        carton['billed_139'] = billed_weight_lb(
            carton['scale_lb'], carton['volume_in3'], DIVISOR_STANDARD)
        carton['billed_usps'] = billed_weight_lb(
            carton['scale_lb'], carton['volume_in3'], DIVISOR_USPS_GA,
            only_above_cubic_foot=True)
        carton['dim_139'] = dim_weight_lb(carton['volume_in3'], DIVISOR_STANDARD)
        carton['dim_166'] = dim_weight_lb(carton['volume_in3'], DIVISOR_USPS_GA)
        carton['usps_dim_applies'] = carton['volume_in3'] > CUBIC_FOOT_IN3

    ordered = sorted(cartons.values(), key=lambda c: c['volume_in3'], reverse=True)
    return issue, captured_on, ordered


def print_index(issue, captured_on, cartons):
    # One origin and five destinations is the honest limit of this data, and it
    # belongs in the output rather than only on the page: this is the part that
    # gets pasted into a thread on its own.
    origin_note = ''
    if cartons:
        cities = sorted({lane['city'] for lane in cartons[0]['lanes'].values()
                         if lane.get('city')})
        origin = cartons[0].get('origin', '').strip()
        if origin and origin != '()':
            origin_note = ' Origin %s' % origin
            if cities:
                origin_note += '; destinations: %s' % '; '.join(cities)
            origin_note += '.'
    print('PART 1 - what the box weighs, and what it bills')
    print('=' * 78)
    print('Issue %s, captured %s, %d cartons, cheapest price averaged over %d lanes.%s'
          % (issue, captured_on, len(cartons),
             cartons[0]['lane_count'] if cartons else 0, origin_note))
    print('Source: %s (CC BY 4.0). No account, no key, no charge.' % CSV_URL)
    print()
    row_format = '%-8s %-12s %8s %8s %8s %9s  %-14s %10s'
    header = row_format % ('carton', 'box (in)', 'scale', 'cu in', 'bill139',
                           'billUSPS', 'USPS dim?', 'avg price')
    print(header)
    print('-' * len(header))
    for carton in cartons:
        print(row_format
              % (carton['name'], carton['box_in'].replace(' in', ''),
                 '%.0f lb' % carton['scale_lb'],
                 '%.0f' % carton['volume_in3'],
                 '%d lb' % carton['billed_139'],
                 '%d lb' % carton['billed_usps'],
                 'yes' if carton['usps_dim_applies'] else 'no (<1 cu ft)',
                 '$%.2f' % carton['avg_cheapest']))
    print()
    print('bill139  = billed weight at divisor %d (UPS and FedEx ground).' % DIVISOR_STANDARD)
    print('billUSPS = billed weight under the USPS Ground Advantage rule:')
    print('           divisor %d, and only when the box is over %d cubic inches'
          % (DIVISOR_USPS_GA, CUBIC_FOOT_IN3))
    print('           (1 cubic foot). At or under that, the scale weight stands.')
    print()

    air_139 = [c for c in cartons if c['billed_139'] > c['scale_lb']]
    air_usps = [c for c in cartons if c['billed_usps'] > c['scale_lb']]
    print('Cartons billed above their scale weight - paying for air:')
    print('  at divisor %d:                    %d of %d  (%s)'
          % (DIVISOR_STANDARD, len(air_139), len(cartons),
             ', '.join(c['name'] for c in air_139) or 'none'))
    print('  under the USPS Ground Advantage rule: %d of %d  (%s)'
          % (len(air_usps), len(cartons),
             ', '.join(c['name'] for c in air_usps) or 'none'))
    print('The gap between those two counts is the %d cubic inch threshold, not'
          % CUBIC_FOOT_IN3)
    print('the divisor. Quote both numbers; quoting only divisor %d overstates it.'
          % DIVISOR_STANDARD)
    print()


# ------------------------------------------------------- part 3: repack check

def repack_check(big, small, divisor=DIVISOR_STANDARD):
    """Is a saving quoted from the Part 1 table honest?

    Two cartons in the index carry different scale weights, so subtracting one
    row's price from another's silently compares two different shipments. The
    comparison is only about the CARTON when the smaller carton is still priced
    on its dimensions - that is, when the smaller carton's dimensional weight
    is above BOTH scale weights. Then neither price came off a scale and the
    difference is the box alone.
    """
    small_dim = dim_weight_lb(small['volume_in3'], divisor)
    ok_big = small_dim > big['scale_lb']
    ok_small = small_dim > small['scale_lb']
    return {
        'ok': ok_big and ok_small,
        'small_dim': small_dim,
        'ok_big': ok_big,
        'ok_small': ok_small,
        'divisor': divisor,
    }


def print_repack_check(big, small):
    check = repack_check(big, small)
    print('  %s (%s, scale %.0f lb, $%.2f)  ->  %s (%s, scale %.0f lb, $%.2f)'
          % (big['name'], big['box_in'], big['scale_lb'], big['avg_cheapest'],
             small['name'], small['box_in'], small['scale_lb'], small['avg_cheapest']))
    print('    dimensional weight of %s at divisor %d: %d lb'
          % (small['name'], check['divisor'], check['small_dim']))
    print('      %s %d lb > %.0f lb scale weight of %s'
          % ('PASS' if check['ok_big'] else 'FAIL',
             check['small_dim'], big['scale_lb'], big['name']))
    print('      %s %d lb > %.0f lb scale weight of %s'
          % ('PASS' if check['ok_small'] else 'FAIL',
             check['small_dim'], small['scale_lb'], small['name']))
    if check['ok']:
        saving = big['avg_cheapest'] - small['avg_cheapest']
        print('    CHECK PASSES. Neither price was set by a scale, so the difference')
        print('    is the carton: $%.2f - $%.2f = $%.2f saved per parcel, averaged'
              % (big['avg_cheapest'], small['avg_cheapest'], saving))
        print('    over the %d lanes in this issue.' % big['lane_count'])
    elif not check['ok_small']:
        print('    CHECK FAILS - no saving stated for this pair.')
        print('    The smaller carton bills on its scale weight, not its size, so the')
        print('    price gap is partly the %.0f lb vs %.0f lb difference in what is'
              % (big['scale_lb'], small['scale_lb']))
        print('    inside the box. Subtracting these two rows would credit the carton')
        print('    with a saving that came from shipping a lighter item.')
    else:
        # ok_small passed and ok_big did not: the smaller carton IS billing on
        # its size, it just does not clear the bigger row's scale weight. Saying
        # "it bills on its scale weight" here would contradict the PASS line
        # printed two lines above.
        print('    CHECK FAILS - no saving stated for this pair.')
        print('    The %s carton bills %d lb on volume, which only ties the %.0f lb'
              % (small['name'], check['small_dim'], big['scale_lb']))
        print('    the %s row carries, so the pair sits exactly on the boundary of'
              % big['name'])
        print('    the rule. One more pound of contents in the smaller box and it')
        print('    starts billing on the scale instead, and the gap stops being the')
        print('    carton. The rule is strict on purpose: a boundary pair is not a')
        print('    saving you can quote.')
    return check


def print_part3(cartons, pair=None):
    print('PART 3 - the repack check (no calls)')
    print('=' * 78)
    print('Before quoting any saving off the Part 1 table, prove the two rows are')
    print('comparable. Rule: the smaller carton\'s dimensional weight must exceed')
    print('BOTH cartons\' scale weights, so no price in the comparison came off a')
    print('scale. Otherwise the "saving" is partly a lighter item.')
    print()

    by_name = dict((c['name'], c) for c in cartons)
    if pair:
        big_name, small_name = pair
        if big_name not in by_name or small_name not in by_name:
            print('Unknown carton name. Available: %s'
                  % ', '.join(sorted(by_name)))
            return
        big, small = by_name[big_name], by_name[small_name]
        # --box/--into read current-then-candidate, so a reader typing the small
        # box second here is following the same habit. Without this,
        # --pair heavy,bulky "passes" and reports $-20.26 saved per parcel.
        if small['name'] == big['name']:
            print('--pair needs two different cartons: %s against itself saves '
                  'nothing by construction.' % big['name'])
            print('Available: %s' % ', '.join(sorted(by_name)))
            print()
            return
        if small['volume_in3'] >= big['volume_in3']:
            print('--pair takes the BIGGER carton first: %s is %.0f cubic inches '
                  'and %s is %.0f.' % (big['name'], big['volume_in3'],
                                       small['name'], small['volume_in3']))
            print('Try --pair %s,%s.' % (small['name'], big['name']))
            print()
            return
    else:
        big, small = cartons[0], cartons[1]

    print('Checked pair:')
    print_repack_check(big, small)
    print()

    # Show a refusal too, so the failure branch is not something a reader has
    # to take on faith. First ordered pair in the table that does not qualify.
    for i, outer in enumerate(cartons):
        for inner in cartons[i + 1:]:
            if (outer, inner) == (big, small):
                continue
            if not repack_check(outer, inner)['ok']:
                print('And a pair the same check refuses:')
                print_repack_check(outer, inner)
                print()
                return


# -------------------------------------------------------- part 2: live quotes

def quote_payload(dims_in, weight_lb, from_zip, to_zip, item):
    """Build a /quote body with EXACT dimensions.

    All four of length_cm, width_cm, height_cm and weight_kg count only as a
    complete set. Send the set with package_source "manual" and the packed-box
    estimate is skipped outright: the request never touches the box-estimate
    budget, only the ordinary quote allowance. Leave one field out and all four
    are ignored, the box is estimated from the words instead, and the request
    spends from the much smaller box-estimate budget.
    """
    length, width, height = dims_in
    return {
        'product': item,
        'quantity': 1,
        'from_postal_code': from_zip,
        'to_postal_code': to_zip,
        'to_country_code': 'US',
        'length_cm': round(length * IN_TO_CM, 2),
        'width_cm': round(width * IN_TO_CM, 2),
        'height_cm': round(height * IN_TO_CM, 2),
        'weight_kg': round(weight_lb * LB_TO_KG, 3),
        'package_source': 'manual',
    }


def sorted_rates(data):
    """Rates are NOT returned cheapest first - the default order weighs speed
    against price. Sort by the amount yourself before calling anything cheapest."""
    rates = [r for r in (data.get('rates') or []) if r.get('amount') is not None]
    return sorted(rates, key=lambda r: float(r['amount']))


def rate_label(rate):
    """Always the display name. The raw service field is an internal code and
    means nothing to a reader."""
    return rate.get('service_label') or rate.get('carrier') or 'service'


def carrier_cost(rate):
    """The carrier's own charge inside a rate, or None when it is not quoted.

    amount is the checkout total and it carries a service fee that is a share
    of the carrier cost, so the fee shrinks as the box does. A saving computed
    from amount therefore includes some of that fee getting smaller, which is
    not money the carrier stopped charging. If you buy labels on your own
    carrier account, this is your figure.
    """
    value = rate.get('carrier_amount')
    try:
        return float(value) if value is not None else None
    except (TypeError, ValueError):
        return None


def carrier_saving(old_rate, new_rate):
    """What the carrier alone stopped charging, or None if either side is silent."""
    before, after = carrier_cost(old_rate), carrier_cost(new_rate)
    if before is None or after is None:
        return None
    return before - after


def print_rates(title, data):
    print('  %s' % title)
    mode = data.get('mode')
    rates = sorted_rates(data)
    if not rates:
        print('    %s' % MODE_MESSAGES.get(
            mode, 'No rates came back (mode %r) and this script has no specific '
                  'advice for that case.' % mode))
        return []
    print('    %10s %10s  %-28s %s' % ('checkout', 'carrier', 'service', 'transit'))
    for rate in rates:
        days = rate.get('delivery_days')
        days_text = '%s business days' % days if days else 'transit not quoted'
        cost = carrier_cost(rate)
        print('    %10s %10s  %-28s %s'
              % ('$%.2f' % float(rate['amount']),
                 '$%.2f' % cost if cost is not None else '-',
                 rate_label(rate), days_text))
    return rates


def run_part2(args):
    print('PART 2 - price the repack on your own lane')
    print('=' * 78)
    current = quote_payload(args.box, args.weight, args.from_zip, args.to, args.item)
    candidate = quote_payload(args.into, args.weight, args.from_zip, args.to, args.item)
    box_text = 'x'.join('%g' % d for d in args.box)
    into_text = 'x'.join('%g' % d for d in args.into)
    print('%s in -> %s in, %g lb, %s -> %s. Exact dimensions on both, so the'
          % (box_text, into_text, args.weight, args.from_zip, args.to))
    print('packed-box estimate is skipped and neither call touches the box-estimate')
    print('budget. Two requests total.')
    print()

    if args.dry_run:
        print('  --dry-run: nothing was sent. These are the two request bodies.')
        print()
        for name, payload in (('current carton', current), ('candidate carton', candidate)):
            print('  POST %s   (%s)' % (QUOTE_URL, name))
            print('  headers: User-Agent: %s' % USER_AGENT)
            print('           Content-Type: application/json')
            for line in json.dumps(payload, indent=2).splitlines():
                print('  %s' % line)
            print()
        print('  Drop --dry-run to send them and get the comparison.')
        print()
        return True

    answers = []
    for name, payload in (('current carton', current), ('candidate carton', candidate)):
        try:
            status, data = http_post_json(QUOTE_URL, payload, QUOTE_TIMEOUT)
        except Exception as error:  # noqa: BLE001 - one readable line for any failure
            print('  %s' % explain_network_error(error, 'The quote for the %s' % name))
            return False
        if status >= 400 or data.get('error'):
            code = data.get('error') or 'http_%s' % status
            print('  The %s was rejected (%s).' % (name, code))
            print('  %s' % ERROR_MESSAGES.get(
                code, data.get('message', 'No message was returned.')))
            print('  Nothing was charged: this script only asks for prices.')
            return False
        answers.append(data)

    current_data, candidate_data = answers
    print('  Box estimate used: %s / %s (both "customer" means your numbers were'
          % (current_data.get('estimate_basis'), candidate_data.get('estimate_basis')))
    print('  priced as given, nothing was guessed).')
    print()
    current_rates = print_rates('Current carton  %s in, %g lb' % (box_text, args.weight), current_data)
    print()
    candidate_rates = print_rates('Candidate carton %s in, %g lb' % (into_text, args.weight), candidate_data)
    print()

    if not current_rates or not candidate_rates:
        print('  One side has no rates, so there is nothing to compare. See the line')
        print('  above for why that side came back empty.')
        print()
        return False

    best_current, best_candidate = current_rates[0], candidate_rates[0]
    cheapest_saving = float(best_current['amount']) - float(best_candidate['amount'])
    print('  Cheapest to cheapest: $%.2f (%s) -> $%.2f (%s) = $%.2f'
          % (float(best_current['amount']), rate_label(best_current),
             float(best_candidate['amount']), rate_label(best_candidate),
             cheapest_saving))
    if rate_label(best_current) != rate_label(best_candidate):
        print('    The service changes between the two, so this number mixes the')
        print('    carton with a different product. Use the same-service figure.')
    cheapest_carrier_saving = carrier_saving(best_current, best_candidate)

    candidate_by_label = {}
    for rate in candidate_rates:
        candidate_by_label.setdefault(rate_label(rate), rate)
    same_service = None
    for rate in current_rates:
        match = candidate_by_label.get(rate_label(rate))
        if match is not None:
            same_service = (rate, match)
            break  # anchor on the service you would actually buy today

    if same_service:
        old, new = same_service
        same_saving = float(old['amount']) - float(new['amount'])
        print('  Same service, %s: $%.2f -> $%.2f = $%.2f'
              % (rate_label(old), float(old['amount']), float(new['amount']), same_saving))
        # The anchor is the FIRST current rate with a counterpart, which is not
        # always current_rates[0]. Excluding by position reprinted the anchor as
        # an "also matched" line and invited a reader to add it in twice.
        others = [(rate_label(r), float(r['amount']) - float(candidate_by_label[rate_label(r)]['amount']))
                  for r in current_rates
                  if r is not old and rate_label(r) in candidate_by_label]
        for label, saving in others:
            print('    also matched: %-28s $%.2f' % (label, saving))
        decision_saving = same_saving
        decision_carrier_saving = carrier_saving(old, new)
        decision_basis = 'same service (%s), the conservative read' % rate_label(old)
    else:
        same_saving = None
        print('  Same service: no service appears in both answers, so there is no')
        print('  like-for-like pair. Falling back to cheapest-to-cheapest, which')
        print('  changes carrier and transit as well as the box.')
        decision_saving = cheapest_saving
        decision_carrier_saving = cheapest_carrier_saving
        decision_basis = 'cheapest to cheapest (no shared service)'

    # Both figures above are checkout prices, fee included. Say what the carrier
    # alone stopped charging, and decide on that: it is the smaller number and
    # the one that survives buying the label anywhere else.
    if decision_carrier_saving is not None:
        print('  Carrier cost only: $%.2f. The rest of the checkout difference is'
              % decision_carrier_saving)
        print('  our own fee getting smaller with the box, which is not money the')
        print('  carrier stopped charging. This is the figure to plan against.')
        decision_saving = decision_carrier_saving
        decision_basis += ', on carrier cost'
    else:
        print('  Carrier cost was not quoted on both sides, so the figure below is')
        print('  a checkout price with our fee inside it. Treat it as the ceiling.')
    print()

    print('  DECISION: %s' % ('REPACK' if decision_saving > 0 else 'KEEP THE CURRENT CARTON'))
    print('  Basis:    %s' % decision_basis)
    if decision_saving > 0:
        if args.parcels_per_month:
            print('  Worth:    $%.2f per parcel, $%.2f a month at %d parcels'
                  % (decision_saving, decision_saving * args.parcels_per_month,
                     args.parcels_per_month))
        else:
            # No monthly figure unless you told us the volume. A defaulted one
            # is our assumption printed as your money.
            print('  Worth:    $%.2f per parcel. Pass --parcels-per-month N for a'
                  % decision_saving)
            print('            monthly figure; this script will not guess your volume.')
        print('  Against:  the smaller carton, the tape and the packing time, plus')
        print('            the risk of a damage claim if the item no longer has room.')
        print('            Only you can price those; the script will not pretend to.')
    else:
        print('  Worth:    $%.2f per parcel - the smaller carton is not cheaper on'
              % decision_saving)
        print('            this lane at this weight. Repacking would cost you time')
        print('            for nothing. Try a different candidate carton.')
    print()
    return True


# ------------------------------------------------------------------- assembly

def build_parser():
    parser = argparse.ArgumentParser(
        prog='repack.py',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description='Find out whether your shipping carton is costing you money, '
                    'and price the fix.',
        epilog="""
Part 1 and Part 3 always run and cost nothing: they read the open Parcel Price
Index CSV (CC BY 4.0). Part 2 runs only when you pass --box and --to, and sends
exactly two quote requests.

Exact dimensions matter for more than accuracy. All four of length, width,
height and weight count only as a complete set; send the set and the packed-box
estimate is skipped entirely, so the request never draws on the small
box-estimate budget - only on the ordinary quote allowance. Leave one out and
all four are ignored, the box is estimated from your words, and the tighter
budget applies.

Examples:
  python3 repack.py
  python3 repack.py --box 24x18x18 --into 16x16x16 --weight 12 --to 98101
  python3 repack.py --box 24x18x18 --into 16x16x16 --weight 12 --to 98101 --dry-run
  python3 repack.py --box 61x46x46cm --into 41x41x41cm --weight 5.5kg --to 98101

No account, no key, no charge. Sizes are inches and weight is pounds unless you
add a cm or kg suffix.
""")
    parser.add_argument('--box', type=parse_box, metavar='LxWxH',
                        help='the carton you ship in today, e.g. 24x18x18 (inches)')
    parser.add_argument('--into', type=parse_box, metavar='LxWxH',
                        help='the smaller carton you think the item fits in, e.g. 16x16x16')
    parser.add_argument('--weight', type=parse_weight, default=parse_weight('12'),
                        metavar='LB', help='packed weight in pounds (default: 12)')
    parser.add_argument('--to', metavar='ZIP',
                        help='destination US ZIP, e.g. 98101')
    parser.add_argument('--from-zip', default='07102', metavar='ZIP',
                        dest='from_zip',
                        help='origin US ZIP (default: 07102, the index origin)')
    parser.add_argument('--item', default=DEFAULT_ITEM,
                        metavar='TEXT',
                        help='plain-words description of what is in the box')
    parser.add_argument('--parcels-per-month', type=int, default=None, metavar='N',
                        help='how many of these you ship a month; pass it and the '
                             'per-parcel saving is also shown per month (no default '
                             '- an assumed volume is our number, not yours)')
    parser.add_argument('--pair', metavar='BIG,SMALL',
                        help='which two Part 1 cartons to run the Part 3 check on, '
                             'BIGGER CARTON FIRST, e.g. --pair bulky,heavy')
    parser.add_argument('--dry-run', action='store_true',
                        help='Part 2 prints the two request bodies instead of sending them')
    return parser


def main(argv=None):
    parser = build_parser()
    args = parser.parse_args(argv)

    if (args.box is None) != (args.into is None):
        parser.error('--box and --into go together: give the carton you use now '
                     'and the carton you want to test.')
    if args.box is not None and not args.to:
        parser.error('--to is required with --box: a saving is per lane, not in general.')
    if args.parcels_per_month is not None and args.parcels_per_month < 1:
        parser.error('--parcels-per-month must be at least 1.')

    pair = None
    if args.pair:
        parts = [p.strip() for p in args.pair.split(',')]
        if len(parts) != 2:
            parser.error('--pair takes two carton names, e.g. --pair bulky,heavy')
        pair = (parts[0], parts[1])

    print()
    try:
        issue, captured_on, cartons = load_index()
    except Exception as error:  # noqa: BLE001 - one readable line for any failure
        print(explain_network_error(error, 'The price index download'))
        return 1
    if len(cartons) < 2:
        print('The newest issue has fewer than two cartons; nothing to compare.')
        return 1

    print_index(issue, captured_on, cartons)

    part2_ok = True
    if args.box is not None:
        part2_ok = run_part2(args)
    else:
        print('PART 2 - skipped')
        print('=' * 78)
        if args.to or args.item != DEFAULT_ITEM:
            # --to and --item alone did nothing, and saying nothing about that
            # looks exactly like a request that was sent and came back quiet.
            print('Note: --to and --item only take effect together with --box and')
            print('--into, so nothing was sent and neither flag was used.')
        print('Add --box, --into and --to to price your own carton against a smaller')
        print('one on your own lane. Two requests, or none with --dry-run:')
        print('  python3 repack.py --box 24x18x18 --into 16x16x16 --weight 12 --to 98101')
        print()
        print('This script always sends exact dimensions, on purpose. Describing an')
        print('item in words and having the carton worked out for you is a feature of')
        print('the endpoint, not of this script, and it draws on a much smaller')
        print('hourly budget - so measure the box once and pass it.')
        print()

    print_part3(cartons, pair)
    # Part 3 costs nothing and is still valid when Part 2 failed, so it prints
    # either way - but a failed quote must not exit 0 into somebody's cron.
    return 0 if part2_ok else 1


if __name__ == '__main__':
    sys.exit(main())
