import argparse
import sys
import os
import datetime
import logging
import glob
from pathlib import Path

# Fix OpenMP runtime conflict on macOS (common with PyTorch/Anaconda)
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"

# from macer import __version__
from macer.defaults import DEFAULT_DEVICE, resolve_model_path, DEFAULT_FF, DEFAULT_MODELS
from macer.calculator.factory import ALL_SUPPORTED_FFS

MACER_LOGO = r"""
███╗   ███╗  █████╗   ██████╗ ███████╗ ██████╗ 
████╗ ████║ ██╔══██╗ ██╔════╝ ██╔════╝ ██╔══██╗
██╔████╔██║ ███████║ ██║      █████╗   ██████╔╝
██║╚██╔╝██║ ██╔══██║ ██║      ██╔══╝   ██╔══██╗
██║ ╚═╝ ██║ ██║  ██║ ╚██████╗ ███████╗ ██║  ██║
╚═╝     ╚═╝ ╚═╝  ╚═╝  ╚═════╝ ╚══════╝ ╚═╝  ╚═╝
macer: ML-accelerated Atomic Computational Environment for Research
"""

def _add_common_train_args(parser, default_lr=None, lr_help="Learning rate.", default_weights=(1.0, 1.0, 0.1)):
    """Add arguments common to both MatterSim and MACE training with grouping."""
    e_w, f_w, s_w = default_weights
    
    # 1. Data & Output Group
    io_group = parser.add_argument_group("Input & Output Options")
    io_group.add_argument("--data", "-d", type=str, help="Path to input dataset for auto-splitting (VASP ML_AB, .xyz, etc.).")
    io_group.add_argument("--train-data", "-t", type=str, default=None, help="Explicitly specify training data. Overrides --data.")
    io_group.add_argument("--valid-data", "-v", type=str, default=None, help="Explicitly specify validation data. Overrides --data splitting.")
    io_group.add_argument("--save-path", type=str, default="./mlff_results", help="Directory to save results (default: ./mlff_results).")
    io_group.add_argument("--model-name", type=str, default=None, help="Output filename for the trained model.")
    
    # 2. Training Loop Group
    loop_group = parser.add_argument_group("Training Loop Options")
    loop_group.add_argument("--epochs", type=int, default=100, help="Number of training epochs (default: 100).")
    loop_group.add_argument("--batch-size", type=int, default=16, help="Batch size (default: 16).")
    loop_group.add_argument("--lr", type=float, default=default_lr, help=lr_help)
    loop_group.add_argument("--patience", type=int, default=20, help="Early stopping patience (default: 20).")
    loop_group.add_argument("--seed", type=int, default=42, help="Random seed (default: 42).")
    
    # 3. Loss Weights Group
    loss_group = parser.add_argument_group("Loss Weights & Objective Options")
    loss_group.add_argument("--energy-weight", type=float, default=e_w, help=f"Loss weight for energy (default: {e_w}).")
    loss_group.add_argument("--forces-weight", type=float, default=f_w, help=f"Loss weight for forces (default: {f_w}).")
    loss_group.add_argument("--stress-weight", type=float, default=s_w, help=f"Loss weight for stress (default: {s_w}).")
    loss_group.add_argument("--no-stresses", action="store_true", help="Disable stress training.")
    
    # 4. Thresholds Group
    threshold_group = parser.add_argument_group("Stop Thresholds (MAE based early-exit)")
    threshold_group.add_argument("--mae-energy", type=float, default=None, 
                                 help="Stop if MAE Energy (eV/atom) < threshold. Default: Disabled.")
    threshold_group.add_argument("--mae-force", type=float, default=None, 
                                 help="Stop if MAE Force (eV/A) < threshold. Default: Disabled.")
    threshold_group.add_argument("--mae-stress", type=float, default=None, 
                                 help="Stop if MAE Stress (eV/A^3) < threshold. Default: Disabled.")

    # 5. Computing Group
    compute_group = parser.add_argument_group("Optimization & Device Options")
    compute_group.add_argument("--device", type=str, default=DEFAULT_DEVICE, choices=["cpu", "cuda", "mps"], help=f"Device (default: {DEFAULT_DEVICE}).")
    compute_group.add_argument("--dtype", type=str, default=None, choices=["float32", "float64"], help="Data type.")

    return {
        "io": io_group,
        "loop": loop_group,
        "loss": loss_group,
        "threshold": threshold_group,
        "compute": compute_group
    }

def print_util_banner(category, action=None, ff=None, model=None):
    from macer import __version__
    """Prints a consistent banner for util commands."""
    print(MACER_LOGO)
    print(f"  Version: {__version__}")
    
    cmd_str = f"util {category}"
    if action: cmd_str += f" {action}"
    print(f"  Command: {cmd_str}")
    
    if ff:
        model_display = model if model else DEFAULT_MODELS.get(ff, "Default")
        print(f"  Model  : {ff.upper()} ({model_display})")
        
    print(f"  Web    : https://github.com/soungmin-bae/macer")
    print("-" * 50 + "\n")

