"""Pydantic response models for the supply module.

Layering note: this file is imported by routers/supply.py and the service
that backs it. Repos return raw dicts; services map them into these shapes.
"""
from __future__ import annotations

from typing import Optional

from pydantic import BaseModel


# ----------------------------------------------------------------------
# Stock summary — dashboard headline
# ----------------------------------------------------------------------

class StockByLocation(BaseModel):
    """One row per (country, warehouse-flag) bucket. Used for the dashboard
    breakdown bar."""
    country: str
    is_warehouse: bool
    n_stores: int
    units: float
    purchase_value: float
    retail_value: float


class StockSummary(BaseModel):
    total_units: float
    total_purchase_value: float
    total_retail_value: float
    warehouse_units: float
    store_units: float
    by_location: list[StockByLocation]
    n_products: int          # how many SKUs have stock rows
    updated_at: Optional[str] = None  # max(updated_at) across rows


# ----------------------------------------------------------------------
# Supply dashboard — combines stock + demand + incoming
# ----------------------------------------------------------------------

class HorizonWeek(BaseModel):
    """One point in the dashboard's projection line chart."""
    year: int
    week: int
    year_week: int
    cw_label: str
    demand: float
    incoming: float
    opening_stock: float
    closing_stock: float


class SupplyDashboard(BaseModel):
    stock: StockSummary
    total_demand_13w: float
    total_incoming_13w: float
    avg_coverage_weeks: Optional[float] = None
    n_skus_low_coverage: int = 0      # coverage < 2
    n_skus_overstocked: int = 0       # coverage > 13
    horizon: list[HorizonWeek]
    note: Optional[str] = None


# ----------------------------------------------------------------------
# Stock projection — per-SKU 13-week roll-forward
# ----------------------------------------------------------------------

class ProjectionWeek(BaseModel):
    """One row in the per-SKU horizon view (kept compact so a 13-week grid
    isn't a 13-field object per SKU)."""
    year: int
    week: int
    year_week: int
    cw_label: str
    opening_stock: float
    demand: float
    incoming: float
    closing_stock: float


class ProjectionRow(BaseModel):
    sku: str
    name: Optional[str] = None
    category: Optional[str] = None
    tier: Optional[str] = None
    xyz: Optional[str] = None
    current_stock: float              # total = WH + stores
    warehouse_stock: float = 0        # dim_stores.is_warehouse
    store_stock: float = 0            # all non-warehouse locations
    avg_weekly_demand: float
    demand_13w: float
    incoming_13w: float
    coverage_weeks: Optional[float] = None
    closing_stock_eow_13: float       # closing stock at end of week 13
    status: str                       # "OK" / "Order Soon" / "Order Now" / "Pull in"
    weeks: list[ProjectionWeek]


class ProjectionTotals(BaseModel):
    n_skus: int
    total_current_stock: float
    total_warehouse_stock: float = 0
    total_store_stock: float = 0
    total_demand_13w: float
    total_incoming_13w: float
    by_status: dict[str, int]         # status → count
    # Streamlit-parity € rollup
    total_current_value_eur: float = 0
    total_forecasted_value_eur: float = 0   # planned SKUs (in sku_planning)
    total_longtail_value_eur: float = 0     # SKUs with stock but no plan


class ProjectionWeeklyAggregate(BaseModel):
    """Per-week roll-up across all SKUs in scope. Mirrors Streamlit's
    page_supply_projection global view: demand/incoming/closing + value."""
    year: int
    week: int
    cw_label: str
    opening_stock: float
    demand: float
    incoming: float
    closing_stock: float
    closing_value_eur: float
    # Supplier-plan overlay: per-week €NV from the buying team's
    # Template_Plan_ulaza_izlaza workbook. 0 for weeks before CW29/2026 —
    # the existing per-SKU PO data in incoming_supply covers those.
    planned_incoming_eur: float = 0.0


class KamSubmissionRow(BaseModel):
    """One person who's submitted on-top inputs. Used to populate the
    exclusion checkboxes on the StockProjection page."""
    submitted_by_id: Optional[int] = None
    person: str
    role: Optional[str] = None
    channel: Optional[str] = None
    n_sku_weeks: int
    qty_total: float
    buyers: list[str] = []            # distinct buyer names for nested checkboxes


class ProjectionResponse(BaseModel):
    rows: list[ProjectionRow]
    totals: ProjectionTotals
    horizon: list[str]                # ordered CW labels covering the 13w window
    kams_available: list[KamSubmissionRow]
    excluded_kams: list[str]          # echo of the filter we applied
    excluded_buyers: list[str]
    weekly_aggregate: list[ProjectionWeeklyAggregate] = []
    note: Optional[str] = None


