"""Logistics service — pallet master data, weekly pallet flow, 13-week truck plan.

Builds on top of the existing supply pipeline. Source of truth for pallet
data is `data/logistic_master.csv` (extracted from the AIPK / ABC supplier
master file). Falls back to category-average pieces-per-pallet when a SKU
isn't listed there.

Three public methods, all returning plain dicts (FastAPI converts to JSON):
  * get_master()       — per-SKU logistic info + coverage stats
  * get_pallet_flow()  — weekly pallet in/out for the 13-week horizon
  * get_truck_plan()   — current ABC PO load packed into trucks (33 pallets each)
"""
from __future__ import annotations

import math
from collections import defaultdict
from pathlib import Path
from typing import Optional

import pandas as pd
from sqlalchemy import text
from sqlalchemy.orm import Session


ROOT = Path(__file__).resolve().parents[2]
DATA = ROOT / "data"

LOGISTIC_CSV       = DATA / "logistic_master.csv"
MOQ_CSV            = DATA / "moq_master.csv"
TRUCK_CAPACITY     = 33               # EUR pallets per truck
MAX_TRUCKS_WEEK    = 2                # 1 default, up to 2 for urgent / overflow
GOLD_SAFETY_W      = 2
DEFAULT_SAFETY_W   = 1
PRODUCTION_LAG     = 1                # produce in week N → arrive N+1
MAX_DELAY_WEEKS    = 8
HORIZON_WEEKS      = 13               # next 13 weeks for truck plan view

ABC_SUPPLIER_PATTERN = "ABC NUTRITIONAL"


# ---------------------------------------------------------------------------
def _load_master() -> pd.DataFrame:
    if not LOGISTIC_CSV.exists():
        return pd.DataFrame(columns=["sku", "article", "pcs_per_pallet"])
    return pd.read_csv(LOGISTIC_CSV)


def _load_moq() -> dict[str, dict]:
    """Load MOQ master and return {sku → {type, pallets, pieces, is_eol, raw}}.
    Empty dict if file missing — caller treats every SKU as 'no MOQ data'."""
    if not MOQ_CSV.exists():
        return {}
    df = pd.read_csv(MOQ_CSV)
    out: dict[str, dict] = {}
    for _, r in df.iterrows():
        out[r["sku"]] = {
            "moq_type":    r.get("moq_type", "unknown"),
            "moq_pallets": (None if pd.isna(r.get("moq_pallets"))
                              else float(r["moq_pallets"])),
            "moq_pieces":  (None if pd.isna(r.get("moq_pieces"))
                              else float(r["moq_pieces"])),
            "is_eol":      bool(r.get("is_eol", False)),
            "moq_raw":     str(r.get("moq_raw", "") or ""),
        }
    return out


def round_qty_to_moq(qty: float, sku: str, ppp: float,
                      moq_map: dict) -> tuple[float, str]:
    """Round qty up to nearest MOQ-compliant value. Returns (new_qty, reason)."""
    info = moq_map.get(sku)
    if info is None:
        # No MOQ data — fall back to whole-pallet rounding
        if ppp > 0:
            new = math.ceil(qty / ppp) * ppp
            return (new, "rounded to full pallet (no MOQ data)")
        return (qty, "")
    if info["is_eol"]:
        return (0, "EOL — do not reorder")
    if info["moq_type"] == "pallet" and info["moq_pallets"] and ppp > 0:
        # Round up to multiple of moq_pallets full pallets
        step = info["moq_pallets"] * ppp
        n_steps = max(1, math.ceil(qty / step))
        return (n_steps * step, f"rounded to {n_steps}×{info['moq_pallets']:.0f} pallet(s)")
    if info["moq_type"] == "pieces" and info["moq_pieces"]:
        # Must be ≥ MOQ pieces; also round up to whole pallet
        step = info["moq_pieces"]
        n_steps = max(1, math.ceil(qty / step))
        new_qty = n_steps * step
        # Then round to whole pallet on top
        if ppp > 0:
            new_qty = math.ceil(new_qty / ppp) * ppp
        return (new_qty, f"≥ MOQ {step:.0f} pcs, rounded to full pallet")
    if info["moq_type"] == "none":
        if ppp > 0:
            new = math.ceil(qty / ppp) * ppp
            return (new, "no MOQ — rounded to full pallet")
        return (qty, "no MOQ")
    return (qty, "unknown MOQ")


