"""Convert the Steps tree from a ROOT file to Parquet.

See `uv run dwarf convert --help` for the CLI.
"""

from pathlib import Path
from typing import Literal

import awkward as ak
import polars as pl
import uproot

ParquetCompression = Literal["lz4", "uncompressed", "snappy", "gzip", "brotli", "zstd"]


def _add_secondary_energy(df: pl.DataFrame) -> pl.DataFrame:
    """Add per-step `e_sec`: total initial kinetic energy of the secondaries born in it.

    Each secondary's creation energy is the `pre_E` of that child track's first step
    (min `step_no`) in the same event, so for a parent step
    `e_sec = Σ over child_track_ids of the child track's first-step pre_E`. Steps that
    spawn nothing get 0.0. The full event must be present in `df` (it is — the writer
    concatenates every batch before this runs), since a child track's first step can
    live in a different read batch than its parent step.
    """
    first_E = (
        df.sort("step_no")
        .group_by(["event_id", "track_id"])
        .agg(pl.col("pre_E").first().alias("child_E"))
        .rename({"track_id": "child_track_id"})
    )

    exploded = (
        df.select(["event_id", "child_track_ids"])
        .with_row_index("_step_row")
        .explode("child_track_ids")
        .rename({"child_track_ids": "child_track_id"})
        .drop_nulls("child_track_id")  # steps with no children explode to a null row
    )
    summed = (
        exploded.join(first_E, on=["event_id", "child_track_id"], how="left")
        .group_by("_step_row")
        .agg(pl.col("child_E").sum().alias("e_sec"))
    )
    return (
        df.with_row_index("_step_row")
        .join(summed, on="_step_row", how="left")
        .with_columns(pl.col("e_sec").fill_null(0.0).cast(pl.Float64))
        .drop("_step_row")
    )


def _batch_to_polars(batch: ak.Array) -> pl.DataFrame:
    """Convert one awkward-array batch to a Polars DataFrame.

    Flat numeric/string fields are converted via numpy; variable-length fields
    (like child_track_ids) fall back to Python lists so polars stores them as
    List columns — a type parquet understands natively.
    """
    col_dict: dict = {}
    for field in ak.fields(batch):
        arr = batch[field]
        if arr.ndim == 1 and not isinstance(arr.layout, ak.contents.ListOffsetArray):
            col_dict[field] = ak.to_numpy(arr)
        else:
            col_dict[field] = ak.to_list(arr)
    return pl.DataFrame(col_dict)


def convert_steps_to_parquet(
    root_path: str | Path,
    output_path: str | Path | None = None,
    batch_size: str = "100 MB",
    tree_name: str = "Steps",
    compression: ParquetCompression = "snappy",
) -> Path:
    """Read *tree_name* from *root_path* and write it to a Parquet file.

    Reads in batches of *batch_size* so that peak ROOT-deserialization memory
    stays bounded. All batches are collected as Polars DataFrames and written
    in a single pass at the end (Polars' parquet writer does not support
    row-group appending without pyarrow).

    Parameters
    ----------
    root_path:   Input ROOT file.
    output_path: Output Parquet file. Defaults to *root_path* with .parquet suffix.
    batch_size:  Uproot read batch size — an uproot size string ("100 MB") or
                 integer row count (500_000).
    tree_name:   Name of the TTree inside the ROOT file.
    compression: Parquet compression codec (snappy | lz4 | zstd | gzip | none).
    """
    root_path = Path(root_path)
    if output_path is None:
        output_path = root_path.with_suffix(".parquet")
    else:
        output_path = Path(output_path)

    with uproot.open(root_path) as f:
        tree = f[tree_name]
        n_entries = tree.num_entries
        print(f"Reading '{tree_name}' from {root_path.name}  ({n_entries} entries)")

        batches: list[pl.DataFrame] = []
        rows_done = 0
        for batch in tree.iterate(library="ak", step_size=batch_size):
            batches.append(_batch_to_polars(batch))
            rows_done += len(batch)
            print(f"  {rows_done:,} / {n_entries:,} rows read", end="\r", flush=True)

    df = pl.concat(batches)
    # Steps tree carries the parent→child links needed to derive secondary energy;
    # other trees (e.g. Hits) don't, so only augment when the column is present.
    if "child_track_ids" in df.columns:
        print("\nComputing per-step secondary energy (e_sec) …", end=" ", flush=True)
        df = _add_secondary_energy(df)

    print(f"\nWriting {output_path} …", end=" ", flush=True)
    df.write_parquet(output_path, compression=compression)
    print(f"done  ({output_path.stat().st_size / 1e6:.1f} MB)")
    return output_path