# ----------------------------------------------------------------------
# Coverage grid — 13-week per-SKU rollforward keyed for the wide table
# ----------------------------------------------------------------------
# This is the same roll-forward as ProjectionResponse but with one row per
# metric (stock/demand/incoming/closing/coverage) per week — gives the
# frontend exactly what it needs to render Streamlit's "Coverage workbook"
# layout without any pivoting.

class CoverageCell(BaseModel):
    year: int
    week: int
    year_week: int
    cw_label: str
    opening_stock: float
    demand: float
    incoming: float
    closing_stock: float
    coverage_weeks: Optional[float] = None  # closing / avg_weekly_demand


class CoverageSku(BaseModel):
    sku: str
    name: Optional[str] = None
    category: Optional[str] = None
    tier: Optional[str] = None
    xyz: Optional[str] = None
    current_stock: float
    avg_weekly_demand: float
    cover_tail: float = 0.0     # run-rate used to extrapolate cover past the horizon
    lead_time_weeks: Optional[float] = None
    moq: Optional[float] = None
    supplier: Optional[str] = None
    weeks: list[CoverageCell]


class CoverageWeekTotal(BaseModel):
    year_week: int
    cw_label: str
    opening_stock: float
    demand: float
    incoming: float
    closing_stock: float


class CoverageResponse(BaseModel):
    rows: list[CoverageSku]
    horizon: list[str]
    totals_by_week: list[CoverageWeekTotal]
    note: Optional[str] = None


# ----------------------------------------------------------------------
# Reorder alerts — categorized by urgency
# ----------------------------------------------------------------------

class ReorderAlertRow(BaseModel):
    sku: str
    name: Optional[str] = None
    category: Optional[str] = None
    tier: Optional[str] = None
    current_stock: float
    avg_weekly_demand: float
    coverage_weeks: Optional[float] = None
    effective_coverage_weeks: Optional[float] = None  # includes incoming in lead time
    incoming_in_lead_time: float
    lead_time_weeks: Optional[float] = None
    reorder_point_units: Optional[float] = None       # 2× LT × avg_demand
    suggested_action: str          # human-readable: "Order 240 units (1 MOQ)"
    urgency: str                   # "Order Now" / "Order Soon" / "Pull in"
    supplier: Optional[str] = None


class AlertsResponse(BaseModel):
    order_now: list[ReorderAlertRow]
    order_soon: list[ReorderAlertRow]
    pull_in: list[ReorderAlertRow]
    n_total: int
    note: Optional[str] = None


# ----------------------------------------------------------------------
# Order entry — pre-filled suggestions + write surface
# ----------------------------------------------------------------------

class OrderSuggestionRow(BaseModel):
    sku: str
    name: Optional[str] = None
    category: Optional[str] = None
    tier: Optional[str] = None
    current_stock: float
    avg_weekly_demand: float
    incoming_in_lead_time: float
    coverage_weeks: Optional[float] = None
    lead_time_weeks: Optional[float] = None
    moq: Optional[float] = None
    supplier: Optional[str] = None
    target_stock_units: float          # 2 * LT * avg_demand
    gap_units: float                   # target - current - incoming
    suggested_qty: float               # gap rounded up to MOQ
    target_year_week: int              # earliest delivery week (now + lead_time)
    urgency: str                       # mirrors AlertsResponse


class OrderSuggestionsResponse(BaseModel):
    rows: list[OrderSuggestionRow]
    note: Optional[str] = None


# Write side — POST body
class OrderProposalInput(BaseModel):
    sku: str
    year_week: int
    proposed_qty: float
    status: str = "draft"   # 'draft' / 'pending' / 'approved'


class OrderProposalResult(BaseModel):
    """One row of feedback per submitted proposal. Sku-keyed so the client
    can paint per-row success/failure in the order-entry grid."""
    sku: str
    year_week: int
    id: Optional[int] = None
    status: str        # 'created' / 'skipped' (unknown sku) / 'error'
    message: Optional[str] = None


class OrderProposalSaveResponse(BaseModel):
    n_created: int
    n_skipped: int
    n_errors: int
    results: list[OrderProposalResult]


# ----------------------------------------------------------------------
# Inventory health — Streamlit page_supply_inventory_health
# ----------------------------------------------------------------------
# Three statuses ONLY: overstock / stockout_risk / balanced. The Streamlit
# formula is reproduced verbatim in the service — see app.py:9053-9372.