def _build_ppp_map(master: pd.DataFrame, sku_to_cat: dict) -> tuple[dict, dict, float]:
    """Return (sku→ppp, category→avg_ppp, overall_avg). Used by all three
    endpoints so the fallback chain is consistent."""
    sku_ppp = {
        r["sku"]: float(r["pcs_per_pallet"])
        for _, r in master.iterrows()
        if pd.notna(r.get("pcs_per_pallet")) and r["pcs_per_pallet"] > 0
    }
    master_cat = master.copy()
    master_cat["cat"] = master_cat["sku"].map(sku_to_cat)
    cat_avg = (
        master_cat[master_cat["pcs_per_pallet"] > 0]
        .groupby("cat")["pcs_per_pallet"].mean().to_dict()
    )
    overall = float(master_cat[master_cat["pcs_per_pallet"] > 0]
                     ["pcs_per_pallet"].mean()) if not master_cat.empty else 500.0
    return sku_ppp, cat_avg, overall


def _resolve_ppp(sku: str, sku_ppp: dict, cat_avg: dict, overall: float,
                  sku_to_cat: dict) -> tuple[float, str]:
    """Resolve pieces-per-pallet with fallback chain. Returns (value, source)."""
    if sku in sku_ppp:
        return sku_ppp[sku], "direct"
    cat = sku_to_cat.get(sku)
    if cat and cat in cat_avg and cat_avg[cat] > 0:
        return float(cat_avg[cat]), "category_avg"
    return overall, "overall_avg"


