# =============================================================================
#
#   KONFIGURACJA ANALIZY – EDYTUJ TĘ SEKCJĘ, ZAPISZ (Ctrl+S), URUCHOM SKRYPT
#
# =============================================================================

# --- Plik danych wejściowych (MissionLog) ------------------------------------
#     Wpisz sciezke do pliku CSV z danymi symulacji.
#     Mozesz uzyc ukosnikow (/) lub podwojnych odwrotnych ukosnikow (\\).
#     Przyklady:
#       "MissionLog.csv"            (plik w tym samym folderze)
#       "C:/dane/MissionLog.csv"
INPUT_FILE = "PKG_AS_1_3_10.csv"

# --- Plik typow pojazdow (opcjonalny) ----------------------------------------
#     Format: kolumna 1 = numer vehType,  kolumna 2 = nazwa pojazdu.
#     Wpisz None aby pomin.
VEHICLES_FILE = "vehicles.csv"

# --- Jednostka czasu wyswietlanego w tabelach --------------------------------
#     1  ->  sekundy symulacji (np. 3600)
#     2  ->  zegar 24h  HH:MM  (symulacja startuje o 00:00)
TIME_UNIT = 2

# --- Zakres danych do analizy ------------------------------------------------
#     Dla TIME_UNIT=1 (sekundy): podaj liczbe, np.  0   lub  86400
#     Dla TIME_UNIT=2 (HH:MM):  podaj string,  np. "00:00"  lub  "24:00"
#     Wpisz None aby uzyc pelnego zakresu.
ANALYSE_FROM  = None
ANALYSE_UNTIL = None

# --- Szerokosc przedzialu czasowego na profilu CO2 ---------------------------
#     Zawsze w sekundach.
#     Przyklady:  3600 = 1h,  1800 = 30 min,  7200 = 2h,  86400 = 1 doba
TIME_BIN_INTERVAL = 3600

# =============================================================================
#   KONIEC KONFIGURACJI – ponizej nie ma potrzeby niczego zmieniac
# =============================================================================

import sys, warnings, io, os
import pandas as pd
import numpy as np
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter

warnings.filterwarnings("ignore")

# ---------------------------------------------------------------------------
# Column definitions – 18 columns matching new MissionLog CSV format
# (last two columns: energyEquiv[kWh] and energyEquiv[MJ] pre-computed)
# ---------------------------------------------------------------------------
COLS = [
    "row", "startTime", "endTime", "distance",
    "fuelUsed_l", "fuelUsed_kg", "energyUsed_kWh", "CO2Emiss_kg",
    "numberOfNodesVisited", "missionType", "LMD_mode",
    "vehicle", "vehType", "noOfPackages", "listPushTime", "listPullTime",
    "energyEquiv_kWh", "energyEquiv_MJ",
]
NUMERIC_COLS = [
    "row", "startTime", "endTime", "distance",
    "fuelUsed_l", "fuelUsed_kg", "energyUsed_kWh", "CO2Emiss_kg",
    "missionType", "LMD_mode", "vehType", "noOfPackages",
    "listPushTime", "listPullTime", "energyEquiv_kWh", "energyEquiv_MJ",
]

LMD_LABELS = {
    1: "Food delivery (restaurants)",
    2: "Direct package delivery (couriers)",
    3: "Delivery to parcel lockers",
    4: "PL pickup by clients",
    5: "Drone delivery from PLs",
    6: "Direct drone delivery",
    7: "Food-box delivery to clients",
}

# ---------------------------------------------------------------------------
# Styles
# ---------------------------------------------------------------------------
HDR_FONT  = Font(bold=True, color="FFFFFF", name="Arial", size=10)
HDR_FILL  = PatternFill("solid", start_color="1F4E79")
SEC_FILL  = PatternFill("solid", start_color="2E75B6")
ALT_FILL  = PatternFill("solid", start_color="D6E4F0")
WHT_FILL  = PatternFill("solid", start_color="FFFFFF")
NORM_FONT = Font(name="Arial", size=10)
BOLD_FONT = Font(name="Arial", size=10, bold=True)
TITL_FONT = Font(bold=True, name="Arial", size=14, color="1F4E79")
SEC_FONT  = Font(bold=True, color="FFFFFF", name="Arial", size=10)
CENTER    = Alignment(horizontal="center", vertical="center")
LEFT      = Alignment(horizontal="left",   vertical="center")
RIGHT     = Alignment(horizontal="right",  vertical="center")