class InventoryHealthRow(BaseModel):
    sku: str
    name: Optional[str] = None
    category: Optional[str] = None
    tier: Optional[str] = None          # "01 GOLD" / ... / None for unplanned
    is_planned: bool
    supplier: Optional[str] = None
    on_hand: float
    incoming: float                     # total incoming across the horizon
    avg_weekly: float
    demand_signal: str                  # "Forecast" or "Run rate"
    cv: float
    lead_time_weeks: float
    fa_used: float                      # in [0, 1]
    fa_source: str                      # "measured" / "global" / "default"
    safety_stock: float
    safety_weeks: float
    max_cover_needed: float             # 1.5 × LT + safety_weeks
    max_on_hand: float                  # demand_over(max_cover_needed, series)
    cover_now: Optional[float] = None
    excess_units: float
    excess_eur: float                   # excess × cost_price (fallback: avg_sell)
    shortage_units: float
    status: str                         # "overstock" | "stockout_risk" | "balanced"
    unit_value: float
    valued_with: str                    # "cost" | "sell" | "—"


class InventoryHealthSplit(BaseModel):
    """Counts per status, split by planned vs unplanned for the summary cards."""
    planned: int = 0
    unplanned: int = 0


class InventoryHealthResponse(BaseModel):
    rows: list[InventoryHealthRow]
    total_excess_eur: float
    total_shortage_units: float
    by_status: dict[str, int]                  # overstock / stockout_risk / balanced → count
    by_status_planned:   dict[str, int]
    by_status_unplanned: dict[str, int]
    n_planned: int
    n_unplanned: int
    n_measured_fa: int                         # SKUs using per-SKU FA
    fa_global_default: float                   # global fallback FA in use
    valued_with_cost: int                      # SKUs valued using cost_price
    note: Optional[str] = None


# ----------------------------------------------------------------------
# Store overstock — Streamlit page_supply_store_overstock (planned only)
# ----------------------------------------------------------------------

class StoreOverstockRow(BaseModel):
    sku: str
    name: Optional[str] = None
    category: Optional[str] = None
    tier: str                       # planned only — tier always present
    store_code: str
    store_name: str
    on_hand: float
    avg_weekly: float               # non-promo avg
    sigma: float
    weeks_with_sales: int
    safety_stock: float
    cycle_stock: float
    optimal_stock: float
    overstock_units: float
    cost_price: Optional[float] = None
    sell_price: Optional[float] = None
    overstock_eur_cost: float
    overstock_eur_sell: float
    insufficient_history: bool


class StoreOverstockAgg(BaseModel):
    """One bucket of the rollups (by tier / store / category)."""
    label: str
    n_pairs: int
    overstock_units: float
    overstock_eur_cost: float
    overstock_eur_sell: float


class StoreOverstockResponse(BaseModel):
    rows: list[StoreOverstockRow]
    n_pairs_with_overstock: int
    n_pairs_total: int
    n_insufficient_history: int
    total_overstock_units: float
    total_overstock_eur_cost: float
    total_overstock_eur_sell: float
    by_tier:     list[StoreOverstockAgg]
    by_store:    list[StoreOverstockAgg]
    by_category: list[StoreOverstockAgg]
    country: str
    min_weeks_required: int
    lead_time_weeks: int
    note: Optional[str] = None


# ----------------------------------------------------------------------
# MOQ Analysis — supply_master + run rates for planned SKUs
# ----------------------------------------------------------------------

class MoqRow(BaseModel):
    sku: str
    name: Optional[str] = None
    category: Optional[str] = None
    tier: Optional[str] = None
    supplier: Optional[str] = None
    lead_time_weeks: Optional[float] = None
    moq: Optional[float] = None
    avg_weekly: float
    on_hand: float
    cover_now: Optional[float] = None          # on_hand / avg_weekly
    weeks_per_moq: Optional[float] = None      # moq / avg_weekly
    moq_eur_cost: Optional[float] = None       # moq × cost_price


class MoqResponse(BaseModel):
    rows: list[MoqRow]
    n_with_moq: int
    n_missing_moq: int
    note: Optional[str] = None


# ----------------------------------------------------------------------
# Logistics — incoming supply timeline grouped by supplier
# ----------------------------------------------------------------------

class LogisticsDelivery(BaseModel):
    sku: str
    name: Optional[str] = None
    tier: Optional[str] = None
    year: int
    week: int
    year_week: int
    cw_label: str
    quantity: float
    status: Optional[str] = None
    purchase_value: Optional[float] = None