# ---------------------------------------------------------------------------
class LogisticsService:
    """Three views: master / pallet-flow / truck-plan."""

    def __init__(self, db: Session) -> None:
        self.db = db
        self._master = _load_master()
        self._moq = _load_moq()
        # Lookup: sku → category (pulled once from sku_planning + dim_products)
        rows = db.execute(text("""
            SELECT p.sku, COALESCE(c.name, '') AS cat
            FROM dim_products p
            LEFT JOIN dim_categories c ON c.id = p.category_id
        """)).mappings().all()
        self._sku_to_cat = {r["sku"]: r["cat"] for r in rows}
        self._sku_ppp, self._cat_avg, self._overall = _build_ppp_map(
            self._master, self._sku_to_cat
        )

    # -------------------------------------------------------------------
    # Endpoint 1: master data
    # -------------------------------------------------------------------
    def get_master(self) -> dict:
        """Per-SKU logistic info + MOQ + coverage stats."""
        rows = []
        for _, r in self._master.iterrows():
            sku = r.get("sku")
            moq = self._moq.get(sku, {})
            rows.append({
                "sku":              sku,
                "article":          r.get("article", ""),
                "shelf_life_days":  None if pd.isna(r.get("shelf_life_days"))
                                      else int(r["shelf_life_days"]),
                "pcs_per_showbox":  None if pd.isna(r.get("pcs_per_showbox"))
                                      else int(r["pcs_per_showbox"]),
                "pcs_per_box":      None if pd.isna(r.get("pcs_per_box"))
                                      else int(r["pcs_per_box"]),
                "boxes_per_pallet": None if pd.isna(r.get("boxes_per_pallet"))
                                      else int(r["boxes_per_pallet"]),
                "pcs_per_pallet":   None if pd.isna(r.get("pcs_per_pallet"))
                                      else int(r["pcs_per_pallet"]),
                "layers_per_pallet": None if pd.isna(r.get("layers_per_pallet"))
                                      else int(r["layers_per_pallet"]),
                "pallet_gross_kg":  None if pd.isna(r.get("pallet_gross_kg"))
                                      else float(r["pallet_gross_kg"]),
                # --- MOQ ---
                "moq_type":         moq.get("moq_type", "unknown"),
                "moq_raw":          moq.get("moq_raw", ""),
                "moq_pallets":      moq.get("moq_pallets"),
                "moq_pieces":       moq.get("moq_pieces"),
                "is_eol":           bool(moq.get("is_eol", False)),
            })

        # Coverage stats vs. all SKUs in the system
        total_skus = self.db.execute(text(
            "SELECT COUNT(*) AS n FROM dim_products"
        )).mappings().first()["n"]
        with_data = len(self._sku_ppp)
        n_moq      = sum(1 for m in self._moq.values()
                          if m["moq_type"] in ("pallet", "pieces", "none"))
        n_eol      = sum(1 for m in self._moq.values() if m["is_eol"])
        moq_type_breakdown = defaultdict(int)
        for m in self._moq.values():
            moq_type_breakdown[m["moq_type"]] += 1
        return {
            "total_skus_in_system":   int(total_skus or 0),
            "skus_with_master_data":  with_data,
            "coverage_pct":           (with_data / total_skus) if total_skus else 0,
            "category_avg_ppp":       {k: round(v, 0) for k, v in self._cat_avg.items()},
            "overall_avg_ppp":        round(self._overall, 0),
            "moq_skus":               n_moq,
            "moq_eol_skus":           n_eol,
            "moq_type_breakdown":     dict(moq_type_breakdown),
            "rows":                   rows,
        }

    # -------------------------------------------------------------------
    # Endpoint 4: MOQ compliance audit
    # -------------------------------------------------------------------
    def get_moq_audit(self) -> dict:
        """Compare open ABC POs against MOQ + pallet rules. Surface:
          - POs ordered in partial pallets (waste)
          - POs below MOQ
          - EOL SKUs being reordered (should not happen)
          - Recommended rounded qty per PO
        """
        rows = self.db.execute(text("""
            SELECT
                ins.year, ins.week,
                p.sku, COALESCE(p.name, '') AS article,
                COALESCE(c.name, '') AS category,
                COALESCE(sp.tier, '') AS tier,
                COALESCE(ds.name, '') AS supplier,
                ins.quantity,
                COALESCE(ec.cost_price, 0) AS cost_price
            FROM incoming_supply ins
            JOIN dim_products p        ON p.id = ins.product_id
            LEFT JOIN supply_master sm  ON sm.product_id = p.id
            LEFT JOIN dim_suppliers ds  ON ds.id = sm.supplier_id
            LEFT JOIN sku_planning sp   ON sp.product_id = p.id
            LEFT JOIN dim_categories c  ON c.id = p.category_id
            LEFT JOIN erp_costs ec      ON ec.product_id = p.id
            WHERE ds.name ILIKE :pat
              AND ins.year IS NOT NULL AND ins.week IS NOT NULL
            ORDER BY ins.year, ins.week, p.sku
        """), {"pat": f"%{ABC_SUPPLIER_PATTERN}%"}).mappings().all()

        audit = []
        for r in rows:
            sku = r["sku"]
            qty = float(r["quantity"])
            ppp, ppp_src = _resolve_ppp(sku, self._sku_ppp, self._cat_avg,
                                          self._overall, self._sku_to_cat)
            moq = self._moq.get(sku, {})
            pallets_exact = qty / ppp if ppp > 0 else 0
            pallets_ceil = math.ceil(pallets_exact)
            is_full_pallets = pallets_exact == math.floor(pallets_exact) and ppp > 0
            waste_pcs = (pallets_ceil - pallets_exact) * ppp if ppp > 0 else 0
            new_qty, reason = round_qty_to_moq(qty, sku, ppp, self._moq)
            below_moq = False
            if moq.get("moq_type") == "pieces" and moq.get("moq_pieces"):
                below_moq = qty < moq["moq_pieces"]
            cost = float(r["cost_price"])
            audit.append({
                "year":              int(r["year"]),
                "week":              int(r["week"]),
                "cw_label":          f"CW{int(r['week'])}",
                "sku":               sku,
                "article":           r["article"],
                "tier":              r["tier"],
                "category":          r["category"],
                "qty":               int(qty),
                "pcs_per_pallet":    int(ppp) if ppp else None,
                "ppp_source":        ppp_src,
                "pallets_exact":     round(pallets_exact, 2),
                "pallets_ceil":      pallets_ceil,
                "is_full_pallets":   bool(is_full_pallets),
                "waste_pcs":         int(round(waste_pcs)),
                "waste_pct":         round(
                    (waste_pcs / (pallets_ceil * ppp) * 100) if (ppp and pallets_ceil) else 0, 1
                ),
                "moq_type":          moq.get("moq_type", "unknown"),
                "moq_pieces":        moq.get("moq_pieces"),
                "moq_pallets":       moq.get("moq_pallets"),
                "moq_raw":           moq.get("moq_raw", ""),
                "is_eol":            bool(moq.get("is_eol", False)),
                "below_moq":         below_moq,
                "recommended_qty":   int(round(new_qty)),
                "recommended_reason": reason,
                "po_eur":            round(qty * cost, 2),
                "recommended_eur":   round(new_qty * cost, 2),
                "qty_delta":         int(round(new_qty - qty)),
            })

        n = len(audit)
        full = sum(1 for r in audit if r["is_full_pallets"])
        partial = n - full
        eol_violations = [r for r in audit if r["is_eol"]]
        below_moq = [r for r in audit if r["below_moq"]]
        total_waste_pcs = sum(r["waste_pcs"] for r in audit)
        return {
            "supplier":              ABC_SUPPLIER_PATTERN,
            "total_pos":             n,
            "full_pallet_pos":       full,
            "partial_pallet_pos":    partial,
            "partial_pallet_pct":    round(partial / n, 3) if n else 0,
            "eol_violations":        len(eol_violations),
            "below_moq_violations":  len(below_moq),
            "total_waste_pieces":    total_waste_pcs,
            "audit":                 audit,
        }

    # -------------------------------------------------------------------
    # Endpoint 2: weekly pallet flow (in/out)
    # -------------------------------------------------------------------
    def get_pallet_flow(self, n_weeks: int = HORIZON_WEEKS) -> dict:
        """Per-week pallet inflow (incoming POs) + outflow (forecast demand)
        for the next `n_weeks` weeks, with truck count per supplier."""
        # Current ISO week
        cur = self.db.execute(text(
            "SELECT EXTRACT(ISOYEAR FROM now())::int AS y, "
            "       EXTRACT(WEEK FROM now())::int AS w"
        )).mappings().first()
        cur_y, cur_w = int(cur["y"]), int(cur["w"])
        weeks: list[tuple[int, int]] = []
        y, w = cur_y, cur_w
        for _ in range(n_weeks):
            weeks.append((y, w))
            w += 1
            if w > 52: w = 1; y += 1

        # Inflow per (week, supplier)
        inflow_rows = self.db.execute(text("""
            SELECT
                ins.year,
                ins.week,
                p.sku,
                COALESCE(ds.name, '(unknown)') AS supplier,
                ins.quantity,
                COALESCE(ec.cost_price, 0) AS cost_price
            FROM incoming_supply ins
            JOIN dim_products p       ON p.id = ins.product_id
            LEFT JOIN supply_master sm ON sm.product_id = p.id
            LEFT JOIN dim_suppliers ds ON ds.id = sm.supplier_id
            LEFT JOIN erp_costs ec     ON ec.product_id = p.id
            WHERE ins.year IS NOT NULL AND ins.week IS NOT NULL
        """)).mappings().all()

        # Aggregate per (week, supplier) in pallets
        flow: dict[tuple[int, int], dict] = defaultdict(
            lambda: {"abc_pallets": 0.0, "non_abc_pallets": 0.0,
                      "abc_eur": 0.0, "non_abc_eur": 0.0,
                      "abc_pos": 0, "non_abc_pos": 0}
        )
        for r in inflow_rows:
            ppp, _ = _resolve_ppp(r["sku"], self._sku_ppp,
                                    self._cat_avg, self._overall,
                                    self._sku_to_cat)
            pal = math.ceil(float(r["quantity"]) / ppp) if ppp > 0 else 0
            eur = float(r["quantity"]) * float(r["cost_price"])
            key = (int(r["year"]), int(r["week"]))
            is_abc = ABC_SUPPLIER_PATTERN.lower() in (r["supplier"] or "").lower()
            if is_abc:
                flow[key]["abc_pallets"] += pal
                flow[key]["abc_eur"]      += eur
                flow[key]["abc_pos"]      += 1
            else:
                flow[key]["non_abc_pallets"] += pal
                flow[key]["non_abc_eur"]      += eur
                flow[key]["non_abc_pos"]      += 1

        # Outflow from latest forecast run
        fc_rows = self.db.execute(text("""
            SELECT f.year, f.week, p.sku, COALESCE(f.total, 0) AS qty,
                   COALESCE(ec.cost_price, 0) AS cost_price
            FROM forecasts f
            JOIN dim_products p ON p.id = f.product_id
            LEFT JOIN erp_costs ec ON ec.product_id = p.id
            WHERE f.run_id = (SELECT MAX(id) FROM forecast_runs)
        """)).mappings().all()

        outflow_pallets: dict[tuple[int, int], float] = defaultdict(float)
        outflow_eur:     dict[tuple[int, int], float] = defaultdict(float)
        for r in fc_rows:
            ppp, _ = _resolve_ppp(r["sku"], self._sku_ppp,
                                    self._cat_avg, self._overall,
                                    self._sku_to_cat)
            qty = float(r["qty"])
            if ppp > 0:
                outflow_pallets[(int(r["year"]), int(r["week"]))] += qty / ppp
            outflow_eur[(int(r["year"]), int(r["week"]))] += qty * float(r["cost_price"])

        # Build response
        result = []
        for (y, w) in weeks:
            f = flow.get((y, w), {"abc_pallets": 0, "non_abc_pallets": 0,
                                    "abc_eur": 0, "non_abc_eur": 0,
                                    "abc_pos": 0, "non_abc_pos": 0})
            out_p = outflow_pallets.get((y, w), 0.0)
            out_e = outflow_eur.get((y, w), 0.0)
            abc_p = f["abc_pallets"]
            total_in_p = abc_p + f["non_abc_pallets"]
            result.append({
                "year":             y,
                "week":             w,
                "cw_label":         f"CW{w}",
                "abc_pallets_in":   round(abc_p, 1),
                "abc_eur_in":       round(f["abc_eur"], 0),
                "abc_pos_count":    f["abc_pos"],
                "non_abc_pallets_in": round(f["non_abc_pallets"], 1),
                "non_abc_eur_in":   round(f["non_abc_eur"], 0),
                "total_pallets_in": round(total_in_p, 1),
                "pallets_out":      round(out_p, 1),
                "eur_out":          round(out_e, 0),
                "net_pallets":      round(total_in_p - out_p, 1),
                "abc_trucks":       round(abc_p / TRUCK_CAPACITY, 2),
                "abc_truck_overrun": abc_p > TRUCK_CAPACITY,
            })

        return {
            "current_year_week":  f"{cur_y}/CW{cur_w}",
            "horizon_weeks":      n_weeks,
            "truck_capacity_pallets": TRUCK_CAPACITY,
            "weeks":              result,
        }

    # -------------------------------------------------------------------
    # Endpoint 3: 13-week truck plan
    # -------------------------------------------------------------------
    def get_truck_plan(self, n_weeks: int = HORIZON_WEEKS) -> dict:
        """ABC POs packed into trucks (33 pallets each, full only).

        Algorithm:
          - Pull all open ABC POs from incoming_supply
          - Compute pieces_per_pallet per SKU (with category fallback)
          - Determine each PO's deadline from current stock cover
          - Best-fit pack into trucks (1 truck/week default, 2/week if urgent)
        """
        cur = self.db.execute(text(
            "SELECT EXTRACT(ISOYEAR FROM now())::int AS y, "
            "       EXTRACT(WEEK FROM now())::int AS w"
        )).mappings().first()
        cur_y, cur_w = int(cur["y"]), int(cur["w"])
        horizon_end_w = cur_w + n_weeks - 1
        end_y, end_w = cur_y, horizon_end_w
        while end_w > 52: end_w -= 52; end_y += 1

        # Pull ABC POs
        rows = self.db.execute(text("""
            SELECT
                ins.year, ins.week,
                p.sku, COALESCE(p.name, '') AS name,
                COALESCE(sp.tier, '') AS tier,
                COALESCE(c.name, '') AS category,
                ins.quantity,
                COALESCE(ec.cost_price, 0) AS cost_price,
                COALESCE(sm.lead_time_weeks, 9) AS lead_time_weeks
            FROM incoming_supply ins
            JOIN dim_products p        ON p.id = ins.product_id
            LEFT JOIN supply_master sm  ON sm.product_id = p.id
            LEFT JOIN dim_suppliers ds  ON ds.id = sm.supplier_id
            LEFT JOIN sku_planning sp   ON sp.product_id = p.id
            LEFT JOIN dim_categories c  ON c.id = p.category_id
            LEFT JOIN erp_costs ec      ON ec.product_id = p.id
            WHERE ds.name ILIKE :pattern
              AND ins.year IS NOT NULL AND ins.week IS NOT NULL
        """), {"pattern": f"%{ABC_SUPPLIER_PATTERN}%"}).mappings().all()

        # WH stock per SKU + run-rate (last 13 wks of sales)
        stock_rows = self.db.execute(text("""
            SELECT p.sku, SUM(esc.stock_qty)::float AS qty
            FROM erp_stock_current esc
            JOIN dim_products p ON p.id = esc.product_id
            JOIN dim_stores  ds ON ds.id = esc.store_id
            WHERE ds.is_warehouse = TRUE
            GROUP BY p.sku
        """)).mappings().all()
        wh = {r["sku"]: float(r["qty"] or 0) for r in stock_rows}

        run_rate_rows = self.db.execute(text("""
            SELECT p.sku,
                   SUM(GREATEST(t.quantity, 0)) / 13.0 AS rr
            FROM erp_transactions t
            JOIN dim_products p ON p.id = t.product_id
            LEFT JOIN lookup_channel_map cm ON cm.id = t.channel_map_id
            WHERE t.transaction_date >= (CURRENT_DATE - INTERVAL '91 days')
              AND cm.channel IN ('retail', 'webshop', 'wholesale')
            GROUP BY p.sku
        """)).mappings().all()
        rr = {r["sku"]: float(r["rr"] or 0) for r in run_rate_rows}

        # Build PO list with planning fields
        pos_info = []
        for r in rows:
            sku = r["sku"]
            ppp, ppp_src = _resolve_ppp(sku, self._sku_ppp, self._cat_avg,
                                          self._overall, self._sku_to_cat)
            qty = float(r["quantity"])
            pallets = math.ceil(qty / ppp) if ppp > 0 else 0
            cost = float(r["cost_price"])
            tier = str(r["tier"] or "").strip()
            is_gold = "GOLD" in tier.upper()
            safety = GOLD_SAFETY_W if is_gold else DEFAULT_SAFETY_W
            wh_qty = wh.get(sku, 0)
            wkly = rr.get(sku, 0)
            cover_w = (wh_qty / wkly) if wkly > 0 else 999.0
            deadline_from_cover = cur_w + max(1, int(cover_w - safety)) \
                                    if cover_w < 999 else cur_w + n_weeks - 1
            earliest = max(int(r["week"]), cur_w + PRODUCTION_LAG)
            deadline = max(earliest,
                            min(deadline_from_cover, cur_w + n_weeks - 1,
                                int(r["week"]) + MAX_DELAY_WEEKS))
            pos_info.append({
                "r":             r,
                "sku":           sku,
                "name":          r["name"],
                "tier":          tier,
                "category":      r["category"],
                "is_gold":       is_gold,
                "is_urgent":     is_gold and cover_w < 4,
                "original_week": int(r["week"]),
                "earliest":      earliest,
                "deadline":      deadline,
                "pallets":       pallets,
                "po_eur":        round(qty * cost, 2),
                "quantity":      int(qty),
                "cost_price":    round(cost, 4),
                "cover_weeks":   round(cover_w, 2),
                "ppp_used":      int(ppp),
                "ppp_source":    ppp_src,
                "lead_time_weeks": float(r["lead_time_weeks"]),
            })

        # Best-fit truck packing (mirrors build_abc_optimized_schedule.py)
        trucks: list[dict] = []

        def find_truck(po):
            candidates = [t for t in trucks
                           if (po["earliest"] <= t["week"] <= po["deadline"])
                           and (t["pallets"] + po["pallets"] <= TRUCK_CAPACITY)]
            if not candidates: return None
            return max(candidates, key=lambda t: t["pallets"])

        def open_truck(week: int):
            same = [t for t in trucks if t["week"] == week]
            if len(same) >= MAX_TRUCKS_WEEK: return None
            t = {"week": week, "pallets": 0.0, "pos": [],
                  "is_second": len(same) == 1}
            trucks.append(t)
            trucks.sort(key=lambda x: (x["week"], x["is_second"]))
            return t

        pos_sorted = sorted(pos_info, key=lambda p: (
            not p["is_urgent"], p["deadline"], -p["pallets"],
        ))
        unassigned: list[dict] = []
        for po in pos_sorted:
            t = find_truck(po)
            if t is not None:
                t["pos"].append(po); t["pallets"] += po["pallets"]; continue
            # Open new truck — urgent fills earliest, non-urgent latest
            week_order = (range(po["earliest"], po["deadline"] + 1)
                           if po["is_urgent"]
                           else range(po["deadline"], po["earliest"] - 1, -1))
            placed = False
            for w in week_order:
                new_t = open_truck(w)
                if new_t is not None:
                    new_t["pos"].append(po); new_t["pallets"] += po["pallets"]
                    placed = True; break
            if not placed:
                # No truck slot available within window (typically because the
                # PO's earliest delivery > horizon end, or all weeks already
                # have 2 trucks). Surface so caller can act.
                unassigned.append(po)

        # Format response
        trucks_out = []
        for t in trucks:
            fill = t["pallets"] / TRUCK_CAPACITY
            trucks_out.append({
                "truck_id":      f"TRUCK_CW{t['week']}_{'B' if t['is_second'] else 'A'}",
                "week":          t["week"],
                "cw_label":      f"CW{t['week']}",
                "production_week": t["week"] - PRODUCTION_LAG,
                "pallets":       round(t["pallets"], 1),
                "fill_rate":     round(fill, 3),
                "is_partial":    fill < 0.9,
                "is_second_truck": t["is_second"],
                "po_count":      len(t["pos"]),
                "po_eur":        round(sum(p["po_eur"] for p in t["pos"]), 2),
                "pos":           [{
                    "sku":       p["sku"],
                    "article":   p["name"],
                    "tier":      p["tier"],
                    "category":  p["category"],
                    "quantity":  p["quantity"],
                    "pallets":   p["pallets"],
                    "po_eur":    p["po_eur"],
                    "original_week": p["original_week"],
                    "cover_weeks": p["cover_weeks"],
                    "is_gold":   p["is_gold"],
                    "is_urgent": p["is_urgent"],
                    "ppp_used":  p["ppp_used"],
                    "ppp_source": p["ppp_source"],
                } for p in t["pos"]],
            })

        total_pallets = sum(p["pallets"] for p in pos_info)
        unassigned_out = [{
            "sku":            p["sku"],
            "article":        p["name"],
            "tier":           p["tier"],
            "category":       p["category"],
            "quantity":       p["quantity"],
            "pallets":        p["pallets"],
            "po_eur":         p["po_eur"],
            "original_week":  p["original_week"],
            "cover_weeks":    p["cover_weeks"],
            "is_gold":        p["is_gold"],
            "is_urgent":      p["is_urgent"],
            "ppp_used":       p["ppp_used"],
            "ppp_source":     p["ppp_source"],
        } for p in unassigned]
        return {
            "current_year_week": f"{cur_y}/CW{cur_w}",
            "horizon_weeks":     n_weeks,
            "horizon_end":       f"CW{end_w}",
            "truck_capacity_pallets": TRUCK_CAPACITY,
            "max_trucks_per_week":    MAX_TRUCKS_WEEK,
            "total_pos":         len(pos_info),
            "total_pallets":     round(total_pallets, 1),
            "trucks_needed":     math.ceil(total_pallets / TRUCK_CAPACITY)
                                   if total_pallets else 0,
            "trucks":            trucks_out,
            "unassigned_pos":    unassigned_out,
        }