def _bd():
    s = Side(border_style="thin", color="AAAAAA")
    return Border(left=s, right=s, top=s, bottom=s)

def _hdr(cell, fill=HDR_FILL):
    cell.font = HDR_FONT; cell.fill = fill
    cell.alignment = CENTER; cell.border = _bd()

def _data(cell, alt=False, bold=False, align=RIGHT):
    cell.font = BOLD_FONT if bold else NORM_FONT
    cell.fill = ALT_FILL if alt else WHT_FILL
    cell.alignment = align; cell.border = _bd()

def _lbl(cell, alt=False, bold=False):
    _data(cell, alt=alt, bold=bold, align=LEFT)

def _sec_hdr(ws, row, text, ncols, fill=SEC_FILL):
    ws.merge_cells(start_row=row, start_column=1,
                   end_row=row, end_column=ncols)
    c = ws.cell(row=row, column=1, value=text)
    c.font = SEC_FONT; c.fill = fill
    c.alignment = CENTER; c.border = _bd()
    return row + 1

def _auto_width(ws):
    for col in ws.columns:
        w = 10
        for cell in col:
            try:
                if cell.value:
                    w = max(w, len(str(cell.value)) + 2)
            except Exception:
                pass
        ws.column_dimensions[
            get_column_letter(col[0].column)].width = min(w, 52)

# ---------------------------------------------------------------------------
# Time helpers
# ---------------------------------------------------------------------------
def sec_to_hhmm(s):
    if pd.isna(s): return ""
    s = int(s)
    return f"{s // 3600:02d}:{(s % 3600) // 60:02d}"

def fmt_time(s, use_clock):
    return sec_to_hhmm(s) if use_clock else f"{s:.0f}"

def fmt_range(s1, s2, use_clock):
    return f"{fmt_time(s1, use_clock)} – {fmt_time(s2, use_clock)}"

def parse_time_cfg(val, use_clock):
    if val is None: return None
    s = str(val).strip()
    if ":" in s:
        h, m = s.split(":")
        return int(h) * 3600 + int(m) * 60
    return float(s)

# ---------------------------------------------------------------------------
# Config validation
# ---------------------------------------------------------------------------
def validate_config():
    errors = []
    if not INPUT_FILE:
        errors.append("INPUT_FILE is empty.")
    if TIME_UNIT not in (1, 2):
        errors.append("TIME_UNIT must be 1 or 2.")
    if not isinstance(TIME_BIN_INTERVAL, (int, float)) or TIME_BIN_INTERVAL <= 0:
        errors.append("TIME_BIN_INTERVAL must be a positive number.")
    if errors:
        print("\n  [CONFIG ERROR]")
        for e in errors: print(f"    - {e}")
        sys.exit(1)

# ---------------------------------------------------------------------------
# Data loading
# ---------------------------------------------------------------------------
def _detect_sep(path):
    with open(path, "r", encoding="utf-8-sig") as fh:
        sample = fh.read(3000)
    tabs   = sample.count("\t")
    semis  = sample.count(";")
    commas = sample.count(",")
    if tabs >= semis and tabs >= commas // 4: return "\t"
    if semis >= tabs: return ";"
    return ","

def load_mission(path):
    sep = _detect_sep(path)
    with open(path, "r", encoding="utf-8-sig") as fh:
        raw = fh.read().replace(",", ".")
    df = pd.read_csv(
        io.StringIO(raw), sep=sep, header=0,
        names=COLS, dtype=str, on_bad_lines="skip",
    )
    df = df.dropna(how="all")
    for col in NUMERIC_COLS:
        if col in df.columns:
            df[col] = pd.to_numeric(df[col], errors="coerce")
        else:
            df[col] = np.nan
    return df

