#!/usr/bin/env python3
"""Generate a deterministic single-SKU container-loading benchmark corpus.

The output is intended for public marketing/research notes:

- fixed 40HQ-style interior space;
- seeded, stratified carton dimensions named as package L/W/H;
- transparent heuristic baselines for six-orientation and upright-only policies;
- optional local SmartPacker T1 counts from PackCli --t1-batch.

No public MCP endpoint is called. Use local PackCli for the large batch, then
spot-check selected rows through the public MCP/web tools.
"""

from __future__ import annotations

import argparse
import csv
import itertools
import json
import math
import random
import statistics
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Literal


ROOT = Path(__file__).resolve().parents[2]
DEFAULT_OUT_DIR = ROOT / "PublicSites" / "Doc" / "container_loading_benchmark_10000_20260619"
DEFAULT_PACKCLI = ROOT / "SP3" / "Pack" / "x64" / "Release" / "PackCli.exe"

SEED = 20260619
N_CASES = 10000
SPACE = (12032, 2352, 2698)
CONTAINER_LABEL = "40HQ_exact_12032x2352x2698"
REPORT_DATE = "2026-06-20"
OrientationPolicy = Literal["all_six", "upright_only"]


@dataclass(frozen=True)
class Case:
    case_id: str
    stratum: str
    space_l: int
    space_w: int
    space_h: int
    box_l: int
    box_w: int
    box_h: int

    @property
    def space(self) -> tuple[int, int, int]:
        return (self.space_l, self.space_w, self.space_h)

    @property
    def box(self) -> tuple[int, int, int]:
        return (self.box_l, self.box_w, self.box_h)


STRATA = [
    "small_dense",
    "medium_carton",
    "large_carton",
    "flat_panel",
    "tall_column",
    "long_case",
    "near_divisor",
    "awkward_remainder",
    "mixed_scale",
    "realistic_carton",
]


def round5(value: float) -> int:
    return max(5, int(round(value / 5.0) * 5))


def randint5(rng: random.Random, lo: int, hi: int) -> int:
    return rng.randrange(math.ceil(lo / 5) * 5, math.floor(hi / 5) * 5 + 1, 5)


def ordered_dims(dims: Iterable[int]) -> tuple[int, int, int]:
    """Return package-style L/W/H dimensions for normal cartons."""
    vals = sorted(dims, reverse=True)
    return tuple(vals)  # type: ignore[return-value]


def tall_upright_dims(rng: random.Random) -> tuple[int, int, int]:
    """Tall-product exception such as a refrigerator carton: H may exceed L and W."""
    footprint_l = randint5(rng, 300, 900)
    footprint_w = randint5(rng, 160, min(650, footprint_l))
    height = randint5(rng, 900, 2500)
    return (footprint_l, footprint_w, height)


def orientations_all(box: tuple[int, int, int]) -> list[tuple[int, int, int]]:
    return sorted(set(itertools.permutations(box)))


def orientations_upright(box: tuple[int, int, int]) -> list[tuple[int, int, int]]:
    l, w, h = box
    return sorted(set([(l, w, h), (w, l, h)]))


def orientations(box: tuple[int, int, int], policy: OrientationPolicy) -> list[tuple[int, int, int]]:
    if policy == "all_six":
        return orientations_all(box)
    if policy == "upright_only":
        return orientations_upright(box)
    raise ValueError(policy)