class LogisticsSupplier(BaseModel):
    supplier: str
    n_skus: int
    n_deliveries: int
    total_qty: float
    total_eur: Optional[float] = None
    deliveries: list[LogisticsDelivery]


class LogisticsWeekSummary(BaseModel):
    year_week: int
    cw_label: str
    total_qty: float
    total_eur: Optional[float] = None
    n_suppliers: int


class LogisticsResponse(BaseModel):
    by_supplier: list[LogisticsSupplier]
    by_week: list[LogisticsWeekSummary]
    total_qty: float
    total_eur: Optional[float] = None
    n_weeks: int
    note: Optional[str] = None


# ----------------------------------------------------------------------
# Logistics MASTER — per-SKU pallet info from POLLEO Logistic data table
# ----------------------------------------------------------------------
class LogisticsMasterRow(BaseModel):
    sku: str
    article: Optional[str] = None
    shelf_life_days: Optional[int] = None
    pcs_per_showbox: Optional[int] = None
    pcs_per_box: Optional[int] = None
    boxes_per_pallet: Optional[int] = None
    pcs_per_pallet: Optional[int] = None
    layers_per_pallet: Optional[int] = None
    pallet_gross_kg: Optional[float] = None
    # MOQ
    moq_type: Optional[str] = None     # pallet / pieces / none / eol / unknown
    moq_raw: Optional[str] = None
    moq_pallets: Optional[float] = None
    moq_pieces: Optional[float] = None
    is_eol: bool = False


class LogisticsMasterResponse(BaseModel):
    total_skus_in_system: int
    skus_with_master_data: int
    coverage_pct: float
    category_avg_ppp: dict[str, float]
    overall_avg_ppp: float
    moq_skus: int = 0
    moq_eol_skus: int = 0
    moq_type_breakdown: dict[str, int] = {}
    rows: list[LogisticsMasterRow]


# ----------------------------------------------------------------------
# MOQ Audit — compare open ABC POs against pallet + MOQ rules
# ----------------------------------------------------------------------
class MoqAuditRow(BaseModel):
    year: int
    week: int
    cw_label: str
    sku: str
    article: Optional[str] = None
    tier: Optional[str] = None
    category: Optional[str] = None
    qty: int
    pcs_per_pallet: Optional[int] = None
    ppp_source: Optional[str] = None
    pallets_exact: float
    pallets_ceil: int
    is_full_pallets: bool
    waste_pcs: int
    waste_pct: float
    moq_type: Optional[str] = None
    moq_pieces: Optional[float] = None
    moq_pallets: Optional[float] = None
    moq_raw: Optional[str] = None
    is_eol: bool = False
    below_moq: bool = False
    recommended_qty: int
    recommended_reason: Optional[str] = None
    po_eur: float
    recommended_eur: float
    qty_delta: int


class MoqAuditResponse(BaseModel):
    supplier: str
    total_pos: int
    full_pallet_pos: int
    partial_pallet_pos: int
    partial_pallet_pct: float
    eol_violations: int
    below_moq_violations: int
    total_waste_pieces: int
    audit: list[MoqAuditRow]


# ----------------------------------------------------------------------
# Pallet Flow — per-week pallet in/out + truck count
# ----------------------------------------------------------------------
class PalletFlowWeek(BaseModel):
    year: int
    week: int
    cw_label: str
    abc_pallets_in: float
    abc_eur_in: float
    abc_pos_count: int
    non_abc_pallets_in: float
    non_abc_eur_in: float
    total_pallets_in: float
    pallets_out: float
    eur_out: float
    net_pallets: float
    abc_trucks: float
    abc_truck_overrun: bool


class PalletFlowResponse(BaseModel):
    current_year_week: str
    horizon_weeks: int
    truck_capacity_pallets: int
    weeks: list[PalletFlowWeek]


# ----------------------------------------------------------------------
# Truck Plan — ABC POs packed into full trucks for the 13-week horizon
# ----------------------------------------------------------------------
class TruckPo(BaseModel):
    sku: str
    article: Optional[str] = None
    tier: Optional[str] = None
    category: Optional[str] = None
    quantity: int
    pallets: float
    po_eur: float
    original_week: int
    cover_weeks: float
    is_gold: bool
    is_urgent: bool
    ppp_used: int
    ppp_source: str