def load_vehicles(path):
    if not path or not os.path.exists(path): return {}
    sep = _detect_sep(path)
    with open(path, "r", encoding="utf-8-sig") as fh:
        raw = fh.read()
    try:
        df = pd.read_csv(io.StringIO(raw), sep=sep,
                         header=0, dtype=str, on_bad_lines="skip")
        if df.shape[1] < 2: return {}
        mapping = {}
        for _, row in df.iterrows():
            try:
                vt   = int(float(str(row.iloc[0]).strip()))
                name = str(row.iloc[1]).strip()
                if name and name.lower() != "nan":
                    mapping[vt] = name
            except Exception:
                pass
        return mapping
    except Exception as e:
        print(f"  [WARN] vehicles file error: {e}")
        return {}

def apply_span(df, t0, t1):
    return df[(df["startTime"] >= t0) & (df["startTime"] <= t1)].copy()

# ---------------------------------------------------------------------------
# Filter helpers
# ---------------------------------------------------------------------------
def svc(df):
    """Service deliveries: missionType==2 with at least 1 package."""
    return df[(df["missionType"] == 2) & (df["noOfPackages"] >= 1)].copy()

# ---------------------------------------------------------------------------
# KPI computations
# ---------------------------------------------------------------------------

# 1. Per LMD_mode aggregates (service deliveries)
def compute_per_lmd(df):
    sv = svc(df)
    g = sv.groupby("LMD_mode").agg(
        trips      =("row",             "count"),
        packages   =("noOfPackages",    "sum"),
        energy_kWh =("energyEquiv_kWh", "sum"),
        energy_MJ  =("energyEquiv_MJ",  "sum"),
        co2_kg     =("CO2Emiss_kg",      "sum"),
    ).reset_index()
    p = g["packages"].replace(0, np.nan)
    g["energy_kWh_per_pkg"] = g["energy_kWh"] / p
    g["energy_MJ_per_pkg"]  = g["energy_MJ"]  / p
    g["co2_per_pkg"]        = g["co2_kg"]      / p
    g["lmd_label"]          = g["LMD_mode"].astype(int).map(LMD_LABELS).fillna("")
    return g

# 2. Per LMD_mode × vehType matrix (service deliveries)
def compute_vehtype_lmd(df):
    sv = svc(df)
    g = sv.groupby(["LMD_mode", "vehType"]).agg(
        trips      =("row",             "count"),
        packages   =("noOfPackages",    "sum"),
        energy_kWh =("energyEquiv_kWh", "sum"),
        energy_MJ  =("energyEquiv_MJ",  "sum"),
        co2_kg     =("CO2Emiss_kg",      "sum"),
    ).reset_index()
    p = g["packages"].replace(0, np.nan)
    g["energy_kWh_per_pkg"] = g["energy_kWh"] / p
    g["energy_MJ_per_pkg"]  = g["energy_MJ"]  / p
    g["co2_per_pkg"]        = g["co2_kg"]      / p
    return g.sort_values(["LMD_mode", "vehType"]).reset_index(drop=True)

# 3. Transfer vs Service split per LMD_mode (all rows, not just deliveries)
def compute_split(df):
    rows = []
    for lmd in sorted(df["LMD_mode"].dropna().unique().astype(int)):
        sub  = df[df["LMD_mode"] == lmd]
        tr   = sub[sub["missionType"] == 1]
        sv   = sub[sub["missionType"] == 2]
        d_tr = tr["distance"].sum()
        d_sv = sv["distance"].sum()
        d_tot = d_tr + d_sv
        rows.append({
            "LMD_mode"            : lmd,
            "lmd_label"           : LMD_LABELS.get(lmd, ""),
            "dist_transfer"       : d_tr,
            "dist_service"        : d_sv,
            "dist_total"          : d_tot,
            "dist_transfer_pct"   : 100.0 * d_tr / d_tot if d_tot > 0 else 0.0,
            "dist_service_pct"    : 100.0 * d_sv / d_tot if d_tot > 0 else 0.0,
            "co2_transfer"        : tr["CO2Emiss_kg"].sum(),
            "co2_service"         : sv["CO2Emiss_kg"].sum(),
            "energy_kWh_transfer" : tr["energyEquiv_kWh"].sum(),
            "energy_kWh_service"  : sv["energyEquiv_kWh"].sum(),
            "energy_MJ_transfer"  : tr["energyEquiv_MJ"].sum(),
            "energy_MJ_service"   : sv["energyEquiv_MJ"].sum(),
        })
    return pd.DataFrame(rows)

