"""批次分配与序列号。纯逻辑，不触库；DB 装配在 services 层。

FEFO = First Expired First Out：先出效期最早的批次。无效期的批次排在有效期之后
（按 FIFO 即入库顺序），保证有临期货先走。
"""
from dataclasses import dataclass
from datetime import date
from decimal import Decimal


@dataclass(frozen=True)
class BatchStock:
    batch_id: int
    qty: Decimal
    expires_on: date | None
    created_id: int  # 批次自增 id，用作无效期时的 FIFO 次序与稳定排序


@dataclass(frozen=True)
class Allocation:
    batch_id: int
    qty: Decimal


def fefo_order(batches: list[BatchStock]) -> list[BatchStock]:
    """FEFO 排序：有效期升序在前，无效期按入库次序在后，同期按 id 稳定。"""
    return sorted(
        batches,
        key=lambda b: (b.expires_on is None, b.expires_on or date.max, b.created_id),
    )


def allocate_fefo(batches: list[BatchStock], need: Decimal
                  ) -> tuple[list[Allocation], Decimal]:
    """按 FEFO 从可用批次扣 need。

    返回 (分配明细, 缺口)。缺口 > 0 表示批次总量不足，剩余需求无批次可分——
    调用方据此进入「未分配负库存」异常桶，绝不虚构批次。
    """
    if need <= 0:
        raise ValueError("分配数量必须大于 0")
    remaining = need
    out: list[Allocation] = []
    for b in fefo_order(batches):
        if remaining <= 0:
            break
        avail = b.qty
        if avail <= 0:
            continue
        take = avail if avail < remaining else remaining
        out.append(Allocation(batch_id=b.batch_id, qty=take))
        remaining -= take
    return out, remaining


def validate_serials(requested: list[str], available: set[str]) -> None:
    """序列号出库校验：必须全部指定且存在且唯一（无重复）。"""
    if not requested:
        raise ValueError("序列号商品出库必须指定序列号")
    seen = set()
    for s in requested:
        if s in seen:
            raise ValueError(f"序列号重复：{s}")
        if s not in available:
            raise ValueError(f"序列号不存在或不可售：{s}")
        seen.add(s)