class TruckPlanTruck(BaseModel):
    truck_id: str
    week: int
    cw_label: str
    production_week: int
    pallets: float
    fill_rate: float
    is_partial: bool
    is_second_truck: bool
    po_count: int
    po_eur: float
    pos: list[TruckPo]


class TruckPlanResponse(BaseModel):
    current_year_week: str
    horizon_weeks: int
    horizon_end: str
    truck_capacity_pallets: int
    max_trucks_per_week: int
    total_pos: int
    total_pallets: float
    trucks_needed: int
    trucks: list[TruckPlanTruck]
    unassigned_pos: list[TruckPo] = []


# ----------------------------------------------------------------------
# Costs — erp_costs + erp_prices, margin by category / SKU
# ----------------------------------------------------------------------

class CostRow(BaseModel):
    sku: str
    name: Optional[str] = None
    category: Optional[str] = None
    tier: Optional[str] = None
    is_planned: bool
    supplier: Optional[str] = None
    cost_price: Optional[float] = None
    avg_sell_price: Optional[float] = None
    margin_pct: Optional[float] = None        # (sell - cost) / sell × 100
    on_hand: float
    stock_value_cost: Optional[float] = None
    stock_value_sell: Optional[float] = None


class CostCategoryAgg(BaseModel):
    category: str
    n_skus: int
    stock_value_cost: float
    stock_value_sell: float
    avg_margin_pct: Optional[float] = None


class CostsResponse(BaseModel):
    rows: list[CostRow]
    by_category: list[CostCategoryAgg]
    total_stock_value_cost: float
    total_stock_value_sell: float
    n_with_cost: int
    n_with_price: int
    note: Optional[str] = None


# ----------------------------------------------------------------------
# Settings — read/write supply_master (MOQ + lead times) for planned SKUs
# ----------------------------------------------------------------------

class SettingsRow(BaseModel):
    sku: str
    name: Optional[str] = None
    category: Optional[str] = None
    tier: Optional[str] = None
    supplier: Optional[str] = None
    supplier_code: Optional[str] = None
    lead_time_weeks: Optional[float] = None
    moq: Optional[float] = None
    supply_master_id: Optional[int] = None    # None = no supply_master row yet


class SettingsResponse(BaseModel):
    rows: list[SettingsRow]
    n_with_supply_master: int
    n_missing: int
    note: Optional[str] = None


class SettingsUpdateItem(BaseModel):
    sku: str
    lead_time_weeks: Optional[float] = None
    moq: Optional[float] = None


class SettingsUpdateResponse(BaseModel):
    n_updated: int
    n_inserted: int
    n_errors: int
    errors: list[str] = []


# ----------------------------------------------------------------------
# Scenario Planner (Streamlit page_supply_scenarios parity)
# ----------------------------------------------------------------------

class ScenarioPolicy(BaseModel):
    postpone_trigger: float = 4.0
    postpone_delay: int = 4
    cancel_on: bool = True
    cancel_threshold: float = 16.0
    target_eur: float = 5_000_000.0


class ScenarioOverride(BaseModel):
    """One per-PO manual override. Action AUTO restores auto-classification."""
    sku: str
    po_year: int
    po_week: int
    action: str           # AUTO | PRODUCE | POSTPONE | CANCEL
    new_cw: Optional[int] = None


class ScenarioRequest(BaseModel):
    suppliers: list[str]
    window_start_cw: int
    window_end_cw: int
    stock_basis: str = "wh_plus_stores"     # "wh_only" or "wh_plus_stores"
    policy: ScenarioPolicy
    excluded_buyers: list[str] = []
    overrides: list[ScenarioOverride] = []


class ScenarioPORow(BaseModel):
    sku: str
    name: str = ""
    tier: str = ""
    category: str = ""
    supplier: str
    po_year: int
    po_week: int
    po_label: str
    qty: int
    cost_price: float
    eur_value: float
    lead_time_wk: float
    cover_now_wk: float
    cover_vs_lt: float
    cover_post_delivery_wk: float
    cover_after_arrival_wk: Optional[float] = None
    action_raw: str
    auto_action: str
    auto_new_week: str
    action: str
    new_delivery_week: str
    is_override: bool = False


class ScenarioChartPoint(BaseModel):
    cw_label: str
    year: int
    week: int
    baseline_eur: float
    scenario_eur: float
    # Supplier-plan overlay from the buying team's Template_Plan_ulaza_izlaza
    # workbook. Display-only, paints planned arrivals for CW29/2026+.
    planned_incoming_eur: float = 0.0