# 4. Average delivery time per LMD_mode
def compute_delivery_time(df):
    sv = svc(df)
    mask = (sv["listPushTime"] > 0) & sv["listPushTime"].notna()
    sv_t = sv[mask].copy()
    sv_t["delivery_s"] = sv_t["endTime"] - sv_t["listPushTime"]
    g = sv_t.groupby("LMD_mode").agg(
        count          =("delivery_s", "count"),
        avg_delivery_s =("delivery_s", "mean"),
    ).reset_index()
    g["avg_delivery_min"] = g["avg_delivery_s"] / 60.0
    g["lmd_label"]        = g["LMD_mode"].astype(int).map(LMD_LABELS).fillna("")
    return g

# 5. CO2 emission profile per LMD_mode (all rows, by startTime bin)
def compute_co2_profile(df, interval, span_start, span_end):
    bins = np.arange(span_start, span_end + interval, interval)
    results = {}
    for lmd in sorted(df["LMD_mode"].dropna().unique().astype(int)):
        sub = df[df["LMD_mode"] == lmd].copy()
        sub["time_bin"] = pd.cut(sub["startTime"], bins=bins,
                                  labels=bins[:-1], right=False)
        g = sub.groupby("time_bin", observed=False).agg(
            co2_total =("CO2Emiss_kg", "sum"),
            trips     =("row",          "count"),
        ).reset_index()
        g["time_bin_s"]       = g["time_bin"].astype(float)
        g["time_bin_end_s"]   = g["time_bin_s"] + interval
        g["co2_avg_per_trip"] = g["co2_total"] / g["trips"].replace(0, np.nan)
        results[lmd] = g
    return results

# ---------------------------------------------------------------------------
# Excel writer
# ---------------------------------------------------------------------------
def _rv(v):
    """Round float or return empty string for NaN."""
    if isinstance(v, float) and np.isnan(v): return ""
    return v