def grid_count(space: tuple[int, int, int], orient: tuple[int, int, int]) -> int:
    return (space[0] // orient[0]) * (space[1] // orient[1]) * (space[2] // orient[2])


def best_single_grid(
    space: tuple[int, int, int], box: tuple[int, int, int], policy: OrientationPolicy = "all_six"
) -> int:
    return max(grid_count(space, orient) for orient in orientations(box, policy))


def best_one_cut(
    space: tuple[int, int, int], box: tuple[int, int, int], policy: OrientationPolicy = "all_six"
) -> int:
    """Best count from one guillotine slab split plus grid packing on each side."""
    orients = orientations(box, policy)
    best = best_single_grid(space, box, policy)
    for axis in range(3):
        axis_len = space[axis]
        for left_orient in orients:
            max_layers = axis_len // left_orient[axis]
            if max_layers <= 0:
                continue
            other_count = 1
            for other_axis in range(3):
                if other_axis != axis:
                    other_count *= space[other_axis] // left_orient[other_axis]
            if other_count <= 0:
                continue
            for layers in range(1, max_layers + 1):
                used = layers * left_orient[axis]
                left_count = layers * other_count
                right_space = list(space)
                right_space[axis] = axis_len - used
                right_count = best_single_grid(tuple(right_space), box, policy) if right_space[axis] > 0 else 0
                total = left_count + right_count
                if total > best:
                    best = total
    return best


def volume_upper_bound(space: tuple[int, int, int], box: tuple[int, int, int]) -> int:
    return (space[0] * space[1] * space[2]) // (box[0] * box[1] * box[2])


def fill_pct(count: int, space: tuple[int, int, int], box: tuple[int, int, int]) -> float:
    return round(100.0 * count * box[0] * box[1] * box[2] / (space[0] * space[1] * space[2]), 3)


def improvement_pct(new_count: int, old_count: int) -> float:
    if old_count <= 0:
        return 0.0
    return round(100.0 * (new_count - old_count) / old_count, 3)


def q(values: list[float], pct: float) -> float:
    if not values:
        return 0.0
    vals = sorted(values)
    pos = min(len(vals) - 1, max(0, math.ceil(pct / 100.0 * len(vals)) - 1))
    return vals[pos]


def near_divisor_dim(rng: random.Random, span: int, min_n: int, max_n: int, jitter: float) -> int:
    n = rng.randint(min_n, max_n)
    base = span / (n + rng.uniform(-jitter, jitter))
    return round5(base)


def awkward_dim(rng: random.Random, span: int, min_n: int, max_n: int) -> int:
    n = rng.randint(min_n, max_n)
    base = span / (n + rng.uniform(0.28, 0.62))
    return round5(base)


def gen_box(rng: random.Random, stratum: str) -> tuple[int, int, int]:
    if stratum == "small_dense":
        return ordered_dims([randint5(rng, 120, 320), randint5(rng, 120, 320), randint5(rng, 120, 320)])
    if stratum == "medium_carton":
        return ordered_dims([randint5(rng, 300, 900), randint5(rng, 240, 760), randint5(rng, 180, 650)])
    if stratum == "large_carton":
        return ordered_dims([randint5(rng, 900, 1800), randint5(rng, 600, 1400), randint5(rng, 400, 1100)])
    if stratum == "flat_panel":
        return ordered_dims([randint5(rng, 900, 2400), randint5(rng, 500, 1600), randint5(rng, 60, 180)])
    if stratum == "tall_column":
        return tall_upright_dims(rng)
    if stratum == "long_case":
        return ordered_dims([randint5(rng, 1500, 4200), randint5(rng, 120, 700), randint5(rng, 120, 700)])
    if stratum == "near_divisor":
        dims = [
            near_divisor_dim(rng, SPACE[0], 6, 42, 0.06),
            near_divisor_dim(rng, SPACE[1], 2, 10, 0.06),
            near_divisor_dim(rng, SPACE[2], 2, 12, 0.06),
        ]
        return ordered_dims([max(60, min(d, 4200)) for d in dims])
    if stratum == "awkward_remainder":
        dims = [
            awkward_dim(rng, SPACE[0], 5, 35),
            awkward_dim(rng, SPACE[1], 2, 9),
            awkward_dim(rng, SPACE[2], 2, 10),
        ]
        return ordered_dims([max(60, min(d, 4200)) for d in dims])
    if stratum == "mixed_scale":
        choice = rng.choice(["small_dense", "medium_carton", "large_carton", "flat_panel", "long_case"])
        return gen_box(rng, choice)
    if stratum == "realistic_carton":
        # Common e-commerce/export-carton-ish dimensions, with mild aspect ratio.
        long = randint5(rng, 350, 1200)
        mid = randint5(rng, 250, min(900, long))
        short = randint5(rng, 150, min(650, mid))
        return ordered_dims([long, mid, short])
    raise ValueError(stratum)


def dimension_profile(case: Case) -> str:
    if case.box_h > max(case.box_l, case.box_w):
        return "upright_tall_exception"
    if case.box_l >= case.box_w >= case.box_h:
        return "canonical_l_ge_w_ge_h"
    return "noncanonical"


def generate_cases(n_cases: int, seed: int) -> list[Case]:
    if n_cases % len(STRATA) != 0:
        raise ValueError(f"case count must be divisible by {len(STRATA)} strata")
    rng = random.Random(seed)
    cases: list[Case] = []
    per_stratum = n_cases // len(STRATA)
    case_no = 1
    for stratum in STRATA:
        made = 0
        while made < per_stratum:
            box = gen_box(rng, stratum)
            if best_single_grid(SPACE, box) <= 0:
                continue
            cases.append(Case(f"CLB40HQ-{case_no:05d}", stratum, *SPACE, *box))
            made += 1
            case_no += 1
    return cases


def run_packcli(packcli: Path, cases: list[Case], timeout_sec: int) -> list[tuple[int, int, int, int]]:
    stdin = "\n".join(
        f"{c.space_l} {c.space_w} {c.space_h} {c.box_l} {c.box_w} {c.box_h}" for c in cases
    ) + "\n"
    proc = subprocess.run(
        [str(packcli), "--t1-batch"],
        input=stdin.encode("ascii"),
        capture_output=True,
        timeout=timeout_sec,
    )
    if proc.returncode != 0:
        raise RuntimeError(
            f"PackCli failed rc={proc.returncode}: {proc.stderr.decode(errors='replace')[:1000]}"
        )
    rows: list[tuple[int, int, int, int]] = []
    for line in proc.stdout.decode("ascii", errors="replace").splitlines():
        line = line.strip()
        if not line:
            continue
        vals = tuple(int(x) for x in line.split())
        if len(vals) != 4:
            raise RuntimeError(f"unexpected PackCli output line: {line!r}")
        rows.append(vals)  # type: ignore[arg-type]
    if len(rows) != len(cases):
        raise RuntimeError(f"PackCli returned {len(rows)} rows for {len(cases)} cases")
    return rows


def summarize(rows: list[dict[str, object]], seed: int, packcli_used: str | None) -> dict[str, object]:
    t1_rows = [r for r in rows if r.get("t1_depth3_count") not in ("", None)]
    baseline_key = "one_cut_count"
    single_key = "single_grid_count"

    def diff_values(key: str) -> list[int]:
        return [int(r["t1_depth3_count"]) - int(r[key]) for r in t1_rows]

    def imp_values(key: str) -> list[float]:
        return [float(r[f"t1_vs_{key}_improvement_pct"]) for r in t1_rows]

    def depth_improvement_values(depth_key: str) -> list[float]:
        values = []
        for r in t1_rows:
            baseline = int(r[depth_key])
            values.append((int(r["t1_depth3_count"]) - baseline) / baseline * 100 if baseline else 0.0)
        return values

    orientation_diffs = [int(r["one_cut_count"]) - int(r["upright_one_cut_count"]) for r in rows]
    orientation_imps = [
        (int(r["one_cut_count"]) - int(r["upright_one_cut_count"])) / int(r["upright_one_cut_count"]) * 100
        if int(r["upright_one_cut_count"]) else 0.0
        for r in rows
    ]

    by_stratum = []
    for stratum in STRATA:
        part = [r for r in t1_rows if r["stratum"] == stratum]
        if not part:
            continue
        diffs = [int(r["t1_depth3_count"]) - int(r[baseline_key]) for r in part]
        imps = [float(r[f"t1_vs_{baseline_key}_improvement_pct"]) for r in part]
        fills = [float(r["t1_fill_pct"]) for r in part]
        by_stratum.append(
            {
                "stratum": stratum,
                "cases": len(part),
                "wins_vs_one_cut": sum(1 for d in diffs if d > 0),
                "ties_vs_one_cut": sum(1 for d in diffs if d == 0),
                "losses_vs_one_cut": sum(1 for d in diffs if d < 0),
                "avg_improvement_pct_vs_one_cut": round(statistics.fmean(imps), 3),
                "median_improvement_pct_vs_one_cut": round(statistics.median(imps), 3),
                "p95_improvement_pct_vs_one_cut": round(q(imps, 95), 3),
                "avg_t1_fill_pct": round(statistics.fmean(fills), 3),
            }
        )

    summary: dict[str, object] = {
        "benchmark": "single_sku_40hq_stratified",
        "date": REPORT_DATE,
        "seed": seed,
        "cases": len(rows),
        "container": CONTAINER_LABEL,
        "space_lwh_mm": list(SPACE),
        "strata": STRATA,
        "packcli_used": packcli_used,
        "packcli_cases": len(t1_rows),
        "case_count_per_stratum": len(rows) // len(STRATA),
        "dimension_policy": "Normal generated cartons use box_l >= box_w >= box_h. The tall_column stratum is an upright-product exception where box_h may be larger than box_l and box_w.",
        "normal_l_ge_w_ge_h_cases": sum(
            1 for r in rows if r["dimension_profile"] == "canonical_l_ge_w_ge_h"
        ),
        "tall_upright_exception_cases": sum(
            1 for r in rows if r["dimension_profile"] == "upright_tall_exception"
        ),
        "box_width_gt_length_cases": sum(1 for r in rows if int(r["box_w"]) > int(r["box_l"])),
        "box_height_gt_length_cases": sum(1 for r in rows if int(r["box_h"]) > int(r["box_l"])),
        "main_orientation_policy": "all_six_axis_aligned_orientations",
        "upright_baseline_policy": "upright_only_lwh_wlh_height_fixed",
        "all_six_one_cut_better_than_upright_cases": sum(1 for d in orientation_diffs if d > 0),
        "all_six_one_cut_same_as_upright_cases": sum(1 for d in orientation_diffs if d == 0),
        "all_six_one_cut_worse_than_upright_cases": sum(1 for d in orientation_diffs if d < 0),
        "avg_all_six_one_cut_gain_vs_upright_boxes": round(statistics.fmean(orientation_diffs), 3),
        "avg_all_six_one_cut_improvement_pct_vs_upright": round(statistics.fmean(orientation_imps), 3),
        "median_all_six_one_cut_improvement_pct_vs_upright": round(statistics.median(orientation_imps), 3),
        "p95_all_six_one_cut_improvement_pct_vs_upright": round(q(orientation_imps, 95), 3),
    }
    if t1_rows:
        one_cut_diffs = diff_values(baseline_key)
        one_cut_imps = imp_values(baseline_key)
        single_diffs = diff_values(single_key)
        single_imps = imp_values(single_key)
        depth2_diffs = [int(r["t1_depth3_count"]) - int(r["t1_depth2_count"]) for r in t1_rows]
        depth2_imps = depth_improvement_values("t1_depth2_count")
        fills = [float(r["t1_fill_pct"]) for r in t1_rows]
        summary.update(
            {
                "wins_vs_one_cut": sum(1 for d in one_cut_diffs if d > 0),
                "ties_vs_one_cut": sum(1 for d in one_cut_diffs if d == 0),
                "losses_vs_one_cut": sum(1 for d in one_cut_diffs if d < 0),
                "avg_improvement_pct_vs_one_cut": round(statistics.fmean(one_cut_imps), 3),
                "median_improvement_pct_vs_one_cut": round(statistics.median(one_cut_imps), 3),
                "p90_improvement_pct_vs_one_cut": round(q(one_cut_imps, 90), 3),
                "p95_improvement_pct_vs_one_cut": round(q(one_cut_imps, 95), 3),
                "max_improvement_pct_vs_one_cut": round(max(one_cut_imps), 3),
                "avg_box_gain_vs_one_cut": round(statistics.fmean(one_cut_diffs), 3),
                "wins_vs_single_grid": sum(1 for d in single_diffs if d > 0),
                "ties_vs_single_grid": sum(1 for d in single_diffs if d == 0),
                "losses_vs_single_grid": sum(1 for d in single_diffs if d < 0),
                "avg_improvement_pct_vs_single_grid": round(statistics.fmean(single_imps), 3),
                "median_improvement_pct_vs_single_grid": round(statistics.median(single_imps), 3),
                "p95_improvement_pct_vs_single_grid": round(q(single_imps, 95), 3),
                "wins_vs_depth2": sum(1 for d in depth2_diffs if d > 0),
                "ties_vs_depth2": sum(1 for d in depth2_diffs if d == 0),
                "losses_vs_depth2": sum(1 for d in depth2_diffs if d < 0),
                "avg_improvement_pct_vs_depth2": round(statistics.fmean(depth2_imps), 3),
                "median_improvement_pct_vs_depth2": round(statistics.median(depth2_imps), 3),
                "p95_improvement_pct_vs_depth2": round(q(depth2_imps, 95), 3),
                "avg_t1_fill_pct": round(statistics.fmean(fills), 3),
                "median_t1_fill_pct": round(statistics.median(fills), 3),
                "p95_t1_fill_pct": round(q(fills, 95), 3),
                "by_stratum": by_stratum,
            }
        )
    return summary


def markdown_report(summary: dict[str, object], top_rows: list[dict[str, object]]) -> str:
    packcli_used = summary.get("packcli_used") or "not run"
    lines = [
        "# 10,000-Case Single-SKU Container Loading Benchmark",
        "",
        f"Date: {REPORT_DATE}",
        "",
        "This folder contains a deterministic, stratified random benchmark corpus for single-SKU carton loading into a fixed 40HQ-style space. Normal generated carton dimensions are named as package-style `length >= width >= height`; the `tall_column` class represents upright-product exceptions where height can be largest.",
        "",
        "## Reproduction",
        "",
        "```powershell",
        "python PublicSites/dev/container_loading_benchmark_generator.py",
        "```",
        "",
        "The generator uses only Python standard-library modules. By default it also runs local `SP3/Pack/x64/Release/PackCli.exe --t1-batch` when available. Use `--skip-packcli` to regenerate only the dataset and transparent heuristic baselines.",
        "",
        "## Configuration",
        "",
        f"- Seed: `{summary['seed']}`",
        f"- Cases: `{summary['cases']}`",
        f"- Container: `{summary['container']}`",
        f"- Space L/W/H: `{summary['space_lwh_mm']}` mm",
        f"- Cases per stratum: `{summary['case_count_per_stratum']}`",
        f"- PackCli: `{packcli_used}`",
        f"- Dimension policy: `{summary['dimension_policy']}`",
        f"- Main orientation policy: `{summary['main_orientation_policy']}`",
        f"- Upright baseline policy: `{summary['upright_baseline_policy']}`",
        f"- Normal `L >= W >= H` cases: `{summary['normal_l_ge_w_ge_h_cases']}`",
        f"- Tall upright-exception cases: `{summary['tall_upright_exception_cases']}`",
        f"- `box_w > box_l` cases: `{summary['box_width_gt_length_cases']}`",
        f"- `box_h > box_l` cases: `{summary['box_height_gt_length_cases']}`",
        "",
        "Strata:",
        "",
    ]
    for stratum in summary["strata"]:  # type: ignore[index]
        lines.append(f"- `{stratum}`")

    lines.extend(
        [
            "",
            "## Baselines",
            "",
            "- `volume_upper_bound`: raw volume-only upper bound. This ignores geometry, so it is not necessarily reachable.",
            "- `single_grid_count`: best one-orientation full grid over all six axis-aligned carton orientations.",
            "- `one_cut_count`: transparent six-orientation heuristic that makes one guillotine slab split, packs one side as a grid, and packs the remaining slab by its best single grid.",
            "- `upright_single_grid_count` and `upright_one_cut_count`: the same transparent baselines with the carton height fixed vertical, allowing only `LWH` and `WLH`.",
            "- `t1_depth0_count` through `t1_depth3_count`: local SmartPacker T1 six-orientation batch counts from `PackCli --t1-batch` when available. `t1_depth3_count` is used as the strongest local T1 count in the summary.",
            "",
            "The headline benchmark does not compare a six-orientation SmartPacker result against an upright-only competitor score. Upright-only baseline columns are included so readers can see how much orientation policy changes the answer.",
            "",
            "## T1 Depth Notes",
            "",
            "This benchmark reports local count-only `PackCli --t1-batch` depth-3 results. It is not a claim that the public MCP placed-layout endpoint currently runs depth 3 by default.",
            "",
            f"Current product behavior as of {REPORT_DATE}:",
            "",
            "- T1 desktop and browser app code uses the SP30 depth-3 path internally for maximum-count calculations and max-layout generation.",
            "- T1 MCP placed-layout service currently stays on depth 2 for public reliability. The native `t1_pack` path can be staged with `T1_DEPTH=3`, but depth 3 was too slow for some live layout cases under the current public timeout.",
            "- Depth 3 will be released in the downloadable T1 desktop app and a local CLI first, so readers can reproduce the count benchmark on their own PC and inspect selected loading plans locally. Public MCP remains the depth-2 live-layout service.",
            "",
            "## Summary",
            "",
        ]
    )
    if summary.get("packcli_cases"):
        lines.extend(
            [
                "| Metric | Value |",
                "|---|---:|",
                f"| PackCli cases | `{summary['packcli_cases']}` |",
                f"| Normal `L >= W >= H` cases | `{summary['normal_l_ge_w_ge_h_cases']}` |",
                f"| Tall upright-exception cases | `{summary['tall_upright_exception_cases']}` |",
                f"| Wins vs one-cut heuristic | `{summary['wins_vs_one_cut']}` |",
                f"| Ties vs one-cut heuristic | `{summary['ties_vs_one_cut']}` |",
                f"| Losses vs one-cut heuristic | `{summary['losses_vs_one_cut']}` |",
                f"| Average improvement vs one-cut | `{summary['avg_improvement_pct_vs_one_cut']}%` |",
                f"| Median improvement vs one-cut | `{summary['median_improvement_pct_vs_one_cut']}%` |",
                f"| P90 improvement vs one-cut | `{summary['p90_improvement_pct_vs_one_cut']}%` |",
                f"| P95 improvement vs one-cut | `{summary['p95_improvement_pct_vs_one_cut']}%` |",
                f"| Wins vs single-grid | `{summary['wins_vs_single_grid']}` |",
                f"| Average improvement vs single-grid | `{summary['avg_improvement_pct_vs_single_grid']}%` |",
                f"| Wins vs T1 depth 2 | `{summary['wins_vs_depth2']}` |",
                f"| Average improvement vs T1 depth 2 | `{summary['avg_improvement_pct_vs_depth2']}%` |",
                f"| Median T1 fill | `{summary['median_t1_fill_pct']}%` |",
                f"| Six-orientation one-cut better than upright one-cut | `{summary['all_six_one_cut_better_than_upright_cases']}` |",
                f"| Average six-orientation one-cut gain vs upright one-cut | `{summary['avg_all_six_one_cut_improvement_pct_vs_upright']}%` |",
                "",
                "## By Stratum",
                "",
                "| Stratum | Cases | Wins | Ties | Losses | Avg Improvement | Median Improvement | P95 Improvement | Avg T1 Fill |",
                "|---|---:|---:|---:|---:|---:|---:|---:|---:|",
            ]
        )
        for row in summary["by_stratum"]:  # type: ignore[index]
            lines.append(
                f"| `{row['stratum']}` | `{row['cases']}` | `{row['wins_vs_one_cut']}` | "
                f"`{row['ties_vs_one_cut']}` | `{row['losses_vs_one_cut']}` | "
                f"`{row['avg_improvement_pct_vs_one_cut']}%` | "
                f"`{row['median_improvement_pct_vs_one_cut']}%` | "
                f"`{row['p95_improvement_pct_vs_one_cut']}%` | `{row['avg_t1_fill_pct']}%` |"
            )
        lines.extend(
            [
                "",
                "## Largest T1 Gains Vs Six-Orientation One-Cut",
                "",
                "| Case | Stratum | Box L/W/H | One-cut | T1 depth3 | Gain | Improvement | T1 fill |",
                "|---|---|---:|---:|---:|---:|---:|---:|",
            ]
        )
        for row in top_rows[:20]:
            lines.append(
                f"| `{row['case_id']}` | `{row['stratum']}` | "
                f"`{row['box_l']} x {row['box_w']} x {row['box_h']}` | "
                f"`{row['one_cut_count']}` | `{row['t1_depth3_count']}` | "
                f"`{row['t1_vs_one_cut_gain']}` | "
                f"`{row['t1_vs_one_cut_count_improvement_pct']}%` | `{row['t1_fill_pct']}%` |"
            )
    else:
        lines.append("PackCli was not run; the CSV contains generated cases plus heuristic baselines only.")

    lines.extend(
        [
            "",
            "## Marketing Use",
            "",
            "This is stronger than a single flagship case because the case generator is fixed and the whole CSV is published. Website copy should still avoid universal claims. The formal score compares six-orientation SmartPacker T1 with six-orientation transparent baselines. Competitor spot checks should be labeled orientation-unknown unless the competitor exposes and confirms the same orientation policy.",
            "",
            "Suggested careful claim shape:",
            "",
            "> On a deterministic 10,000-case single-SKU 40HQ benchmark with package-style carton dimensions and six axis-aligned orientations allowed, SmartPacker T1 batch results beat a transparent one-cut guillotine heuristic on N cases, tied on M cases, and averaged X% more cartons. We publish the generator, all input cases, and upright-only baseline columns.",
            "",
            "Replace `N`, `M`, and `X` from the summary above when using the generated results.",
            "",
            "## Files",
            "",
            "- `single_sku_40hq_cases_10000_seed20260619.csv`: generated cases, six-orientation baselines, upright-only baselines, and optional PackCli counts.",
            "- `single_sku_40hq_summary_10000_seed20260619.json`: machine-readable summary.",
            "- `README.md`: this report.",
            "",
            "## Caveats",
            "",
            "- This is a single-SKU carton benchmark. It is not a mixed-SKU, palletized, axle-weight, fragile-cargo, or loading-sequence benchmark.",
            "- The headline SmartPacker T1 comparisons allow all six axis-aligned orientations. If a competitor assumes upright-only loading, use the upright baseline columns as context and do not treat the six-orientation T1 delta as a like-for-like competitor score.",
            "- The generator names normal boxes as `box_l >= box_w >= box_h`; `tall_column` intentionally models tall upright-product exceptions.",
            "- The 10,000-case batch uses local `PackCli --t1-batch`, not 10,000 public MCP calls. Public MCP should be used for spot-checks and shareable examples, not bulk load generation.",
            "- Public competitor websites should be compared on a smaller no-login subset to avoid fragile automation, account requirements, rate-limit problems, and unclear orientation controls.",
            "",
        ]
    )
    return "\n".join(lines)


def write_outputs(out_dir: Path, rows: list[dict[str, object]], summary: dict[str, object]) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    csv_path = out_dir / f"single_sku_40hq_cases_{len(rows)}_seed{summary['seed']}.csv"
    json_path = out_dir / f"single_sku_40hq_summary_{len(rows)}_seed{summary['seed']}.json"
    md_path = out_dir / "README.md"

    fieldnames = list(rows[0].keys())
    with csv_path.open("w", newline="", encoding="utf-8") as fh:
        writer = csv.DictWriter(fh, fieldnames=fieldnames, lineterminator="\r\n")
        writer.writeheader()
        writer.writerows(rows)

    json_path.write_bytes((json.dumps(summary, indent=2) + "\n").encode("utf-8"))

    top_rows = sorted(
        [r for r in rows if r.get("t1_vs_one_cut_gain") not in ("", None)],
        key=lambda r: (int(r["t1_vs_one_cut_gain"]), float(r["t1_vs_one_cut_count_improvement_pct"])),
        reverse=True,
    )
    md_path.write_bytes(markdown_report(summary, top_rows).replace("\n", "\r\n").encode("utf-8"))


def build_rows(cases: list[Case], packcli_counts: list[tuple[int, int, int, int]] | None) -> list[dict[str, object]]:
    rows: list[dict[str, object]] = []
    for i, case in enumerate(cases):
        box = case.box
        space = case.space
        upper = volume_upper_bound(space, box)
        single = best_single_grid(space, box, "all_six")
        one_cut = best_one_cut(space, box, "all_six")
        upright_single = best_single_grid(space, box, "upright_only")
        upright_one_cut = best_one_cut(space, box, "upright_only")
        row: dict[str, object] = {
            "case_id": case.case_id,
            "stratum": case.stratum,
            "dimension_profile": dimension_profile(case),
            "container": CONTAINER_LABEL,
            "space_l": case.space_l,
            "space_w": case.space_w,
            "space_h": case.space_h,
            "box_l": case.box_l,
            "box_w": case.box_w,
            "box_h": case.box_h,
            "box_volume_m3": round(case.box_l * case.box_w * case.box_h / 1_000_000_000, 9),
            "volume_upper_bound": upper,
            "single_grid_count": single,
            "single_grid_fill_pct": fill_pct(single, space, box),
            "one_cut_count": one_cut,
            "one_cut_fill_pct": fill_pct(one_cut, space, box),
            "one_cut_vs_single_grid_gain": one_cut - single,
            "one_cut_vs_single_grid_improvement_pct": improvement_pct(one_cut, single),
            "upright_single_grid_count": upright_single,
            "upright_single_grid_fill_pct": fill_pct(upright_single, space, box),
            "upright_one_cut_count": upright_one_cut,
            "upright_one_cut_fill_pct": fill_pct(upright_one_cut, space, box),
            "all_six_one_cut_vs_upright_one_cut_gain": one_cut - upright_one_cut,
            "all_six_one_cut_vs_upright_one_cut_improvement_pct": improvement_pct(one_cut, upright_one_cut),
        }
        if packcli_counts is not None:
            d0, d1, d2, d3 = packcli_counts[i]
            row.update(
                {
                    "t1_depth0_count": d0,
                    "t1_depth1_count": d1,
                    "t1_depth2_count": d2,
                    "t1_depth3_count": d3,
                    "t1_fill_pct": fill_pct(d3, space, box),
                    "t1_gap_to_volume_upper_bound": upper - d3,
                    "t1_vs_single_grid_gain": d3 - single,
                    "t1_vs_single_grid_count_improvement_pct": improvement_pct(d3, single),
                    "t1_vs_one_cut_gain": d3 - one_cut,
                    "t1_vs_one_cut_count_improvement_pct": improvement_pct(d3, one_cut),
                }
            )
        else:
            row.update(
                {
                    "t1_depth0_count": "",
                    "t1_depth1_count": "",
                    "t1_depth2_count": "",
                    "t1_depth3_count": "",
                    "t1_fill_pct": "",
                    "t1_gap_to_volume_upper_bound": "",
                    "t1_vs_single_grid_gain": "",
                    "t1_vs_single_grid_count_improvement_pct": "",
                    "t1_vs_one_cut_gain": "",
                    "t1_vs_one_cut_count_improvement_pct": "",
                }
            )
        rows.append(row)
    return rows


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--cases", type=int, default=N_CASES)
    parser.add_argument("--seed", type=int, default=SEED)
    parser.add_argument("--out-dir", type=Path, default=DEFAULT_OUT_DIR)
    parser.add_argument("--packcli", type=Path, default=DEFAULT_PACKCLI)
    parser.add_argument("--packcli-timeout-sec", type=int, default=600)
    parser.add_argument("--skip-packcli", action="store_true")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    cases = generate_cases(args.cases, args.seed)

    packcli_counts: list[tuple[int, int, int, int]] | None = None
    packcli_used: str | None = None
    if not args.skip_packcli:
        if not args.packcli.exists():
            print(f"PackCli not found: {args.packcli}; generating heuristic-only CSV", file=sys.stderr)
        else:
            print(f"Running {args.packcli} --t1-batch for {len(cases)} cases...")
            packcli_counts = run_packcli(args.packcli, cases, args.packcli_timeout_sec)
            packcli_used = str(args.packcli)

    rows = build_rows(cases, packcli_counts)
    summary = summarize(rows, args.seed, packcli_used)
    write_outputs(args.out_dir, rows, summary)
    print(json.dumps(summary, indent=2))
    return 0


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