def add_util_parsers(subparsers):
    """Adds all util subcommands to the provided subparsers object."""
    from macer import __version__
    
    # ==========================================
    # Group 1: Model Management (Download & Local)
    # ==========================================

    # --- 1. Model Download/Provisioning Category ---
    gm_parser = subparsers.add_parser(
        "get-model", 
        aliases=["gm"],
        description=MACER_LOGO + f"\nmacer util gm (v{__version__}): Download and manage pre-trained MLFF models.\nSupports automatic provisioning for SevenNet, MACE, and MatterSim.",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    gm_parser.add_argument("--ff", choices=ALL_SUPPORTED_FFS, help="Filter models by force field.")
    gm_parser.add_argument("--model", help="Specific model name to download (or 'all').")
    gm_parser.add_argument("--replace", action="store_true", help="Force re-download existing models.")

    # --- 2. Model Category ---
    model_parser = subparsers.add_parser(
        "model", 
        description=MACER_LOGO + "\nmacer util model: MLFF model management.",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    model_subparsers = model_parser.add_subparsers(dest="action", help="Model actions")

    # model fp32
    fp32_parser = model_subparsers.add_parser("fp32", help="Convert model to float32 precision")
    fp32_parser.add_argument("-i", "--input", required=True, help="Input model file (.pth or .model)")
    fp32_parser.add_argument("-o", "--output", help="Output model file (optional)")

    # model list
    model_subparsers.add_parser("list", help="List available models in mlff-model/")

    # model compile (new)
    compile_parser = model_subparsers.add_parser("compile", help="Compile a MACE checkpoint (.pt) into a full model (.model)")
    compile_parser.add_argument("-i", "--input", required=True, help="Input checkpoint file (.pt)")
    compile_parser.add_argument("-o", "--output", help="Output model file (.model)")
    compile_parser.add_argument("-s", "--size", default="small", choices=["small", "medium", "large"], help="Model size used during training (default: small)")

    # ==========================================
    # Group 1.5: Pydefect Utilities
    # ==========================================

    pydefect_parser = subparsers.add_parser(
        "pydefect",
        description=MACER_LOGO + "\nmacer util pydefect: Pydefect helper utilities (interstitial, cpd plot).",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        help=argparse.SUPPRESS
    )
    pydefect_subparsers = pydefect_parser.add_subparsers(dest="action", help="Pydefect actions")

    # pydefect find-interstitial
    fi_parser = pydefect_subparsers.add_parser(
        "find-interstitial",
        aliases=["fi"],
        help="Find interstitial sites and write volumetric_data_local_extrema-like JSON."
    )
    fi_parser.add_argument("-i", "--input", required=True, help="Input POSCAR or CIF file.")
    fi_parser.add_argument("--min-dist", type=float, default=0.5, help="Minimum distance for Voronoi interstitials (default: 0.5).")
    fi_parser.add_argument("--species", type=str, default="H", help="Insert species for Voronoi generator (default: H).")
    fi_parser.add_argument("-o", "--output-dir", type=str, default=".", help="Output directory (default: current).")

    # pydefect manual-supercell
    sc_parser = pydefect_subparsers.add_parser(
        "supercell",
        aliases=["sc"],
        help="Create supercell_info.json without conventional cell conversion (manual supercell).",
        epilog=(
            "Examples:\n"
            "  macer util pydefect supercell -p POSCAR --dim 3 3 3\n"
            "  macer util pydefect supercell -p POSCAR --dim 1 1 0 0 1 0 0 0 1\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    sc_parser.add_argument("-p", "--poscar", help="Input POSCAR file.")
    sc_parser.add_argument("--dim", nargs="+", type=int, help="Supercell matrix (3 or 9 integers).")
    sc_parser.add_argument("--no-symmetry", dest="analyze_symmetry", action="store_false",
                           help="Disable symmetry reduction (keep all sites).")
    sc_parser.set_defaults(analyze_symmetry=True)
    sc_parser.add_argument("-o", "--output-dir", type=str, default=".", help="Output directory (default: current).")

    # pydefect cpd
    cpd_parser = pydefect_subparsers.add_parser(
        "cpd",
        help="Generate CPD (chem_pot_diag.json) and plot cpd.pdf/cpd.html.",
        epilog=(
            "Examples:\n"
            "  macer util pydefect cpd -r relative_energies.yaml -t MgAl2O4\n"
            "  macer util pydefect cpd -r relative_energies.yaml -t MgAl2O4 --projection\n"
            "  macer util pydefect cpd -r relative_energies.yaml -t KZr2(PO4)3 --projection-elements K Zr\n"
            "  macer util pydefect cpd -r relative_energies.yaml -t KZr2(PO4)3 --plot-elements K Zr\n"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    cpd_parser.add_argument("-r", "--relative-energies", default="relative_energies.yaml", help="relative_energies.yaml path.")
    cpd_parser.add_argument("-t", "--target", help="Target formula (e.g., MgAl2O4).")
    cpd_parser.add_argument("-e", "--elements", nargs="+", help="Elements to construct CPD (e.g., Mg Al O).")
    cpd_parser.add_argument("--plot-elements", nargs="+", help="Element order for plotting (must be exactly 2 elements).")
    cpd_parser.add_argument(
        "--projection",
        action="store_true",
        help="Generate 2D projections for all element pairs (ignored for binary systems)."
    )
    cpd_parser.add_argument(
        "--projection-elements",
        nargs=2,
        metavar=("E1", "E2"),
        help="Generate a 2D projection for the specified element pair (e.g., K Zr)."
    )
    cpd_parser.add_argument("-o", "--output-dir", type=str, default=".", help="Output directory (default: current).")

    # Top-level aliases for pydefect actions
    fi_alias = subparsers.add_parser(
        "find-interstitial",
        aliases=["fi"],
        help="(Alias) Pydefect: find interstitial sites."
    )
    fi_alias.add_argument("-i", "--input", help="Input POSCAR or CIF file.")
    fi_alias.add_argument("--min-dist", type=float, default=0.5, help="Minimum distance for Voronoi interstitials (default: 0.5).")
    fi_alias.add_argument("--species", type=str, default="H", help="Insert species for Voronoi generator (default: H).")
    fi_alias.add_argument("-o", "--output-dir", type=str, default=".", help="Output directory (default: current).")

    sc_alias = subparsers.add_parser(
        "supercell",
        aliases=["sc"],
        help="(Alias) Pydefect: create supercell_info.json (manual supercell).",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    sc_alias.add_argument("-p", "--poscar", help="Input POSCAR file.")
    sc_alias.add_argument("--dim", nargs="+", type=int, help="Supercell matrix (3 or 9 integers).")
    sc_alias.add_argument("--no-symmetry", dest="analyze_symmetry", action="store_false",
                          help="Disable symmetry reduction (keep all sites).")
    sc_alias.set_defaults(analyze_symmetry=True)
    sc_alias.add_argument("-o", "--output-dir", type=str, default=".", help="Output directory (default: current).")

    cpd_alias = subparsers.add_parser(
        "cpd",
        help="(Alias) Pydefect: generate CPD (chem_pot_diag.json) and plot cpd.pdf/cpd.html.",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    cpd_alias.add_argument("-r", "--relative-energies", default="relative_energies.yaml", help="relative_energies.yaml path.")
    cpd_alias.add_argument("-t", "--target", help="Target formula (e.g., MgAl2O4).")
    cpd_alias.add_argument("-e", "--elements", nargs="+", help="Elements to construct CPD (e.g., Mg Al O).")
    cpd_alias.add_argument("--plot-elements", nargs="+", help="Element order for plotting (must be exactly 2 elements).")
    cpd_alias.add_argument("--projection", action="store_true", help="Generate 2D projections for all element pairs (ignored for binary systems).")
    cpd_alias.add_argument("--projection-elements", nargs=2, metavar=("E1", "E2"),
                           help="Generate a 2D projection for the specified element pair (e.g., K Zr).")
    cpd_alias.add_argument("-o", "--output-dir", type=str, default=".", help="Output directory (default: current).")

    # ==========================================
    # Group 2: Training & Fine-tuning
    # ==========================================

    # --- 3. Fine-tunning (MatterSim) ---
    ft_parser = subparsers.add_parser(
        "fine-tunning",
        aliases=["ft"],
        description=MACER_LOGO + f"\nmacer util ft (v{__version__}): Specialized workflow for fine-tunning MatterSim foundation models.\nRefines pre-trained models using DFT data and performs automatic evaluation.",
        epilog="""
Examples:
  1. Standard fine-tuning with auto-splitting (8:1:1) and auto-evaluation:
     macer util ft -d dataset.xyz --epochs 100

  2. Use 100%% of data for training (no test set) with a specific base model:
     macer util ft -d dataset.xyz --full-train --model ./base_model.pth

  3. Explicitly specify training and validation data (differential LRs):
     macer util ft -t train.xyz -v valid.xyz --head-lr 1e-3 --backbone-lr 1e-5

  4. Fast verification run on CPU with a custom output model name:
     macer util ft -d dataset.xyz --epochs 2 --model-name my_model.pth --device cpu

  5. Fine-tuning when stress data is missing (e.g., ISIF=0 or OUTCAR-based XYZ):
     macer util ft -d dataset.xyz --epochs 10 --no-stresses

MatterSim Fine-tunning Strategy:
  - Higher Head LR (e.g., 1e-3) and lower Backbone LR (e.g., 1e-5) is recommended for small datasets.
  - Use --reset-head to re-initialize the predictive head if the chemical space is very different.
        """,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    ft_groups = _add_common_train_args(
        ft_parser, 
        default_lr=2e-4, 
        lr_help="Learning rate (default: 2e-4).",
        default_weights=(1.0, 1.0, 0.1)
    )
    ft_groups["io"].add_argument("--model", type=str, default="mattersim-v1.0.0-1M.pth", help="Base model to fine-tune (e.g., mattersim-v1.0.0-1M.pth).")
    ft_groups["io"].add_argument("--test-data", type=str, default=None, help="Optional test set for automatic evaluation after training.")
    ft_groups["io"].add_argument("--ratio", type=float, nargs=3, default=[0.8, 0.1, 0.1], help="Train/Valid/Test ratios for auto-splitting (default: 0.8 0.1 0.1).")
    ft_groups["io"].add_argument("--full-train", action="store_true", help="Use 100%% of data for training (no splitting, no validation, no test).")
    
    ms_train_group = ft_parser.add_argument_group("MatterSim Fine-tunning Strategy")
    ms_train_group.add_argument("--ensemble", type=int, default=1, help="Number of ensemble models.")
    ms_train_group.add_argument("--reset-head", action="store_true", help="Reset predictive head weights.")
    ms_train_group.add_argument("--head-lr", type=float, default=None, help="LR for head.")
    ms_train_group.add_argument("--backbone-lr", type=float, default=None, help="LR for backbone.")

    # --- 3.5 Fine-tuning Data Convert (ML_AB -> extxyz) ---
    ft_convert_parser = subparsers.add_parser(
        "ft-convert",
        aliases=["ftc"],
        description=MACER_LOGO + "\nConvert VASP ML_AB to Extended XYZ for MatterSim fine-tuning.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Convert ML_AB to extended xyz (MatterSim-compatible)
  macer util ft-convert -i ML_AB -o dataset.mattersim.xyz
"""
    )
    ft_convert_parser.add_argument("-i", "--input", required=True, help="Input ML_AB file path.")
    ft_convert_parser.add_argument("-o", "--output", default=None, help="Output extended XYZ path (default: <input>.mattersim.xyz).")
    ft_convert_parser.add_argument("--stress-unit", default="eV/A^3", help="Stress unit conversion (default: eV/A^3).")

    # --- 4. Train-MACE (MACE) ---
    tm_epilog = """
Examples:
  # 1. Train a small MACE model on a dataset
  macer util tm -d train.xyz --model-size small --epochs 100

  # 2. Train with explicit validation set and custom batch size
  macer util tm -t train.xyz -v valid.xyz --batch-size 32

  # 3. Restart training from latest checkpoint
  macer util tm -d train.xyz --restart --epochs 200

  # 4. Train a larger model with custom loss weights
  macer util tm -d train.xyz --model-size medium --energy-weight 10.0 --forces-weight 100.0

  # 5. Train on GPU with specific learning rate
  macer util tm -d train.xyz --device cuda --lr 0.005
"""
    tm_parser = subparsers.add_parser(
        "train-mace",
        aliases=["tm"],
        description=MACER_LOGO + f"\nmacer util tm (v{__version__}): Train lightweight MACE models (e.g. OMAT-Small) from scratch or checkpoints.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=tm_epilog
    )
    tm_groups = _add_common_train_args(
        tm_parser, 
        default_lr=0.01, 
        lr_help="Learning rate (default: 0.01).",
        default_weights=(1.0, 100.0, 10.0)
    )
    mace_group = tm_parser.add_argument_group("MACE Training Specifics")
    mace_group.add_argument("--model-size", type=str, default="medium", choices=["small", "medium", "large"], help="Model size.")
    mace_group.add_argument("--keep-checkpoints", action="store_true", help="Keep all intermediate checkpoints.")
    mace_group.add_argument("--restart", action="store_true", help="Restart training from the latest checkpoint.")

    # --- 5. Active Learning Category ---
    active_parser = subparsers.add_parser(
        "active", 
        description=MACER_LOGO + "\nmacer util active: Active learning utilities.",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    active_subparsers = active_parser.add_subparsers(dest="action", help="Active learning actions")

    # active query
    query_parser = active_subparsers.add_parser("query", help="Query uncertain structures from trajectory")
    query_parser.add_argument("--traj", required=True, help="Path to MD trajectory (md.traj, XDATCAR, etc.)")
    query_parser.add_argument("--models", required=True, nargs='+', help="List of ensemble model paths (.pth)")
    query_parser.add_argument("--top-k", type=int, default=10, help="Number of structures to select (default: 10)")
    query_parser.add_argument("--threshold", type=float, default=0.05, help="Minimum force uncertainty (eV/A) to consider (default: 0.05)")
    query_parser.add_argument("--output-dir", default="active_selection", help="Output directory for selected POSCARs")
    query_parser.add_argument("--device", default="cpu", help="Device to use (cpu/cuda)")
    query_parser.add_argument("--interval", type=int, default=1, help="Trajectory sampling interval")

    # ==========================================
    # Group 3: Data & Evaluation
    # ==========================================

    # --- 6. Dataset Category ---
    dataset_epilog = """
Examples:
  # 1. Merge multiple vasprun.xml files into a single dataset.xyz
  macer util dataset build -i vasprun-*.xml -o merged.xyz

  # 2. Split dataset into Train/Valid/Test (80/10/10)
  macer util dataset split -i dataset.xyz --ratio 0.8 0.1 0.1

  # 3. Split with custom output filenames
  macer util dataset split -i data.xyz --train-out tr.xyz --valid-out val.xyz --test-out te.xyz

  # 4. Convert ML_AB file to extended XYZ
  macer util dataset build -i ML_AB -o dataset.xyz

  # 5. Build dataset with custom stress unit conversion
  macer util dataset build -i vasprun.xml --stress-unit "kbar"
"""
    dataset_parser = subparsers.add_parser(
        "dataset", 
        aliases=["ds"],
        description=MACER_LOGO + f"\nmacer util ds (v{__version__}): Dataset management utilities.\nSupports merging VASP outputs and splitting into Train/Valid/Test sets.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=dataset_epilog
    )
    dataset_subparsers = dataset_parser.add_subparsers(dest="action", help="Dataset actions")

    # dataset build
    build_parser = dataset_subparsers.add_parser(
        "build", 
        help="Convert/Merge VASP outputs to dataset.xyz",
        description=MACER_LOGO + "\nConvert and Merge VASP outputs (ML_AB, xml, h5) to a single Extended XYZ file.\nBroken or truncated files will be automatically skipped.",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    build_parser.add_argument("-i", "--input", nargs="*", dest="inputs", help="Input files or patterns (e.g., vasprun-*.xml). Supports globs.")
    build_parser.add_argument("-o", "--output", default="dataset.xyz", help="Output XYZ filename (default: dataset.xyz).")
    build_parser.add_argument("--stress-unit", default="eV/A^3", help="Stress unit conversion.")

    # dataset split
    split_parser = dataset_subparsers.add_parser(
        "split", 
        help="Split XYZ dataset into train/valid/test",
        description=MACER_LOGO + "\nRandomly shuffle and split an Extended XYZ dataset into training, validation, and test sets.",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    split_parser.add_argument("-i", "--input", help="Input XYZ file (default: dataset.xyz).")
    split_parser.add_argument("--ratio", type=float, nargs=3, default=[0.8, 0.1, 0.1], help="Train/Valid/Test ratios (default: 0.8 0.1 0.1).")
    split_parser.add_argument("--seed", type=int, default=42, help="Random seed for shuffling (default: 42).")
    split_parser.add_argument("--train-out", default="train.xyz", help="Output filename for training set (default: train.xyz).")
    split_parser.add_argument("--valid-out", default="valid.xyz", help="Output filename for validation set (default: valid.xyz).")
    split_parser.add_argument("--test-out", default="test.xyz", help="Output filename for test set (default: test.xyz).")

    # --- 7. Evaluate (General Accuracy Assessment) ---
    eval_epilog = """
Examples:
  # 1. Evaluate default model on a test set
  macer util evaluate -d test.xyz --ff mattersim

  # 2. Evaluate a specific MACE model
  macer util evaluate -d data.xyz --ff mace --model mace.model

  # 3. Evaluate using CUDA device
  macer util evaluate -d test.xyz --ff sevennet --device cuda

  # 4. Evaluate with float64 precision
  macer util evaluate -d data.xyz --ff chgnet --dtype float64

  # 5. Evaluate custom fine-tuned model
  macer util evaluate -d valid.xyz --ff mattersim --model finetuned_mattersim.pth

  # 6. Evaluate directly from VASP ML_AB (auto-convert to extended xyz)
  macer util evaluate -d ML_AB --ff mattersim
"""
    eval_parser = subparsers.add_parser(
        "evaluate",
        aliases=["eval"],
        description=MACER_LOGO + f"\nmacer util evaluate (v{__version__}): General accuracy assessment for MLFF models.\nCalculates MAE, Pearson Correlation (r²) and generates parity plots.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=eval_epilog
    )
    eval_parser.add_argument("--data", "-d", help="Path to input data for evaluation (XYZ or VASP ML_AB).")
    eval_parser.add_argument("--ff", default=DEFAULT_FF, help=f"Force field to evaluate (default: {DEFAULT_FF}).")
    eval_parser.add_argument("--model", help="Path to model file (default: resolved via macer settings).")
    eval_parser.add_argument("--device", default=DEFAULT_DEVICE, choices=["cpu", "cuda", "mps"], help=f"Device (default: {DEFAULT_DEVICE}).")
    eval_parser.add_argument("--dtype", choices=["float32", "float64"], help="Dtype for evaluation.")

    # --- 8. Struct Category ---
    struct_parser = subparsers.add_parser(
        "struct", 
        description=MACER_LOGO + "\nmacer util struct: Atomic structure file utilities.",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    struct_subparsers = struct_parser.add_subparsers(dest="action", help="Structure actions")

    # struct vasp4to5
    v4to5_parser = struct_subparsers.add_parser("vasp4to5", help="Convert VASP4 POSCAR to VASP5 (add symbols)")
    v4to5_parser.add_argument("-i", "--input", required=True, help="Input VASP4 POSCAR file")
    v4to5_parser.add_argument("-s", "--symbols", help="Element symbols (e.g. \"Li S\"). If omitted, guesses from 1st line.")
    v4to5_parser.add_argument("-o", "--output", help="Output VASP5 POSCAR file (optional)")

    # struct bz-3d
    bz_parser = struct_subparsers.add_parser("bz-3d", help="Plot 1BZ wireframe + special q-points (HTML)")
    bz_parser.add_argument("-p", "--poscar", required=False, help="Input POSCAR or CIF file")
    bz_parser.add_argument("-c", "--cif", required=False, help="Input CIF file")
    bz_parser.add_argument("-f", "--formula", type=str, help="Formula to retrieve from Materials Project (e.g., MgAl2O4)")
    bz_parser.add_argument("-m", "--mpid", type=str, help="Materials Project ID (e.g., mp-3536)")
    bz_parser.add_argument("-o", "--output", default="BZ.html", help="Output HTML filename (default: BZ.html)")
    bz_parser.add_argument("--relative-coordinate", action="store_true", help="Plot in relative coordinates")
    bz_parser.add_argument("--no-special-points", action="store_true", help="Disable special q-point labels")

    # Top-level aliases for struct actions
    v4to5_alias = subparsers.add_parser(
        "vasp4to5",
        description=MACER_LOGO + "\nmacer util vasp4to5: Convert VASP4 POSCAR to VASP5 (add symbols).",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    v4to5_alias.add_argument("-i", "--input", required=True, help="Input VASP4 POSCAR file")
    v4to5_alias.add_argument("-s", "--symbols", help="Element symbols (e.g. \"Li S\"). If omitted, guesses from 1st line.")
    v4to5_alias.add_argument("-o", "--output", help="Output VASP5 POSCAR file (optional)")

    bz_alias = subparsers.add_parser(
        "bz-3d",
        description=MACER_LOGO + "\nmacer util bz-3d: Plot 1BZ wireframe + special q-points (HTML).",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    bz_alias.add_argument("-p", "--poscar", required=False, help="Input POSCAR or CIF file")
    bz_alias.add_argument("-c", "--cif", required=False, help="Input CIF file")
    bz_alias.add_argument("-f", "--formula", type=str, help="Formula to retrieve from Materials Project (e.g., MgAl2O4)")
    bz_alias.add_argument("-m", "--mpid", type=str, help="Materials Project ID (e.g., mp-3536)")
    bz_alias.add_argument("-o", "--output", default="BZ.html", help="Output HTML filename (default: BZ.html)")
    bz_alias.add_argument("--relative-coordinate", action="store_true", help="Plot in relative coordinates")
    bz_alias.add_argument("--no-special-points", action="store_true", help="Disable special q-point labels")

    # ==========================================
    # Group 4: Physics & Analysis
    # ==========================================

    # --- 9. MD Category ---
    md_parser = subparsers.add_parser(
        "md", 
        description=MACER_LOGO + "\nmacer util md: Molecular Dynamics post-processing and analysis.",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    md_subparsers = md_parser.add_subparsers(dest="action", help="MD actions")

    # md traj2xdatcar
    t2x_parser = md_subparsers.add_parser("traj2xdatcar", help="Convert md.traj to VASP XDATCAR")
    t2x_parser.add_argument("-i", "--input", required=True, help="Input .traj file")
    t2x_parser.add_argument("-o", "--output", default="XDATCAR", help="Output XDATCAR file (default: XDATCAR)")
    t2x_parser.add_argument("--interval", type=int, default=1, help="Sampling interval used during MD (e.g. 100).")

    # md summary
    summary_parser = md_subparsers.add_parser("summary", help="Print statistical summary of md.csv")
    summary_parser.add_argument("-i", "--input", default="md.csv", help="Input md.csv file (default: md.csv)")

    # md conductivity
    cond_parser = md_subparsers.add_parser("conductivity", help="Calculate ionic conductivity")
    cond_parser.add_argument("-i", "--input", required=True, help="Input trajectory (md.traj or XDATCAR)")
    cond_parser.add_argument("-t", "--temp", type=float, required=True, help="Temperature (K)")
    cond_parser.add_argument("--dt", type=float, default=2.0, help="Timestep (fs)")
    cond_parser.add_argument("--interval", type=int, default=1, help="Sampling interval used during MD (e.g. 50).")
    cond_parser.add_argument("--charges", help="Oxidation states (e.g. \"Li:1,S:-2\")")

    # md cell (new)
    cell_parser = md_subparsers.add_parser("cell", help="Analyze cell evolution (a, b, c, Vol)")
    cell_parser.add_argument("-i", "--input", required=True, help="Input trajectory (md.traj or XDATCAR)")
    cell_parser.add_argument("--dt", type=float, default=2.0, help="Timestep (fs)")
    cell_parser.add_argument("--skip", type=float, default=0.0, help="Initial time to skip for averaging (ps)")
    cell_parser.add_argument("--interval", type=int, default=1, help="Sampling interval used during MD (default: 1)")
    cell_parser.add_argument("-o", "--output", default="cell_evolution", help="Output prefix")
    cell_parser.add_argument("--poscar", default="POSCAR-cell-averaged", help="Output filename for averaged structure")

    # md plot
    pmd_parser = md_subparsers.add_parser("plot", help="Plot MD trajectory data (T, E, P)")
    pmd_parser.add_argument("-i", "--input", default="md.csv", help="Input md.csv")
    pmd_parser.add_argument("-o", "--output", default="md_plot", help="Output PDF prefix")

    # md rdf
    rdf_parser = md_subparsers.add_parser("rdf", help="Plot Radial Distribution Function (RDF)")
    rdf_parser.add_argument("-i", "--input", required=True, help="Input trajectory (md.traj or XDATCAR)")
    rdf_parser.add_argument("-o", "--output", default="rdf_plot", help="Output PDF prefix")
    rdf_parser.add_argument("--rmax", type=float, default=10.0, help="Maximum radius (Å)")
    rdf_parser.add_argument("--bins", type=int, default=200, help="Number of bins")

    # --- 10. Phonopy Category ---
    phonopy_parser = subparsers.add_parser(
        "phonopy", 
        description=MACER_LOGO + "\nmacer util phonopy: Phonon analysis and visualization.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        help=argparse.SUPPRESS
    )
    phonopy_subparsers = phonopy_parser.add_subparsers(dest="action", help="Phonopy actions")

    # phonon band
    pband_parser = phonopy_subparsers.add_parser("band", help="Plot phonon dispersion from .dat")
    pband_parser.add_argument("-i", "--input", default="band.dat", help="Input .dat file (default: band.dat)")
    pband_parser.add_argument("-o", "--output", default="phonon_band.pdf", help="Output PDF file (default: phonon_band.pdf)")
    pband_parser.add_argument("-y", "--yaml", help="Optional .yaml file for labels (e.g. band.yaml)")
    pband_parser.add_argument("--fmin", type=float, help="Min frequency (THz)")
    pband_parser.add_argument("--fmax", type=float, help="Max frequency (THz)")
    pband_parser.add_argument("--labels", help="High-symmetry labels (e.g. \"G M K G\")")

    # phonon gruneisen-band
    pgru_band_parser = phonopy_subparsers.add_parser("gruneisen-band", help="Plot Grüneisen parameters along band path (2D)")
    pgru_band_parser.add_argument("-i", "--input", default="gruneisen_band.dat", 
                                  help="Input .dat file containing Grüneisen parameters along a band path (default: gruneisen_band.dat)")
    pgru_band_parser.add_argument("-o", "--output", default="gruneisen", 
                                  help="Output filename prefix for PDF and data files (default: gruneisen)")
    pgru_band_parser.add_argument("-y", "--yaml", 
                                  help="Optional Phonopy band.yaml format file to extract high-symmetry labels")
    pgru_band_parser.add_argument("--fmin", type=float, help="Min frequency (THz)")
    pgru_band_parser.add_argument("--fmax", type=float, help="Max frequency (THz)")
    pgru_band_parser.add_argument("--gmin", type=float, help="Min Grüneisen parameter")
    pgru_band_parser.add_argument("--gmax", type=float, help="Max Grüneisen parameter")
    pgru_band_parser.add_argument("--filter", type=float, default=3.0, help="Outlier filter factor (default: 3.0)")
    pgru_band_parser.add_argument("--labels", help="High-symmetry labels (e.g. \"G M K G\")")

    # phonon gruneisen-3d
    pgru_3d_parser = phonopy_subparsers.add_parser("gruneisen-3d", help="Plot Grüneisen parameters in 3D BZ")
    pgru_3d_parser.add_argument("-i", "--input", default="gruneisen_full.dat",
                                help="Input .dat file containing Grüneisen parameters (default: gruneisen_full.dat)")
    pgru_3d_parser.add_argument("-p", "--poscar", default="POSCAR",
                                help="POSCAR file for reciprocal lattice (default: POSCAR)")
    pgru_3d_parser.add_argument("-f", "--formula", type=str, help="Formula to retrieve from Materials Project (e.g., MgAl2O4)")
    pgru_3d_parser.add_argument("-m", "--mpid", type=str, help="Materials Project ID (e.g., mp-3536)")
    pgru_3d_parser.add_argument("-o", "--output", default="average_gruneisen_BZ",
                                help="Output filename prefix for HTML/PDF/CSV (default: average_gruneisen_BZ)")
    pgru_3d_parser.add_argument("--gmin", type=float, default=-5.0, help="Min Grüneisen for colorscale")
    pgru_3d_parser.add_argument("--gmax", type=float, default=5.0, help="Max Grüneisen for colorscale")
    pgru_3d_parser.add_argument("--cutoff-freq", type=float, default=0.0, help="Cutoff frequency (THz)")
    pgru_3d_parser.add_argument("--original-position", action="store_true", help="Do not fix q-positions")
    pgru_3d_parser.add_argument("--fill-BZ", action="store_true", help="Fill in the first Brillouin zone")
    pgru_3d_parser.add_argument("--relative-coordinate", action="store_true", help="Plot in relative coordinate")
    pgru_3d_parser.add_argument("--expand-symmetry", dest="expand_symmetry", action="store_true",
                                help="Expand IBZ q-points to full BZ using symmetry operations")
    pgru_3d_parser.add_argument("--no-expand-symmetry", dest="expand_symmetry", action="store_false",
                                help="Do not expand q-points (use input q-points as-is)")
    pgru_3d_parser.set_defaults(expand_symmetry=True)
    pgru_3d_parser.add_argument("--full-bz", action="store_true",
                                help="Show full 1st BZ q-points (default shows only b3>=0)")
    pgru_3d_parser.add_argument("--half-axis", choices=["auto", "b1", "b2", "b3", "full"], default="auto",
                                help="Axis for half-BZ folding (auto chooses axis with most special points; full disables folding)")
    pgru_3d_parser.add_argument("--hull-filter", action="store_true",
                                help="Filter q-points inside the convex hull of special q-points")
    pgru_3d_parser.add_argument("--hull-tolerance", type=float, default=0.05,
                                help="Convex hull tolerance (default: 0.05)")
    pgru_3d_parser.add_argument("--no-special-points", action="store_true",
                                help="Disable special q-point labels (seekpath)")
    pgru_3d_parser.add_argument("--no-time-reversal", action="store_true",
                                help="Disable time-reversal expansion (-q)")
    pgru_3d_parser.add_argument("--symprec", type=float, default=1e-2,
                                help="symprec for spglib symmetry expansion (default: 1e-2)")

    # Top-level aliases for phonopy visualization tools
    band_alias = subparsers.add_parser(
        "band",
        help="(Alias) Phonopy: plot phonon dispersion from .dat"
    )
    band_alias.add_argument("-i", "--input", default="band.dat", help="Input .dat file (default: band.dat)")
    band_alias.add_argument("-o", "--output", default="phonon_band.pdf", help="Output PDF file (default: phonon_band.pdf)")
    band_alias.add_argument("-y", "--yaml", help="Optional .yaml file for labels (e.g. band.yaml)")
    band_alias.add_argument("--fmin", type=float, help="Min frequency (THz)")
    band_alias.add_argument("--fmax", type=float, help="Max frequency (THz)")
    band_alias.add_argument("--labels", help="High-symmetry labels (e.g. \"G M K G\")")

    gruneisen_band_alias = subparsers.add_parser(
        "gruneisen-band",
        help="(Alias) Phonopy: plot Grüneisen parameters along band path (2D)"
    )
    gruneisen_band_alias.add_argument("-i", "--input", default="gruneisen_band.dat",
                                      help="Input .dat file containing Grüneisen parameters along a band path (default: gruneisen_band.dat)")
    gruneisen_band_alias.add_argument("-o", "--output", default="gruneisen",
                                      help="Output filename prefix for PDF and data files (default: gruneisen)")
    gruneisen_band_alias.add_argument("-y", "--yaml",
                                      help="Optional Phonopy band.yaml format file to extract high-symmetry labels")
    gruneisen_band_alias.add_argument("--fmin", type=float, help="Min frequency (THz)")
    gruneisen_band_alias.add_argument("--fmax", type=float, help="Max frequency (THz)")
    gruneisen_band_alias.add_argument("--gmin", type=float, help="Min Grüneisen parameter")
    gruneisen_band_alias.add_argument("--gmax", type=float, help="Max Grüneisen parameter")
    gruneisen_band_alias.add_argument("--filter", type=float, default=3.0, help="Outlier filter factor (default: 3.0)")
    gruneisen_band_alias.add_argument("--labels", help="High-symmetry labels (e.g. \"G M K G\")")

    gruneisen_3d_alias = subparsers.add_parser(
        "gruneisen-3d",
        help="(Alias) Phonopy: plot Grüneisen parameters in 3D BZ"
    )
    gruneisen_3d_alias.add_argument("-i", "--input", default="gruneisen_full.dat",
                                    help="Input .dat file containing Grüneisen parameters (default: gruneisen_full.dat)")
    gruneisen_3d_alias.add_argument("-p", "--poscar", default="POSCAR",
                                    help="POSCAR file for reciprocal lattice (default: POSCAR)")
    gruneisen_3d_alias.add_argument("-f", "--formula", type=str, help="Formula to retrieve from Materials Project (e.g., MgAl2O4)")
    gruneisen_3d_alias.add_argument("-m", "--mpid", type=str, help="Materials Project ID (e.g., mp-3536)")
    gruneisen_3d_alias.add_argument("-o", "--output", default="average_gruneisen_BZ",
                                    help="Output filename prefix for HTML/PDF/CSV (default: average_gruneisen_BZ)")
    gruneisen_3d_alias.add_argument("--gmin", type=float, default=-5.0, help="Min Grüneisen for colorscale")
    gruneisen_3d_alias.add_argument("--gmax", type=float, default=5.0, help="Max Grüneisen for colorscale")
    gruneisen_3d_alias.add_argument("--cutoff-freq", type=float, default=0.0, help="Cutoff frequency (THz)")
    gruneisen_3d_alias.add_argument("--original-position", action="store_true", help="Do not fix q-positions")
    gruneisen_3d_alias.add_argument("--fill-BZ", action="store_true", help="Fill in the first Brillouin zone")
    gruneisen_3d_alias.add_argument("--relative-coordinate", action="store_true", help="Plot in relative coordinate")
    gruneisen_3d_alias.add_argument("--expand-symmetry", dest="expand_symmetry", action="store_true",
                                    help="Expand IBZ q-points to full BZ using symmetry operations")
    gruneisen_3d_alias.add_argument("--no-expand-symmetry", dest="expand_symmetry", action="store_false",
                                    help="Do not expand q-points (use input q-points as-is)")
    gruneisen_3d_alias.set_defaults(expand_symmetry=True)
    gruneisen_3d_alias.add_argument("--full-bz", action="store_true",
                                    help="Show full 1st BZ q-points (default shows only b3>=0)")
    gruneisen_3d_alias.add_argument("--half-axis", choices=["auto", "b1", "b2", "b3", "full"], default="auto",
                                    help="Axis for half-BZ folding (auto chooses axis with most special points; full disables folding)")
    gruneisen_3d_alias.add_argument("--hull-filter", action="store_true",
                                    help="Filter q-points inside the convex hull of special q-points")
    gruneisen_3d_alias.add_argument("--hull-tolerance", type=float, default=0.05,
                                    help="Convex hull tolerance (default: 0.05)")
    gruneisen_3d_alias.add_argument("--no-special-points", action="store_true",
                                    help="Disable special q-point labels (seekpath)")
    gruneisen_3d_alias.add_argument("--no-time-reversal", action="store_true",
                                    help="Disable time-reversal expansion (-q)")
    gruneisen_3d_alias.add_argument("--symprec", type=float, default=1e-2,
                                    help="symprec for spglib symmetry expansion (default: 1e-2)")

    # Top-level aliases for phonopy subcommands
    phonon_band_alias = subparsers.add_parser(
        "phonon-band",
        aliases=["pb"],
        help="(Alias) Phonopy: full phonon dispersion workflow.",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    phonon_band_alias.add_argument("-p", "--poscar", dest="input_files", required=False, nargs='+', default=None,
                                   help="One or more input cell files in VASP POSCAR format (or CIF).")
    phonon_band_alias.add_argument("-c", "--cif", dest="cif_files", required=False, nargs='+', default=None,
                                   help="One or more input cell files in CIF format.")
    phonon_band_alias.add_argument("-f", "--formula", type=str, help="Formula to retrieve from Materials Project (e.g., MgAl2O4)")
    phonon_band_alias.add_argument("-m", "--mpid", type=str, help="Materials Project ID (e.g., mp-3536)")
    phonon_band_alias.add_argument("--output-dir", help="Directory to save output files.")
    phonon_band_alias.add_argument("--ff", type=str, default=DEFAULT_FF, choices=ALL_SUPPORTED_FFS, help=f"Force field to use. (default: {DEFAULT_FF})")
    phonon_band_alias.add_argument('--model', type=str, default=None, help='Path to the force field model file for macer.')
    phonon_band_alias.add_argument('--device', type=str, default=DEFAULT_DEVICE, choices=['cpu', 'mps', 'cuda'], help='Device for macer computation.')
    phonon_band_alias.add_argument("--modal", type=str, default=None, help="Modal for SevenNet model.")
    phonon_band_alias.add_argument("--use-relax-unit", action="store_true",
                                   help="Use iterative relaxation/symmetrization (macer phonopy sr) for the initial structure preparation.")
    phonon_band_alias.add_argument("--initial-fmax", type=float, default=0.005, help="Force convergence threshold for initial relaxation (eV/Å).")
    phonon_band_alias.add_argument("--initial-symprec", type=float, default=1e-5, help="Symmetry tolerance for FixSymmetry during initial relaxation (default: 1e-5 Å).")
    phonon_band_alias.add_argument("--initial-isif", type=int, default=3, help="VASP ISIF mode for initial relaxation (default: 3)")
    phonon_band_alias.add_argument('--tolerance-sr', type=float, default=0.01, help='Symmetry tolerance (Å) for macer phonopy sr. Default: 0.01.')
    phonon_band_alias.add_argument("--optimizer", type=str, default="FIRE", help="Optimizer to use for relaxation (e.g., FIRE, BFGS, LBFGS).")
    phonon_band_alias.add_argument("--dim", type=int, nargs='+', default=None, help='Set supercell dimension. Accepts 3 integers or 9 for a full matrix.')
    phonon_band_alias.add_argument("-l", "--length", type=float, default=20.0, help="Minimum length of supercell lattice vectors in Å (default: 20.0).")
    phonon_band_alias.add_argument('--fix-axis', type=lambda s: [axis.strip() for axis in s.split(',')], help='Fix specified axes.')
    phonon_band_alias.add_argument("--amplitude", type=float, default=0.01, help="Displacement amplitude in Å (default: 0.01).")
    phonon_band_alias.add_argument('--pm', dest='is_plusminus', action="store_true", help='Generate plus and minus displacements for each direction (always enabled).')
    phonon_band_alias.add_argument('--nodiag', dest='is_diagonal', action="store_false", help='Do not generate diagonal displacements.')
    phonon_band_alias.add_argument("--mass", nargs='+', help="Specify atomic masses. Format: Symbol Mass Symbol Mass ...")
    phonon_band_alias.add_argument("--atom-names", default=None, help='Override ATOM_NAME, e.g. "K Zr P O".')
    phonon_band_alias.add_argument("--rename", default=None, help='Rename mapping, e.g. "Na=K,Zr=Zr".')
    phonon_band_alias.add_argument('--tolerance-phonopy', type=float, default=5e-3, help='Symmetry tolerance for phonopy. Default: 5e-3.')
    phonon_band_alias.add_argument("--symprec", type=float, default=1e-5, help="Symmetry tolerance passed to SeeK-path (default: 1e-5).")
    phonon_band_alias.add_argument("--gamma", default="GM", help="Gamma label for BAND_LABELS (e.g., GM or Γ).")
    phonon_band_alias.add_argument("--yaml", default="phonopy_disp.yaml", type=Path, help="Path to phonopy_disp.yaml to read DIM from (for band.conf).")
    phonon_band_alias.add_argument("--out", default="band.conf", type=Path, help="Output band.conf file name.")
    phonon_band_alias.add_argument("--band-path", type=str, default=None, help='Manual band path.')
    phonon_band_alias.add_argument("--labels", type=str, default=None, help='Labels for band path (e.g., "Gamma M").')
    phonon_band_alias.add_argument("--no-defaults", action="store_true", help="Do not include default FORCE_SETS, FC_SYMMETRY, EIGENVECTORS lines.")
    phonon_band_alias.add_argument('--plot-gruneisen', '-pg', dest='plot_gruneisen', action="store_true", help='Plot Gruneisen parameter on phonon dispersion.')
    phonon_band_alias.add_argument('--strain', type=float, default=None, help='Strain for Gruneisen parameter.')
    phonon_band_alias.add_argument('--gmin', type=float, default=None, help='Minimum Gruneisen parameter for color scale.')
    phonon_band_alias.add_argument('--gmax', type=float, default=None, help='Maximum Gruneisen parameter for color scale.')
    phonon_band_alias.add_argument('--filter-outliers', type=float, nargs='?', const=3.0, default=None, help='Filter outlier Grüneisen values.')
    phonon_band_alias.add_argument('--target-energy', type=float, default=10.0, help='Target energy in meV for bulk modulus-based strain estimation.')
    phonon_band_alias.add_argument("--dos", dest="dos", action="store_true", help="Calculate and plot phonon Density of States (DOS).")
    phonon_band_alias.add_argument("--mesh", type=int, nargs=3, default=[11, 11, 11], help="Q-point mesh for DOS and Grüneisen calculation (default: 11 11 11).")
    phonon_band_alias.add_argument("--irreps", "--irreducible-representation", dest="irreps", action="store_true", help="Calculate irreducible representations.")
    phonon_band_alias.add_argument("--qpoint", nargs=3, type=float, default=[0.0, 0.0, 0.0], help="Q-point for irreducible representations calculation.")
    phonon_band_alias.add_argument("--tolerance-irreps", type=float, default=1e-5, help="Degeneracy tolerance for irreducible representations.")
    phonon_band_alias.add_argument("--write-arrow", "-wa", dest="write_arrow", action="store_true", help="Write VESTA files for phonon mode visualization.")
    phonon_band_alias.add_argument("--arrow-length", type=float, default=1.8, help="Length of the longest arrow (Å).")
    phonon_band_alias.add_argument("--arrow-min-cutoff", type=float, default=0.3, help="Minimum arrow length to draw (Å).")
    phonon_band_alias.add_argument("--arrow-qpoint-gamma", action="store_true", help="Write arrows only for the Gamma point.")
    phonon_band_alias.add_argument("--arrow-qpoint", nargs=3, type=float, default=None, help="Write arrows for a specific q-point vector (3 floats).")

    qha_alias = subparsers.add_parser(
        "run-qha",
        aliases=["qha"],
        help="(Alias) Phonopy: Quasi-Harmonic Approximation workflow.",
    )
    qha_alias.set_defaults(_util_forward_phonopy="qha")

    sscha_alias = subparsers.add_parser(
        "run-sscha",
        aliases=["sscha", "qscaild", "run-qscaild"],
        help="(Alias) Phonopy: qSCAILD workflow (SSCHA alias).",
    )
    sscha_alias.set_defaults(_util_forward_phonopy="qscaild")

    tc_alias = subparsers.add_parser(
        "thermal-conductivity",
        aliases=["tc"],
        help="(Alias) Phonopy: thermal conductivity workflow.",
    )
    tc_alias.set_defaults(_util_forward_phonopy="tc")

    ft_alias = subparsers.add_parser(
        "finite-temperature",
        aliases=["phonon-ft", "pft"],
        help="(Alias) Phonopy: finite-temperature renormalization workflow.",
    )
    ft_alias.set_defaults(_util_forward_phonopy="ft")

    sr_alias = subparsers.add_parser(
        "symmetry-refine",
        aliases=["sr"],
        help="(Alias) Phonopy: symmetry refinement workflow.",
    )
    sr_alias.set_defaults(_util_forward_phonopy="sr")

    # --- 11. DynaPhoPy Category ---
    dp_parser = subparsers.add_parser(
        "dynaphopy",
        add_help=False
    )

    return {
        "gm": gm_parser,
        "vasp4to5": v4to5_alias,
        "bz-3d": bz_alias,
        "model": model_parser,
        "pydefect": pydefect_parser,
        "find-interstitial": fi_alias,
        "fi": fi_alias,
        "pydefect_supercell": sc_parser,
        "pydefect_cpd": cpd_parser,
        "ft": ft_parser,
        "ft-convert": ft_convert_parser,
        "tm": tm_parser,
        "active": active_parser,
        "dataset": dataset_parser,
        "dataset_build": build_parser,
        "dataset_split": split_parser,
        "eval": eval_parser,
        "struct": struct_parser,
        "md": md_parser,
        "phonopy": phonopy_parser,
        "band": band_alias,
        "gruneisen-band": gruneisen_band_alias,
        "gruneisen-3d": gruneisen_3d_alias,
        "phonon-band": phonon_band_alias,
        "run-qha": qha_alias,
        "run-sscha": sscha_alias,
        "thermal-conductivity": tc_alias,
        "finite-temperature": ft_alias,
        "symmetry-refine": sr_alias,
        "gruneisen-band": pgru_band_parser,
        "gruneisen-3d": pgru_3d_parser,
        "dynaphopy": dp_parser
    }

def execute_util(args, unknown_args=None):
    """Execution logic for util commands."""
    dummy_parser = argparse.ArgumentParser(add_help=False)
    subparsers = dummy_parser.add_subparsers(dest="category")
    p_map = add_util_parsers(subparsers)
    unknown_args = unknown_args or []

    def _looks_like_ml_ab(path):
        filename = os.path.basename(path)
        is_xyz_ext = filename.lower().endswith(".xyz") or filename.lower().endswith(".extxyz")
        if ("ML_AB" in filename or "ML_ABN" in filename) and not is_xyz_ext:
            return True
        if not is_xyz_ext:
            try:
                with open(path, "r") as f:
                    header = f.read(100)
                if "1.0 Version" in header:
                    return True
            except Exception:
                return False
        return False

    def _forward_phonopy(cmd, extra_args):
        from macer.cli.phonopy_main import add_phonopy_parsers
        from macer import __version__
        parser = argparse.ArgumentParser(
            description=MACER_LOGO + f"\nmacer_phonopy (v{__version__}): Machine-learning accelerated Atomic Computational Environment for automated Research workflows",
            formatter_class=argparse.RawDescriptionHelpFormatter
        )
        sub = parser.add_subparsers(dest="command", help="Available commands")
        p_map_ph = add_phonopy_parsers(sub)
        ph_args = parser.parse_args([cmd] + extra_args)
        if not ph_args.command:
            parser.print_help()
            return
        # Canonical mapping for checks
        check_cmd = ph_args.command
        if check_cmd == "pb": check_cmd = "pb"
        elif check_cmd == "qha": check_cmd = "qha"
        elif check_cmd == "sscha": check_cmd = "sscha"
        elif check_cmd == "qscaild": check_cmd = "sscha"
        elif check_cmd == "run-sscha": check_cmd = "sscha"
        elif check_cmd == "tc": check_cmd = "tc"
        elif check_cmd == "ft": check_cmd = "ft"
        elif check_cmd == "sr": check_cmd = "sr"

        if check_cmd in p_map_ph:
            has_input = False
            if hasattr(ph_args, "input_files") and ph_args.input_files: has_input = True
            if hasattr(ph_args, "cif_files") and ph_args.cif_files: has_input = True
            if hasattr(ph_args, "poscar") and ph_args.poscar: has_input = True
            if hasattr(ph_args, "cif") and ph_args.cif: has_input = True
            if hasattr(ph_args, "formula") and ph_args.formula: has_input = True
            if hasattr(ph_args, "mpid") and ph_args.mpid: has_input = True
            if not has_input:
                p_map_ph[check_cmd].print_help()
                return
            if check_cmd == "sscha" and not getattr(ph_args, "temperature", None):
                p_map_ph[check_cmd].print_help()
                return
            if check_cmd == "ft" and not getattr(ph_args, "temp", None):
                p_map_ph[check_cmd].print_help()
                return
        if hasattr(ph_args, "func"):
            if ph_args.command == "qha" and hasattr(ph_args, "num_volumes") and ph_args.num_volumes < 4:
                parser.error("For the 'qha' command, number of volume points (--num-volumes) must be at least 4.")
            ph_args.func(ph_args)
        else:
            parser.print_help()

    if not args.category:
        print(MACER_LOGO)
        print(f"  macer util (v{__version__})")
        print(f"  Utility tools for post-processing and analysis.")
        print("-" * 50 + "\n")
        return

    # Top-level aliases for struct actions
    if args.category in ["bz-3d", "vasp4to5"]:
        args.action = args.category
        args.category = "struct"

    # --- Group 1: Model Management ---

    # Execute Get-Model
    if args.category in ["get-model", "gm"]:
        from macer.utils import model_manager
        from macer.defaults import AVAILABLE_MODELS
        
        if args.model:
            # Download mode
            if args.model == "all":
                # Use the new interactive batch downloader
                model_manager.download_all_models(ff_filter=args.ff, force=args.replace)
            else:
                # Specific model
                # Find which FF owns this model
                found_ff = args.ff
                if not found_ff:
                    for ff, models in AVAILABLE_MODELS.items():
                        if args.model in models:
                            found_ff = ff
                            break
                if not found_ff:
                    print(f"Error: Model '{args.model}' not recognized or --ff not specified.")
                    return
                model_manager.download_model(found_ff, args.model, force=args.replace)
        else:
            # List mode
            print_util_banner("gm")
            model_manager.list_models_status(ff_filter=args.ff)

    # Execute Model Utils
    elif args.category == "model":
        from macer.utils.model_tools import convert_model_precision, list_models, compile_mace_model
        if args.action == "fp32":
            convert_model_precision(args.input, args.output)
        elif args.action == "list":
            list_models()
        elif args.action == "compile":
            compile_mace_model(args.input, args.output, model_size=args.size)
        else:
            p_map["model"].print_help()

    # --- Group 1.5: Pydefect Utilities ---

    elif args.category in ["pydefect", "find-interstitial", "fi", "supercell", "sc", "cpd"]:
        action = getattr(args, "action", None)
        if action in ["find-interstitial", "fi"] or args.category in ["find-interstitial", "fi"]:
            if not getattr(args, "input", None):
                p_map["find-interstitial"].print_help()
                return
            from macer.pydefect.utils import find_interstitial_sites
            input_path = Path(args.input)
            if not input_path.exists():
                print(f"Error: {input_path} not found.")
                return
            find_interstitial_sites(input_path, output_dir=Path(args.output_dir), min_dist=args.min_dist, insert_species=args.species)
        elif action in ["supercell", "sc"] or args.category in ["supercell", "sc"]:
            from macer.pydefect.utils import make_manual_supercell_info
            if not getattr(args, "poscar", None) or not getattr(args, "dim", None):
                p_map["pydefect_supercell"].print_help()
                return
            input_path = Path(args.poscar)
            if not input_path.exists():
                print(f"Error: {input_path} not found.")
                return
            make_manual_supercell_info(
                input_path,
                dim=args.dim,
                output_dir=Path(args.output_dir),
                analyze_symmetry=getattr(args, "analyze_symmetry", True)
            )
        elif action == "cpd" or args.category == "cpd":
            from macer.pydefect.utils import plot_cpd_outputs
            from pydefect.chem_pot_diag.chem_pot_diag import RelativeEnergies, ChemPotDiagMaker
            from pymatgen.core import Composition
            if not args.target:
                p_map["pydefect_cpd"].print_help()
                return
            rel_path = Path(args.relative_energies).expanduser().resolve()
            out_dir = Path(args.output_dir).expanduser().resolve()
            out_dir.mkdir(parents=True, exist_ok=True)
            os.chdir(out_dir)

            if not rel_path.exists():
                print(f"Error: {rel_path} not found.")
                return
            rel_energies = RelativeEnergies.from_yaml(str(rel_path))

            if args.elements:
                elements = args.elements
            else:
                elements = [str(e) for e in Composition(args.target).elements]

            if args.plot_elements is not None:
                if len(args.plot_elements) != 2:
                    print("Error: --plot-elements must have exactly 2 elements.")
                    return

            # Validate elements
            for el in elements:
                if el not in rel_energies.all_element_set:
                    print(f"Warning: element {el} not found in relative energies element set.")

            if len(elements) == 1:
                print(f"Unary system detected ({elements[0]}). Skipping CPD plot.")
                return

            cpd_maker = ChemPotDiagMaker(rel_energies, elements=elements, target=args.target)
            cpd = cpd_maker.chem_pot_diag
            cpd.to_json_file()
            if args.target:
                cpd.to_target_vertices.to_yaml_file()
            output_prefix = "cpd-2d" if cpd.dim == 2 else "cpd-3d"
            plot_cpd_outputs(
                cpd,
                plot_elements=args.plot_elements,
                output_prefix=output_prefix,
                project_elements=None
            )

            if args.projection_elements and cpd.dim >= 3:
                e1, e2 = args.projection_elements
                plot_cpd_outputs(
                    cpd,
                    plot_elements=[e1, e2],
                    output_prefix=f"cpd-2d-projected-{e1}-{e2}",
                    make_pdf=False,
                    make_html=True,
                    project_elements=[e1, e2]
                )
                return

            if args.projection and cpd.dim >= 3:
                from itertools import combinations
                for e1, e2 in combinations(cpd.vertex_elements, 2):
                    plot_cpd_outputs(
                        cpd,
                        plot_elements=[e1, e2],
                        output_prefix=f"cpd-2d-projected-{e1}-{e2}",
                        make_pdf=False,
                        make_html=True,
                        project_elements=[e1, e2]
                    )
        else:
            p_map["pydefect"].print_help()

    # --- Group 2: Training & Fine-tuning ---

    # Execute Fine-tunning (MatterSim)
    elif args.category in ["fine-tunning", "ft"]:
        if not args.data and not getattr(args, "train_data", None):
            p_map["ft"].print_help()
            return
        print_util_banner("ft", ff="mattersim", model=args.model)
        from macer.utils.finetuning_mattersim import run_mattersim_finetuning
        run_mattersim_finetuning(args)

    # Execute Fine-tuning Data Convert
    elif args.category in ["ft-convert", "ftc"]:
        if not getattr(args, "input", None):
            p_map["ft-convert"].print_help()
            return
        from macer.training.data_prep import convert_to_mattersim_format
        from macer.utils.dataset_tools import get_unique_filename
        output_path = args.output if args.output else f"{args.input}.mattersim.xyz"
        output_path = get_unique_filename(output_path)
        print(f"Converting ML_AB to extended xyz: {args.input} -> {output_path}")
        convert_to_mattersim_format(args.input, output_path, stress_unit=args.stress_unit)
        print(f"Saved: {output_path}")

    # Execute Train-MACE (MACE)
    elif args.category in ["train-mace", "tm"]:
        if not args.data and not getattr(args, "train_data", None):
            p_map["tm"].print_help()
            return
        print_util_banner("tm", ff="mace")
        from macer.utils.training_mace import run_mace_training
        run_mace_training(args)
    
    # Execute Active Learning
    elif args.category == "active":
        if args.action == "query":
            from macer.active_learning.query import query_uncertain_structures
            query_uncertain_structures(
                traj_path=args.traj,
                model_paths=args.models,
                top_k=args.top_k,
                threshold=args.threshold,
                output_dir=args.output_dir,
                device=args.device,
                step_interval=args.interval
            )
        else:
            p_map["active"].print_help()

    # --- Group 3: Data & Evaluation ---

    # Execute Dataset Actions
    elif args.category in ["dataset", "ds"]:
        if not args.action:
            p_map["dataset"].print_help()
            return
        
        # Check specific dataset actions
        if args.action == "build" and not args.inputs:
            p_map["dataset_build"].print_help()
            return
        elif args.action == "split":
            # Force help if no arguments provided at all (user just typed 'split')
            # Check if flags are in sys.argv to allow using defaults via explicit flags if desired
            if not any(arg in sys.argv for arg in ["-i", "--input", "--ratio", "--seed"]):
                p_map["dataset_split"].print_help()
                return
            
            # Use default input if not specified
            if not args.input:
                args.input = "dataset.xyz"

        print_util_banner("dataset", action=args.action)
        from macer.utils.dataset_tools import build_dataset, split_dataset
        if args.action == "build":
            build_dataset(args.inputs, args.output, args.stress_unit)
        elif args.action == "split":
            split_dataset(
                args.input, 
                args.ratio[0], args.ratio[1], args.ratio[2], 
                args.seed,
                train_out=args.train_out,
                valid_out=args.valid_out,
                test_out=args.test_out
            )

    # Execute General Evaluation
    elif args.category in ["evaluate", "eval"]:
        if not args.data:
            p_map["eval"].print_help()
            return
        data_path = args.data
        converted_path = None
        if _looks_like_ml_ab(data_path):
            from macer.training.data_prep import convert_to_mattersim_format
            from macer.utils.dataset_tools import get_unique_filename
            base_name = os.path.basename(data_path)
            output_name = f"{base_name}.mattersim.xyz"
            output_path = get_unique_filename(os.path.join(os.getcwd(), output_name))
            print(f"Detected ML_AB. Converting to extended xyz: {output_path}")
            convert_to_mattersim_format(data_path, output_path)
            data_path = output_path
            converted_path = output_path
        print_util_banner("evaluate", ff=args.ff, model=args.model)
        from macer.utils.evaluation import evaluate_model
        metrics_str = evaluate_model(
            data_path=data_path,
            ff=args.ff,
            model_path=args.model,
            device=args.device,
            output_dir="."
        )
        if metrics_str:
            print(metrics_str)

    # Execute Struct
    elif args.category == "struct":
        from macer.utils.struct_tools import vasp4to5
        from macer.utils.struct_tools import resolve_structure_inputs
        from macer.utils.viz_tools import plot_bz_3d
        if args.action == "vasp4to5":
            if not getattr(args, "input", None):
                for a in p_map["struct"]._actions:
                    if isinstance(a, argparse._SubParsersAction):
                        a.choices["vasp4to5"].print_help()
                        break
                return
            vasp4to5(args.input, args.symbols, args.output)
        elif args.action == "bz-3d":
            inputs = resolve_structure_inputs(
                poscar_paths=[args.poscar] if args.poscar else None,
                cif_paths=[args.cif] if args.cif else None,
                formula=args.formula,
                mpid=args.mpid,
                show_mp_progress=True
            )
            if not inputs:
                # Print subcommand help for bz-3d
                for a in p_map["struct"]._actions:
                    if isinstance(a, argparse._SubParsersAction):
                        a.choices["bz-3d"].print_help()
                        break
                return
            from pymatgen.core import Structure
            poscar_path = inputs[0]
            try:
                formula = Structure.from_file(poscar_path).composition.reduced_formula
            except Exception:
                formula = "Unknown"
            mpid = args.mpid if args.mpid else "local"
            out_html = f"BZ-{formula}-{mpid}.html"
            plot_bz_3d(
                poscar_path,
                out_html=out_html,
                plot_rc=args.relative_coordinate,
                special_points=(not args.no_special_points),
            )
            print(f"Created: {out_html}")
        else:
            p_map["struct"].print_help()

    # --- Group 4: Physics & Analysis ---

    # Execute MD
    elif args.category == "md":
        from macer.utils.md_tools import traj2xdatcar, md_summary, calculate_conductivity, analyze_cell_evolution
        from macer.utils.viz_tools import plot_md_log, plot_rdf
        if args.action == "traj2xdatcar":
            traj2xdatcar(args.input, args.output, interval=args.interval)
        elif args.action == "summary":
            md_summary(args.input)
        elif args.action == "conductivity":
            calculate_conductivity(args.input, args.temp, args.dt, interval=args.interval, charges_str=args.charges)
        elif args.action == "cell":
            analyze_cell_evolution(args.input, args.dt, args.skip, args.interval, args.output, args.poscar)
        elif args.action == "plot":
            plot_md_log(args.input, args.output)
        elif args.action == "rdf":
            plot_rdf(args.input, args.output, r_max=args.rmax, n_bins=args.bins)
        else:
            p_map["md"].print_help()

    # Execute Phonopy
    elif args.category in ["phonopy", "band", "gruneisen-band", "gruneisen-3d", "phonon-band", "pb", "run-qha", "qha", "run-sscha", "sscha", "qscaild", "run-qscaild", "thermal-conductivity", "tc", "finite-temperature", "phonon-ft", "pft", "symmetry-refine", "sr"]:
        if args.category in ["phonon-band", "pb"]:
            from macer.cli.phonopy_main import run_phonon_band_cli
            if not (getattr(args, "input_files", None) or getattr(args, "cif_files", None) or getattr(args, "formula", None) or getattr(args, "mpid", None)):
                p_map["phonon-band"].print_help()
                return
            run_phonon_band_cli(args)
            return
        if args.category in ["band", "gruneisen-band", "gruneisen-3d"]:
            # Treat as phonopy visualization tools
            args.action = args.category
            args.category = "phonopy"
        if getattr(args, "_util_forward_phonopy", None):
            _forward_phonopy(args._util_forward_phonopy, unknown_args)
            return
        from macer.utils.viz_tools import plot_phonon_band, plot_gruneisen_band, plot_gruneisen_3d, plot_gruneisen_3d_style
        if args.action == "band":
            if args.input == "band.dat" and not os.path.exists(args.input):
                print("Error: File not found: band.dat")
                p_map["band"].print_help()
                return
            plot_phonon_band(args.input, out_pdf=args.output, fmin=args.fmin, fmax=args.fmax, 
                             labels=args.labels, yaml_path=args.yaml)
        elif args.action == "gruneisen-band":
            # Explicit band plot
            if '*' not in args.input and not os.path.exists(args.input):
                print(f"Error: Input file '{args.input}' not found.")
                p_map["gruneisen-band"].print_help()
                return

            inputs = sorted(glob.glob(args.input)) if '*' in args.input else [args.input]
            if not inputs:
                print(f"Error: No files matching '{args.input}' found.")
                p_map["gruneisen-band"].print_help()
                return

            for f in inputs:
                # Construct output prefix
                base_name = os.path.splitext(f)[0]
                if args.output == "gruneisen":
                    current_output = base_name
                else:
                    # If user specified output, append suffix derived from input filename if multiple
                    suffix = base_name.replace("gruneisen", "") # Try to keep suffix
                    if len(inputs) > 1:
                        current_output = args.output + suffix
                    else:
                        current_output = args.output

                print(f"Processing {f} -> Output prefix: {current_output}")
                try:
                    plot_gruneisen_band(f, out_prefix=current_output, fmin=args.fmin, fmax=args.fmax, 
                                        gmin=args.gmin, gmax=args.gmax, filter_outliers=args.filter, 
                                        labels=args.labels, yaml_path=args.yaml)
                except Exception as e:
                    print(f"  Error processing {f}: {e}")

        elif args.action == "gruneisen-3d":
            if args.input == "gruneisen_full.dat" and not os.path.exists(args.input):
                print("No input file found (checked default 'gruneisen_full.dat').")
                print("Please specify an input file with -i/--input or ensure default file exists.")
                p_map["gruneisen-3d"].print_help()
                return

            if '*' not in args.input and not os.path.exists(args.input):
                print(f"Error: Input file '{args.input}' not found.")
                return

            inputs = sorted(glob.glob(args.input)) if '*' in args.input else [args.input]
            if not inputs:
                print(f"Error: No files matching '{args.input}' found.")
                return

            for f in inputs:
                base_name = os.path.splitext(f)[0]
                if args.output == "average_gruneisen_BZ":
                    base_out = base_name
                else:
                    base_out = args.output if len(inputs) == 1 else f"{args.output}_{base_name}"
                tasks = [("average", "mean"), ("minima", "min")]
                for suffix, mode in tasks:
                    current_out = f"{base_out}_{suffix}"
                    print(f"Processing {f} -> Output file: {current_out}.html/.pdf/.csv")
                    try:
                        from macer.utils.struct_tools import resolve_structure_inputs
                        poscar_path = args.poscar
                        if (not os.path.exists(poscar_path)) and (args.formula or args.mpid):
                            resolved = resolve_structure_inputs(
                                poscar_paths=None,
                                cif_paths=None,
                                formula=args.formula,
                                mpid=args.mpid,
                                show_mp_progress=True,
                            )
                            if resolved:
                                poscar_path = resolved[0]
                        plot_gruneisen_3d_style(
                            f,
                            poscar_path=poscar_path,
                            out_prefix=current_out,
                            cmin=args.gmin,
                            cmax=args.gmax,
                            cut_f=args.cutoff_freq,
                            original_position=args.original_position,
                            fill_bz=args.fill_BZ,
                            plot_rc=args.relative_coordinate,
                            expand_symmetry=args.expand_symmetry,
                            full_bz=args.full_bz,
                            half_bz_axis=args.half_axis,
                            use_hull_filter=args.hull_filter,
                            hull_tolerance=args.hull_tolerance,
                            special_points=(not args.no_special_points),
                            time_reversal=(not args.no_time_reversal),
                            symprec=args.symprec,
                            mode=mode,
                        )
                    except Exception as e:
                        print(f"  Error processing {f} ({mode}): {e}")
        else:
            p_map["phonopy"].print_help()

    # Execute DynaPhoPy
    elif args.category == "dynaphopy":
        from macer.cli.util import run_dynaphopy_full_wrapper
        # Use passed unknown_args if available, otherwise empty list
        run_dynaphopy_full_wrapper(args, unknown_args if unknown_args else [])
        return

UTIL_USAGE_GROUPED = """
Analysis tool:
  md                  MD post-processing utilities (traj2xdatcar, conductivity, rdf, etc.)
  bz-3d               Plot 1BZ wireframe + special q-points (HTML)
  vasp4to5            Convert VASP4 POSCAR to VASP5 (add symbols)

Model Management:
  get-model (gm)      Download and manage MLFF models
  model               Model management utilities (fp32, list, compile)

Training & Fine-tuning:
  fine-tunning (ft)   Fine-tune MatterSim models
  ft-convert (ftc)    Convert VASP ML_AB to extended XYZ
  train-mace (tm)     Train MACE models
  active              Active learning utilities (query uncertain structures)

Data & Evaluation:
  dataset (ds)        Dataset management utilities (build, split)
  evaluate (eval)     Evaluate any MLFF model on a dataset

Phonopy Tools:
  band              Plot phonon dispersion from .dat
  gruneisen-band    Plot Grüneisen parameters along band path (2D)
  gruneisen-3d      Plot Grüneisen parameters in 3D BZ

Pydefect Tools:
  find-interstitial (fi)
                    Find interstitial sites and write volumetric_data_local_extrema-like JSON.
  supercell (sc)    Create supercell_info.json without conventional cell conversion (manual supercell).
  cpd               Generate CPD (chem_pot_diag.json) and plot cpd.pdf/cpd.html.

Dynaphopy Utilities:
  dynaphopy           Internal DynaPhoPy engine (NumPy 2.x compatible, https://github.com/abelcarreras/DynaPhoPy)
"""

def main():
    from macer import __version__
    parser = argparse.ArgumentParser(
        description=MACER_LOGO + f"\nmacer util (v{__version__}): Utility tools for post-processing and analysis.\n" + UTIL_USAGE_GROUPED,
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument("--version", "-v", action="version", version=f"macer_util {__version__}")

    subparsers = parser.add_subparsers(dest="category", title=None, help=argparse.SUPPRESS)
    # We keep the help in add_util_parsers for other uses, 
    # but the custom description above provides the grouped view.
    add_util_parsers(subparsers)

    args, unknown_args = parser.parse_known_args()
    
    if not args.category:
        # Re-parse with main parser logic
        parser.print_help()
        return

    execute_util(args, unknown_args)

if __name__ == "__main__":
    main()