def write_analysis_sheet(wb, df_full, per_lmd, veh_lmd, split,
                          delivery_time, co2_profile,
                          interval, veh_map, use_clock,
                          span_start, span_end):
    if "Analysis" in wb.sheetnames:
        del wb["Analysis"]
    ws = wb.create_sheet("Analysis")
    ws.sheet_view.showGridLines = False
    r = 1

    MAX_COLS = 14  # enough for all sections

    # Title row
    ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=MAX_COLS)
    c = ws.cell(row=r, column=1, value="FlexSim Simulation – Analysis Results")
    c.font = TITL_FONT; c.alignment = CENTER
    r += 1

    tu_str   = "24-h clock HH:MM" if use_clock else "simulation seconds"
    span_str = fmt_range(span_start, span_end, use_clock)
    ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=MAX_COLS)
    c2 = ws.cell(
        row=r, column=1,
        value=(f"Span: {span_str}  |  Time unit: {tu_str}  |  "
               f"Bin: {interval:.0f} s = {interval / 3600:.2f} h"))
    c2.font = Font(name="Arial", size=10, italic=True, color="595959")
    c2.alignment = CENTER
    r += 2

    # ── Section 1: KPIs per LMD_mode ─────────────────────────────────────────
    N1 = 10
    r = _sec_hdr(ws, r,
                 "1. KPIs per LMD_mode  (service deliveries: missionType=2, noOfPackages>=1)",
                 N1)
    hdrs1 = ["LMD_mode", "Description", "Trips", "Total packages",
             "Total energy [kWh]", "Total energy [MJ]", "Total CO2 [kg]",
             "Avg energy/pkg [kWh]", "Avg energy/pkg [MJ]", "Avg CO2/pkg [kg]"]
    for ci, h in enumerate(hdrs1, 1):
        _hdr(ws.cell(row=r, column=ci, value=h))
    r += 1
    for i, rd in per_lmd.iterrows():
        alt = i % 2 == 0
        vals = [
            int(rd["LMD_mode"]), rd["lmd_label"],
            int(rd["trips"]), int(rd["packages"]),
            round(rd["energy_kWh"], 4), round(rd["energy_MJ"], 4),
            round(rd["co2_kg"], 4),
            round(rd["energy_kWh_per_pkg"], 6) if not pd.isna(rd["energy_kWh_per_pkg"]) else "",
            round(rd["energy_MJ_per_pkg"],  6) if not pd.isna(rd["energy_MJ_per_pkg"])  else "",
            round(rd["co2_per_pkg"],        6) if not pd.isna(rd["co2_per_pkg"])        else "",
        ]
        for ci, v in enumerate(vals, 1):
            (_lbl if ci <= 2 else _data)(ws.cell(row=r, column=ci, value=v), alt=alt)
        r += 1
    r += 1

    # ── Section 2: KPIs per LMD_mode × vehType ───────────────────────────────
    N2 = 10
    r = _sec_hdr(ws, r,
                 "2. KPIs per LMD_mode x vehicle type  (service deliveries)",
                 N2)
    hdrs2 = ["LMD_mode", "Description", "vehType", "Vehicle name",
             "Trips", "Total packages",
             "Avg energy/pkg [kWh]", "Avg energy/pkg [MJ]",
             "Avg CO2/pkg [kg]", "Total CO2 [kg]"]
    for ci, h in enumerate(hdrs2, 1):
        _hdr(ws.cell(row=r, column=ci, value=h))
    r += 1
    for i, rd in veh_lmd.iterrows():
        alt = i % 2 == 0
        lmd = int(rd["LMD_mode"]); vt = int(rd["vehType"])
        vals = [
            lmd, LMD_LABELS.get(lmd, ""),
            vt,  veh_map.get(vt, ""),
            int(rd["trips"]), int(rd["packages"]),
            round(rd["energy_kWh_per_pkg"], 6) if not pd.isna(rd["energy_kWh_per_pkg"]) else "",
            round(rd["energy_MJ_per_pkg"],  6) if not pd.isna(rd["energy_MJ_per_pkg"])  else "",
            round(rd["co2_per_pkg"],        6) if not pd.isna(rd["co2_per_pkg"])        else "",
            round(rd["co2_kg"], 4),
        ]
        for ci, v in enumerate(vals, 1):
            (_lbl if ci <= 4 else _data)(ws.cell(row=r, column=ci, value=v), alt=alt)
        r += 1
    r += 1

    # ── Section 3: Transfer vs Service split ─────────────────────────────────
    N3 = 13
    r = _sec_hdr(ws, r,
                 "3. Transfer (empty-run, missionType=1) vs Service (missionType=2) split per LMD_mode",
                 N3)
    hdrs3 = [
        "LMD_mode", "Description",
        "Dist Transfer [m]", "Dist Service [m]", "Total dist [m]",
        "Transfer dist [%]", "Service dist [%]",
        "CO2 Transfer [kg]", "CO2 Service [kg]",
        "Energy Transfer [kWh]", "Energy Service [kWh]",
        "Energy Transfer [MJ]",  "Energy Service [MJ]",
    ]
    for ci, h in enumerate(hdrs3, 1):
        _hdr(ws.cell(row=r, column=ci, value=h))
    r += 1
    for i, rd in split.iterrows():
        alt = i % 2 == 0
        vals = [
            int(rd["LMD_mode"]), rd["lmd_label"],
            round(rd["dist_transfer"], 2),
            round(rd["dist_service"],  2),
            round(rd["dist_total"],    2),
            round(rd["dist_transfer_pct"], 2),
            round(rd["dist_service_pct"],  2),
            round(rd["co2_transfer"],        4),
            round(rd["co2_service"],         4),
            round(rd["energy_kWh_transfer"], 4),
            round(rd["energy_kWh_service"],  4),
            round(rd["energy_MJ_transfer"],  4),
            round(rd["energy_MJ_service"],   4),
        ]
        for ci, v in enumerate(vals, 1):
            (_lbl if ci <= 2 else _data)(ws.cell(row=r, column=ci, value=v), alt=alt)
        r += 1
    r += 1

    # ── Section 4: Average delivery time ─────────────────────────────────────
    N4 = 5
    r = _sec_hdr(ws, r,
                 "4. Average delivery time per LMD_mode  (endTime - listPushTime, service deliveries with listPushTime > 0)",
                 N4)
    hdrs4 = ["LMD_mode", "Description", "Deliveries counted",
             "Avg time [s]", "Avg time [min]"]
    for ci, h in enumerate(hdrs4, 1):
        _hdr(ws.cell(row=r, column=ci, value=h))
    r += 1
    for i, rd in delivery_time.iterrows():
        alt = i % 2 == 0
        vals = [
            int(rd["LMD_mode"]), rd["lmd_label"],
            int(rd["count"]),
            round(rd["avg_delivery_s"],   2) if not pd.isna(rd["avg_delivery_s"])   else "",
            round(rd["avg_delivery_min"], 2) if not pd.isna(rd["avg_delivery_min"]) else "",
        ]
        for ci, v in enumerate(vals, 1):
            (_lbl if ci <= 2 else _data)(ws.cell(row=r, column=ci, value=v), alt=alt)
        r += 1
    r += 1

    # ── Section 5: CO2 profile per LMD_mode ──────────────────────────────────
    lmd_ids = sorted(co2_profile.keys())
    N5 = max(3, 1 + len(lmd_ids) * 2)
    r = _sec_hdr(
        ws, r,
        f"5. CO2 emission profile per LMD_mode  "
        f"(all trips, bin = {interval:.0f} s = {interval / 3600:.2f} h)",
        N5,
    )
    t_col = "Time range (HH:MM)" if use_clock else "Time range (s)"
    _hdr(ws.cell(row=r, column=1, value=t_col))
    for li, lmd in enumerate(lmd_ids):
        base = 2 + li * 2
        _hdr(ws.cell(row=r, column=base,     value=f"LMD {lmd} – CO2 total [kg]"))
        _hdr(ws.cell(row=r, column=base + 1, value=f"LMD {lmd} – CO2 avg/trip [kg]"))
    r += 1

    if lmd_ids:
        ref = co2_profile[lmd_ids[0]]
        for i, trow in ref.iterrows():
            alt = i % 2 == 0
            t1  = trow["time_bin_s"]
            t2  = trow["time_bin_end_s"]
            _lbl(ws.cell(row=r, column=1,
                          value=fmt_range(t1, t2, use_clock)), alt=alt)
            for li, lmd in enumerate(lmd_ids):
                base    = 2 + li * 2
                profile = co2_profile[lmd]
                match   = profile[profile["time_bin_s"] == t1]
                if not match.empty:
                    total = float(match["co2_total"].iloc[0])
                    avg   = match["co2_avg_per_trip"].iloc[0]
                    avg_v = round(float(avg), 6) if not pd.isna(avg) else ""
                else:
                    total, avg_v = 0.0, ""
                _data(ws.cell(row=r, column=base,     value=round(total, 6)), alt=alt)
                _data(ws.cell(row=r, column=base + 1, value=avg_v),           alt=alt)
            r += 1

    _auto_width(ws)
    ws.freeze_panes = "A4"