class ScenarioKPI(BaseModel):
    baseline_peak_eur: float
    baseline_peak_cw: str
    scenario_peak_eur: float
    scenario_peak_cw: str
    delta_eur: float
    cancel_eur: float
    postpone_eur: float
    total_unlocked_eur: float
    under_target: bool
    target_eur: float
    pos_affected: int
    n_cancel: int
    n_postpone: int
    n_produce: int


class ScenarioFlip(BaseModel):
    sku: str
    po_year: int
    po_week: int


class ScenarioMeta(BaseModel):
    current_year: int
    current_week: int
    suppliers: list[str]
    vp_buyers: list[str]
    horizon: int


class ScenarioResponse(BaseModel):
    meta: ScenarioMeta
    per_po: list[ScenarioPORow]
    chart: list[ScenarioChartPoint]
    kpi: ScenarioKPI
    flips: list[ScenarioFlip]
    excluded_demand_eur: float = 0


class ScenarioExportRequest(BaseModel):
    """Same body as ScenarioRequest plus action ('CANCEL' or 'POSTPONE')."""
    action: str
    scenario: ScenarioRequest


# ----------------------------------------------------------------------
# Order Proposal — supplier-scoped (s, S) inventory ordering tool
# ----------------------------------------------------------------------

class OrderProposalSupplier(BaseModel):
    """One supplier eligible for the order-proposal dropdown."""
    supplier_id: int
    supplier_name: str
    n_planned_skus: int            # how many planned SKUs this supplier has
    avg_lead_time_weeks: Optional[float] = None
    is_abc: bool = False           # surfaces the truck-fill summary in UI


class OrderProposalRow(BaseModel):
    """One row in the proposal table — per SKU recommended order."""
    sku: str
    name: Optional[str] = None
    tier: Optional[str] = None
    category: Optional[str] = None
    # Supply-side metrics
    current_wh_stock: float
    projected_stock_at_w_lt: float       # closing stock at W + LT walk-forward
    demand_lt_window: float              # sum of forecast demand over LT weeks
    avg_weekly_demand_lt: float          # demand_lt_window / LT (for tier triggers)
    lead_time_weeks: float
    reorder_point: float                 # trigger ×LT × avg_weekly
    target_stock: float                  # target ×LT × avg_weekly
    # Order proposal
    raw_qty: float                       # target - projected (before rounding/MOQ)
    proposed_order_qty: float            # after package-size rounding + MOQ floor
    moq: Optional[float] = None
    package_size: Optional[float] = None
    pcs_per_pallet: Optional[float] = None
    pallets: Optional[float] = None      # ceil(qty / pcs_per_pallet)
    cost_price: Optional[float] = None
    order_value_eur: Optional[float] = None
    # ABC truck packing flags (only meaningful for supplier=ABC Nutritional)
    triggered: bool = True               # False = pulled forward to fill truck
    pull_forward_reason: Optional[str] = None  # e.g. "added to fill ABC truck"


class OrderProposalABCTruck(BaseModel):
    """Truck-fill summary, only populated when supplier matches ABC Nutritional."""
    target_pallets: int = 33
    proposed_pallets: float
    fill_status: str                  # 'green'|'yellow'|'red'
    n_skus_pulled_forward: int = 0
    n_skus_deferred: int = 0
    note: Optional[str] = None


class OrderProposalResponse(BaseModel):
    """Container response — rows + summary + ABC truck (if applicable)."""
    supplier_id: int
    supplier_name: str
    current_year: int
    current_week: int
    horizon_weeks: int                  # LT window used (max across SKUs)
    n_skus_total: int                   # all planned SKUs for this supplier
    n_skus_triggered: int               # SKUs needing order before any pull-forward
    rows: list[OrderProposalRow]
    total_order_value_eur: float
    total_pallets: float
    abc_truck: Optional[OrderProposalABCTruck] = None
    methodology_notes: Optional[str] = None


class OrderProposalExportRequest(BaseModel):
    """Body for POST /supply/order-proposal/export — supplier id only;
    server recomputes the proposal so the export reflects current data."""
    supplier_id: int


class IncomingUploadResponse(BaseModel):
    """Result of POST /supply/upload-incoming — incoming-PO matrix upload."""
    filename: str
    year: int
    rows_parsed: int            # long (sku, week) rows found in the file
    skus: int                   # distinct SKUs
    weeks: list[int]            # ISO weeks covered
    total_units: float
    rows_inserted_to_db: int    # rows now in incoming_supply
    db_error: Optional[str] = None
    warnings: list[str] = []