def write_rawdata_sheet(wb, df_full):
    ws = wb.active
    ws.title = "RawData"
    for ci, col in enumerate(COLS, 1):
        _hdr(ws.cell(row=1, column=ci, value=col))
    for ri, row_vals in enumerate(df_full[COLS].values, 2):
        for ci, v in enumerate(row_vals, 1):
            ws.cell(
                row=ri, column=ci,
                value=None if (isinstance(v, float) and np.isnan(v)) else v,
            )
    for col in ws.columns:
        ws.column_dimensions[
            get_column_letter(col[0].column)].width = 14
    ws.freeze_panes = "A2"


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
    validate_config()

    use_clock = (TIME_UNIT == 2)
    interval  = float(TIME_BIN_INTERVAL)

    script_dir = os.path.dirname(os.path.abspath(__file__))
    input_path = (INPUT_FILE if os.path.isabs(INPUT_FILE)
                  else os.path.join(script_dir, INPUT_FILE))
    if not os.path.exists(input_path):
        print(f"\n  [ERROR] Input file not found: {input_path}")
        print("  Check the INPUT_FILE setting at the top of the script.")
        sys.exit(1)

    veh_path = None
    if VEHICLES_FILE:
        veh_path = (VEHICLES_FILE if os.path.isabs(VEHICLES_FILE)
                    else os.path.join(script_dir, VEHICLES_FILE))

    print(f"\n  Loading: {input_path}")
    df_full = load_mission(input_path)
    t_min   = float(df_full["startTime"].min())
    t_max   = float(df_full["endTime"].dropna().max())
    print(f"  Rows  : {len(df_full):,}")
    print(f"  Span  : {fmt_range(t_min, t_max, use_clock)}")
    print(f"  LMDs  : {sorted(df_full['LMD_mode'].dropna().unique().astype(int))}")
    print(f"  Types : {sorted(df_full['vehType'].dropna().unique().astype(int))}")

    span_start_raw = parse_time_cfg(ANALYSE_FROM,  use_clock)
    span_end_raw   = parse_time_cfg(ANALYSE_UNTIL, use_clock)
    span_start = (max(span_start_raw, t_min) if span_start_raw is not None else t_min)
    span_end   = (min(span_end_raw,   t_max) if span_end_raw   is not None else t_max)

    print(f"\n  Analysing span : {fmt_range(span_start, span_end, use_clock)}")
    print(f"  Bin interval   : {interval:.0f} s = {interval / 3600:.2f} h")

    df = apply_span(df_full, span_start, span_end)

    print(f"\n  Loading vehicles: {veh_path or '(not set)'}")
    veh_map = load_vehicles(veh_path) if veh_path else {}
    if veh_map:
        for vt, name in sorted(veh_map.items()):
            print(f"    vehType {vt:>3}  ->  {name}")
    else:
        print("    (no vehicle names loaded)")

    print("\n  Computing KPIs ...")
    per_lmd       = compute_per_lmd(df)
    veh_lmd       = compute_vehtype_lmd(df)
    split         = compute_split(df)
    delivery_time = compute_delivery_time(df)
    co2_profile   = compute_co2_profile(df, interval, span_start, span_end)

    DIV = "─" * 64
    print(f"\n  {DIV}")
    print("  RESULTS  (service deliveries, missionType=2, noOfPackages>=1)")
    print(f"  {DIV}")
    for _, rd in per_lmd.iterrows():
        print(f"  LMD {int(rd['LMD_mode']):>2}  |  pkgs: {int(rd['packages']):>6}  |"
              f"  kWh/pkg: {rd['energy_kWh_per_pkg']:.6f}  |"
              f"  MJ/pkg: {rd['energy_MJ_per_pkg']:.6f}  |"
              f"  CO2/pkg: {rd['co2_per_pkg']:.6f} kg")
    print(f"\n  Transfer vs Service distance split per LMD_mode:")
    for _, rd in split.iterrows():
        print(f"  LMD {int(rd['LMD_mode']):>2}  |  transfer: {rd['dist_transfer_pct']:.1f}%"
              f"  |  service: {rd['dist_service_pct']:.1f}%")
    print(f"  {DIV}")

    base     = os.path.splitext(input_path)[0]
    xlsx_out = base + "_analysis.xlsx"
    print(f"\n  Writing: {xlsx_out}")

    wb = Workbook()
    write_rawdata_sheet(wb, df_full)
    write_analysis_sheet(
        wb, df_full, per_lmd, veh_lmd, split,
        delivery_time, co2_profile,
        interval, veh_map, use_clock,
        span_start, span_end,
    )

    wb.save(xlsx_out)
    print(f"\n  Saved: {xlsx_out}")
    print("  Done.\n")


if __name__ == "__main__":
    main()
