from __future__ import annotations

from decimal import Decimal
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import false, func, select
from sqlalchemy.orm import Session, aliased

from app.db.session import get_db
from app.models.master_data import Item, Warehouse
from app.models.operations import (
    CompletionReceipt,
    CompletionReceiptItem,
    Delivery,
    DeliveryItem,
    OperationReport,
    PurchaseOrder,
    PurchaseOrderItem,
    PurchaseReceipt,
    PurchaseReceiptItem,
    ReturnDisposition,
    ReturnItem,
    ReturnOrder,
    ScrapRecord,
    StockLot,
    Supplier,
    WorkOrder,
    WorkOrderMaterialIssue,
    WorkOrderOperation,
)
from app.models.org import Employee
from app.models.planning import MaterialDemand
from app.models.sales import Customer, SalesOrder, SalesOrderItem
from app.schemas.dashboard import (
    DashboardPayload,
    DashboardMaterialLotRead,
    DashboardSalesOrderRead,
    LifecycleDetailItem,
    LifecycleEdge,
    LifecycleNode,
    MaterialLotLifecycleRead,
    SalesOrderLifecycleRead,
)
from app.services.auth import require_authenticated_user
from app.services.mock_data import get_dashboard_payload

router = APIRouter(dependencies=[Depends(require_authenticated_user)])

STATUS_LABELS = {
    "ACTIVE": "启用",
    "INACTIVE": "停用",
    "OPEN": "未开始",
    "PARTIAL": "部分完成",
    "CLOSED": "已关闭",
    "ORDERED": "已下单",
    "RECEIVED": "已收货",
    "RELEASED": "已下达",
    "IN_PROGRESS": "进行中",
    "READY_TO_RECEIPT": "待成品入库",
    "COMPLETED": "已完工",
    "DONE": "已完成",
    "PENDING": "待处理",
    "PENDING_QC": "待质检",
    "PASSED": "已放行",
    "PASS": "已放行",
    "REJECTED": "已拒收",
    "REJECT": "已拒收",
    "POSTED": "已过账",
    "AVAILABLE": "可用",
    "LOCKED": "锁定",
    "DEPLETED": "已耗尽",
    "CONFIRMED": "已确认",
    "OVERRIDDEN": "已覆盖",
    "CREATED": "已创建",
    "IDLE": "空闲",
    "SUPERSEDED": "已替代",
    "SCRAP": "已报废",
    "SCRAPPED": "已报废",
    "NORMAL": "正常",
    "ABNORMAL": "异常",
}

WORK_ORDER_TYPE_LABELS = {
    "NORMAL": "常规生产",
    "REWORK": "返工生产",
}

DISPOSAL_TYPE_LABELS = {
    "SCRAP": "报废",
    "REWORK": "返工",
}


def _num(value: Any) -> float:
    if value is None:
        return 0.0
    if isinstance(value, Decimal):
        return float(value)
    try:
        return float(value)
    except (TypeError, ValueError):
        return 0.0


def _fmt_qty(value: Any, digits: int = 2) -> str:
    numeric = _num(value)
    if abs(numeric - round(numeric)) < 0.000001:
        return f"{int(round(numeric))}"
    return f"{numeric:.{digits}f}".rstrip("0").rstrip(".")


def _fmt_weight(value: Any) -> str:
    return f"{_fmt_qty(value, 3)} kg"


def _fmt_money(value: Any) -> str:
    return f"¥{_num(value):,.2f}"


def _fmt_date(value: Any) -> str:
    if not value:
        return "-"
    return str(value)[:10]


def _fmt_datetime(value: Any) -> str:
    if not value:
        return "-"
    return str(value).replace("T", " ")[:19]


def _label(mapping: dict[str, str], value: Any) -> str:
    if value is None or value == "":
        return "-"
    key = str(value).upper()
    return mapping.get(key, str(value))


def _status_label(value: Any) -> str:
    return _label(STATUS_LABELS, value)


def _disposal_label(value: Any) -> str:
    return _label(DISPOSAL_TYPE_LABELS, value)


def _detail(label: str, value: Any) -> LifecycleDetailItem:
    text = "-" if value is None or value == "" else str(value)
    return LifecycleDetailItem(label=label, value=text)


def _node(
    *,
    node_id: str,
    node_type: str,
    title: str,
    stage: str,
    level: int,
    subtitle: str | None = None,
    status: str | None = None,
    tone: str = "normal",
    summary: str | None = None,
    metrics: list[tuple[str, Any]] | None = None,
    details: list[tuple[str, Any]] | None = None,
    raw: dict[str, Any] | None = None,
) -> LifecycleNode:
    return LifecycleNode(
        id=node_id,
        type=node_type,
        title=title,
        subtitle=subtitle,
        status=status,
        stage=stage,
        level=level,
        tone=tone,
        summary=summary,
        metrics=[_detail(label, value) for label, value in (metrics or [])],
        details=[_detail(label, value) for label, value in (details or [])],
        raw=raw or {},
    )


def _edge(source: str, target: str, label: str | None = None) -> LifecycleEdge:
    return LifecycleEdge(id=f"{source}->{target}", source=source, target=target, label=label)


def _tone_for_status(status: str | None) -> str:
    value = str(status or "").upper()
    if value in {"REJECTED", "SCRAP", "SCRAPPED", "LOCKED"}:
        return "danger"
    if value in {"PENDING", "PENDING_QC", "OPEN", "PARTIAL", "IN_PROGRESS", "RELEASED", "NEW", "CREATED"}:
        return "warning"
    if value in {"CLOSED", "COMPLETED", "DONE", "POSTED", "PASSED", "RECEIVED", "CONFIRMED"}:
        return "success"
    return "normal"


def _sales_order_rows(limit: int = 100):
    line_subquery = (
        select(
            SalesOrderItem.sales_order_id.label("sales_order_id"),
            func.count(SalesOrderItem.id).label("line_count"),
            func.coalesce(func.sum(SalesOrderItem.order_qty), 0).label("total_order_qty"),
            func.coalesce(func.sum(SalesOrderItem.delivered_qty), 0).label("total_delivered_qty"),
        )
        .group_by(SalesOrderItem.sales_order_id)
        .subquery()
    )
    mrp_subquery = (
        select(
            SalesOrderItem.sales_order_id.label("sales_order_id"),
            func.count(MaterialDemand.id).label("mrp_count"),
        )
        .join(MaterialDemand, MaterialDemand.sales_order_item_id == SalesOrderItem.id)
        .where(MaterialDemand.status != "SUPERSEDED")
        .group_by(SalesOrderItem.sales_order_id)
        .subquery()
    )
    purchase_subquery = (
        select(
            SalesOrderItem.sales_order_id.label("sales_order_id"),
            func.count(func.distinct(PurchaseOrderItem.purchase_order_id)).label("purchase_order_count"),
        )
        .join(MaterialDemand, MaterialDemand.sales_order_item_id == SalesOrderItem.id)
        .join(PurchaseOrderItem, PurchaseOrderItem.source_demand_id == MaterialDemand.id)
        .group_by(SalesOrderItem.sales_order_id)
        .subquery()
    )
    work_order_subquery = (
        select(
            SalesOrderItem.sales_order_id.label("sales_order_id"),
            func.count(WorkOrder.id).label("work_order_count"),
        )
        .join(WorkOrder, WorkOrder.source_sales_order_item_id == SalesOrderItem.id)
        .group_by(SalesOrderItem.sales_order_id)
        .subquery()
    )
    completion_subquery = (
        select(
            SalesOrderItem.sales_order_id.label("sales_order_id"),
            func.count(func.distinct(CompletionReceipt.id)).label("completion_receipt_count"),
        )
        .join(WorkOrder, WorkOrder.source_sales_order_item_id == SalesOrderItem.id)
        .join(CompletionReceipt, CompletionReceipt.work_order_id == WorkOrder.id)
        .group_by(SalesOrderItem.sales_order_id)
        .subquery()
    )
    delivery_subquery = (
        select(
            Delivery.sales_order_id.label("sales_order_id"),
            func.count(Delivery.id).label("delivery_count"),
        )
        .group_by(Delivery.sales_order_id)
        .subquery()
    )
    return_subquery = (
        select(
            ReturnOrder.sales_order_id.label("sales_order_id"),
            func.count(ReturnOrder.id).label("return_count"),
        )
        .where(ReturnOrder.sales_order_id.is_not(None))
        .group_by(ReturnOrder.sales_order_id)
        .subquery()
    )

    return (
        select(
            SalesOrder.id.label("order_id"),
            SalesOrder.order_no.label("order_no"),
            Customer.customer_code.label("customer_code"),
            Customer.customer_name.label("customer_name"),
            SalesOrder.order_date.label("order_date"),
            SalesOrder.promised_date.label("promised_date"),
            SalesOrder.delivery_address.label("delivery_address"),
            SalesOrder.total_amount.label("total_amount"),
            SalesOrder.status.label("status"),
            func.coalesce(line_subquery.c.line_count, 0).label("line_count"),
            func.coalesce(line_subquery.c.total_order_qty, 0).label("total_order_qty"),
            func.coalesce(line_subquery.c.total_delivered_qty, 0).label("total_delivered_qty"),
            func.coalesce(mrp_subquery.c.mrp_count, 0).label("mrp_count"),
            func.coalesce(purchase_subquery.c.purchase_order_count, 0).label("purchase_order_count"),
            func.coalesce(work_order_subquery.c.work_order_count, 0).label("work_order_count"),
            func.coalesce(completion_subquery.c.completion_receipt_count, 0).label("completion_receipt_count"),
            func.coalesce(delivery_subquery.c.delivery_count, 0).label("delivery_count"),
            func.coalesce(return_subquery.c.return_count, 0).label("return_count"),
            SalesOrder.updated_at.label("updated_at"),
        )
        .join(Customer, Customer.id == SalesOrder.customer_id)
        .outerjoin(line_subquery, line_subquery.c.sales_order_id == SalesOrder.id)
        .outerjoin(mrp_subquery, mrp_subquery.c.sales_order_id == SalesOrder.id)
        .outerjoin(purchase_subquery, purchase_subquery.c.sales_order_id == SalesOrder.id)
        .outerjoin(work_order_subquery, work_order_subquery.c.sales_order_id == SalesOrder.id)
        .outerjoin(completion_subquery, completion_subquery.c.sales_order_id == SalesOrder.id)
        .outerjoin(delivery_subquery, delivery_subquery.c.sales_order_id == SalesOrder.id)
        .outerjoin(return_subquery, return_subquery.c.sales_order_id == SalesOrder.id)
        .order_by(SalesOrder.id.desc())
        .limit(limit)
    )


def _material_lot_rows(limit: int = 100):
    issue_subquery = (
        select(
            WorkOrderMaterialIssue.source_lot_id.label("source_lot_id"),
            func.count(func.distinct(WorkOrderMaterialIssue.material_sub_batch_no)).label("issued_batch_count"),
            func.coalesce(func.sum(WorkOrderMaterialIssue.issued_weight_kg), 0).label("issued_weight_kg"),
        )
        .group_by(WorkOrderMaterialIssue.source_lot_id)
        .subquery()
    )
    finished_subquery = (
        select(
            CompletionReceiptItem.source_material_lot_id.label("source_material_lot_id"),
            func.count(func.distinct(CompletionReceiptItem.id)).label("finished_batch_count"),
            func.coalesce(func.sum(CompletionReceiptItem.receipt_qty), 0).label("finished_qty"),
        )
        .where(CompletionReceiptItem.source_material_lot_id.is_not(None))
        .group_by(CompletionReceiptItem.source_material_lot_id)
        .subquery()
    )
    finished_lot = aliased(StockLot)
    delivery_subquery = (
        select(
            finished_lot.source_material_lot_id.label("source_material_lot_id"),
            func.count(func.distinct(Delivery.id)).label("delivery_count"),
        )
        .join(DeliveryItem, DeliveryItem.lot_id == finished_lot.id)
        .join(Delivery, Delivery.id == DeliveryItem.delivery_id)
        .where(finished_lot.source_material_lot_id.is_not(None))
        .group_by(finished_lot.source_material_lot_id)
        .subquery()
    )
    return (
        select(
            StockLot.id.label("lot_id"),
            StockLot.lot_no.label("lot_no"),
            StockLot.item_id.label("material_item_id"),
            Item.item_code.label("material_code"),
            Item.item_name.label("material_name"),
            Item.specification.label("specification"),
            Warehouse.warehouse_name.label("warehouse_name"),
            StockLot.inbound_weight_kg.label("inbound_weight_kg"),
            StockLot.remaining_weight_kg.label("remaining_weight_kg"),
            StockLot.unit_cost.label("unit_cost"),
            (StockLot.inbound_weight_kg * StockLot.unit_cost).label("inbound_amount"),
            func.coalesce(issue_subquery.c.issued_batch_count, 0).label("issued_batch_count"),
            func.coalesce(issue_subquery.c.issued_weight_kg, 0).label("issued_weight_kg"),
            func.coalesce(finished_subquery.c.finished_batch_count, 0).label("finished_batch_count"),
            func.coalesce(finished_subquery.c.finished_qty, 0).label("finished_qty"),
            func.coalesce(delivery_subquery.c.delivery_count, 0).label("delivery_count"),
            StockLot.status.label("status"),
            StockLot.quality_status.label("quality_status"),
            StockLot.created_at.label("created_at"),
        )
        .join(Item, Item.id == StockLot.item_id)
        .join(Warehouse, Warehouse.id == StockLot.warehouse_id)
        .outerjoin(issue_subquery, issue_subquery.c.source_lot_id == StockLot.id)
        .outerjoin(finished_subquery, finished_subquery.c.source_material_lot_id == StockLot.id)
        .outerjoin(delivery_subquery, delivery_subquery.c.source_material_lot_id == StockLot.id)
        .where(Item.item_type == "RAW_MATERIAL")
        .order_by(StockLot.created_at.desc(), StockLot.id.desc())
        .limit(limit)
    )


@router.get("/overview", response_model=DashboardPayload)
def overview() -> DashboardPayload:
    return get_dashboard_payload()


@router.get("/sales-orders", response_model=list[DashboardSalesOrderRead])
def list_dashboard_sales_orders(
    limit: int = Query(default=100, ge=1, le=500),
    db: Session = Depends(get_db),
) -> list[DashboardSalesOrderRead]:
    rows = db.execute(_sales_order_rows(limit=limit)).mappings().all()
    return [DashboardSalesOrderRead.model_validate(dict(row)) for row in rows]


@router.get("/material-lots", response_model=list[DashboardMaterialLotRead])
def list_dashboard_material_lots(
    limit: int = Query(default=100, ge=1, le=500),
    db: Session = Depends(get_db),
) -> list[DashboardMaterialLotRead]:
    rows = db.execute(_material_lot_rows(limit=limit)).mappings().all()
    return [DashboardMaterialLotRead.model_validate(dict(row)) for row in rows]


@router.get("/material-lots/{lot_id}/lifecycle", response_model=MaterialLotLifecycleRead)
def get_material_lot_lifecycle(lot_id: int, db: Session = Depends(get_db)) -> MaterialLotLifecycleRead:
    lot_row = db.execute(_material_lot_rows(limit=1).where(StockLot.id == lot_id)).mappings().first()
    if not lot_row:
        raise HTTPException(status_code=404, detail="原材料批次不存在")

    lot = DashboardMaterialLotRead.model_validate(dict(lot_row))
    nodes: list[LifecycleNode] = []
    edges: list[LifecycleEdge] = []
    node_ids: set[str] = set()
    edge_ids: set[str] = set()

    def add_node(node: LifecycleNode) -> None:
        if node.id not in node_ids:
            node_ids.add(node.id)
            nodes.append(node)

    def add_edge(edge: LifecycleEdge) -> None:
        if edge.source in node_ids and edge.target in node_ids and edge.id not in edge_ids:
            edge_ids.add(edge.id)
            edges.append(edge)

    lot_node_id = f"raw-lot-{lot.lot_id}"
    add_node(
        _node(
            node_id=lot_node_id,
            node_type="raw_material_lot",
            title=lot.lot_no,
            subtitle=f"{lot.material_code} · {lot.material_name}",
            status=lot.status,
            tone=_tone_for_status(lot.status),
            stage="原料入库",
            level=0,
            summary="原材料驱动的起点。该批次记录入库成本、剩余库存、质检状态，并向后追踪所有生产出库、成品入库和发货。",
            metrics=[
                ("入库重量", _fmt_weight(lot.inbound_weight_kg)),
                ("剩余重量", _fmt_weight(lot.remaining_weight_kg)),
                ("入库金额", _fmt_money(lot.inbound_amount)),
            ],
            details=[
                ("原材料编码", lot.material_code),
                ("原材料名称", lot.material_name),
                ("规格", lot.specification),
                ("仓库", lot.warehouse_name),
                ("入库单价", f"{_fmt_money(lot.unit_cost)}/kg"),
                ("质检状态", _status_label(lot.quality_status)),
                ("生产出库数", lot.issued_batch_count),
                ("已领用重量", _fmt_weight(lot.issued_weight_kg)),
                ("成品批次数", lot.finished_batch_count),
                ("已发货次数", lot.delivery_count),
            ],
            raw=lot.model_dump(mode="json"),
        )
    )

    issues = db.execute(
        select(
            WorkOrderMaterialIssue.id.label("issue_id"),
            WorkOrderMaterialIssue.material_sub_batch_no.label("material_sub_batch_no"),
            StockLot.lot_no.label("source_lot_no"),
            WorkOrderMaterialIssue.work_order_id.label("work_order_id"),
            WorkOrder.work_order_no.label("work_order_no"),
            WorkOrder.product_item_id.label("product_item_id"),
            Item.item_code.label("product_code"),
            Item.item_name.label("product_name"),
            WorkOrder.planned_qty.label("planned_qty"),
            WorkOrder.finished_qty.label("finished_qty"),
            WorkOrder.status.label("work_order_status"),
            WorkOrderMaterialIssue.issued_qty.label("issued_qty"),
            WorkOrderMaterialIssue.issued_weight_kg.label("issued_weight_kg"),
            WorkOrderMaterialIssue.unit_cost.label("unit_cost"),
            WorkOrderMaterialIssue.amount.label("amount"),
            WorkOrderMaterialIssue.issue_time.label("issue_time"),
            WorkOrderMaterialIssue.remark.label("remark"),
        )
        .join(WorkOrder, WorkOrder.id == WorkOrderMaterialIssue.work_order_id)
        .join(Item, Item.id == WorkOrder.product_item_id)
        .outerjoin(StockLot, StockLot.id == WorkOrderMaterialIssue.source_lot_id)
        .where(WorkOrderMaterialIssue.source_lot_id == lot_id)
        .order_by(WorkOrderMaterialIssue.issue_time, WorkOrderMaterialIssue.id)
    ).mappings().all()
    work_order_ids = sorted({row["work_order_id"] for row in issues})

    for row in issues:
        source_lot_no = row["source_lot_no"] or row["material_sub_batch_no"] or "未记录库存批次号"
        issue_node_id = f"issue-{row['issue_id']}"
        add_node(
            _node(
                node_id=issue_node_id,
                node_type="material_issue_batch",
                title=source_lot_no,
                subtitle=row["work_order_no"],
                status="POSTED",
                tone="success",
                stage="生产出库",
                level=1,
                summary="原材料按库存批次生产出库，记录领用重量、单价和成本。",
                metrics=[
                    ("领用重量", _fmt_weight(row["issued_weight_kg"])),
                    ("领用金额", _fmt_money(row["amount"])),
                    ("单价", f"{_fmt_money(row['unit_cost'])}/kg"),
                ],
                details=[
                    ("库存批次号", source_lot_no),
                    ("工单号", row["work_order_no"]),
                    ("产品", f"{row['product_code']} · {row['product_name']}"),
                    ("领用时间", _fmt_datetime(row["issue_time"])),
                    ("领用重量", _fmt_weight(row["issued_weight_kg"])),
                    ("备注", row["remark"]),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(lot_node_id, issue_node_id, "生产出库"))

        work_order_node_id = f"wo-{row['work_order_id']}"
        add_node(
            _node(
                node_id=work_order_node_id,
                node_type="work_order",
                title=row["work_order_no"],
                subtitle=f"{row['product_code']} · {row['product_name']}",
                status=row["work_order_status"],
                tone=_tone_for_status(row["work_order_status"]),
                stage="生产工单",
                level=2,
                summary="原材料按库存批次生产出库进入生产工单，后续工序报工、报废返工和完工入库均挂在该工单下。",
                metrics=[
                    ("计划数", _fmt_qty(row["planned_qty"])),
                    ("完工数", _fmt_qty(row["finished_qty"])),
                    ("本批领用", _fmt_weight(row["issued_weight_kg"])),
                ],
                details=[
                    ("工单号", row["work_order_no"]),
                    ("产品编码", row["product_code"]),
                    ("产品名称", row["product_name"]),
                    ("状态", _status_label(row["work_order_status"])),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(issue_node_id, work_order_node_id, "投入生产"))

    operations = db.execute(
        select(
            WorkOrderOperation.id.label("operation_id"),
            WorkOrderOperation.work_order_id.label("work_order_id"),
            WorkOrderOperation.seq_no.label("seq_no"),
            WorkOrderOperation.operation_name.label("operation_name"),
            WorkOrderOperation.planned_qty.label("planned_qty"),
            WorkOrderOperation.report_qty.label("report_qty"),
            WorkOrderOperation.good_qty.label("good_qty"),
            WorkOrderOperation.scrap_qty.label("scrap_qty"),
            WorkOrderOperation.rework_qty.label("rework_qty"),
            WorkOrderOperation.is_abnormal.label("is_abnormal"),
            WorkOrderOperation.status.label("status"),
        )
        .where(WorkOrderOperation.work_order_id.in_(work_order_ids) if work_order_ids else false())
        .order_by(WorkOrderOperation.work_order_id, WorkOrderOperation.seq_no)
    ).mappings().all()

    for row in operations:
        node_id = f"op-{row['operation_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="work_order_operation",
                title=f"OP{row['seq_no']:02d} {row['operation_name']}",
                subtitle="异常工序" if row["is_abnormal"] else "工序执行",
                status=row["status"],
                tone="danger" if row["is_abnormal"] else _tone_for_status(row["status"]),
                stage="工序执行",
                level=3,
                summary="工序节点展示该库存批次对应工单的加工进度、报废返工和异常情况。",
                metrics=[
                    ("计划数", _fmt_qty(row["planned_qty"])),
                    ("已报工", _fmt_qty(row["report_qty"])),
                    ("报废数", _fmt_qty(row["scrap_qty"])),
                ],
                details=[
                    ("合格数", _fmt_qty(row["good_qty"])),
                    ("返工数", _fmt_qty(row["rework_qty"])),
                    ("是否异常", "是" if row["is_abnormal"] else "否"),
                    ("状态", _status_label(row["status"])),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(f"wo-{row['work_order_id']}", node_id, "工艺路线"))

    reports = db.execute(
        select(
            OperationReport.id.label("report_id"),
            OperationReport.report_no.label("report_no"),
            OperationReport.work_order_id.label("work_order_id"),
            OperationReport.work_order_operation_id.label("work_order_operation_id"),
            Employee.employee_name.label("employee_name"),
            OperationReport.report_qty.label("report_qty"),
            OperationReport.good_qty.label("good_qty"),
            OperationReport.scrap_qty.label("scrap_qty"),
            OperationReport.rework_qty.label("rework_qty"),
            OperationReport.is_abnormal.label("is_abnormal"),
            OperationReport.scrap_reason.label("scrap_reason"),
            OperationReport.start_time.label("start_time"),
            OperationReport.end_time.label("end_time"),
        )
        .join(Employee, Employee.id == OperationReport.employee_id)
        .where(OperationReport.work_order_id.in_(work_order_ids) if work_order_ids else false())
        .order_by(OperationReport.start_time, OperationReport.id)
    ).mappings().all()

    for row in reports:
        node_id = f"report-{row['report_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="operation_report",
                title=row["report_no"],
                subtitle=row["employee_name"],
                status="ABNORMAL" if row["is_abnormal"] else "NORMAL",
                tone="danger" if row["is_abnormal"] else "success",
                stage="工人报工",
                level=4,
                summary="现场报工决定真实产出、报废、返工和人工/设备成本，是材料批次成本落到产品上的关键依据。",
                metrics=[
                    ("报工数", _fmt_qty(row["report_qty"])),
                    ("合格数", _fmt_qty(row["good_qty"])),
                    ("报废数", _fmt_qty(row["scrap_qty"])),
                ],
                details=[
                    ("员工", row["employee_name"]),
                    ("开始", _fmt_datetime(row["start_time"])),
                    ("结束", _fmt_datetime(row["end_time"])),
                    ("返工数", _fmt_qty(row["rework_qty"])),
                    ("异常原因", row["scrap_reason"] or ("报工异常" if row["is_abnormal"] else "-")),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(f"op-{row['work_order_operation_id']}", node_id, "扫码报工"))

    source_material_lot = aliased(StockLot)
    completion_items = db.execute(
        select(
            CompletionReceiptItem.id.label("completion_item_id"),
            CompletionReceipt.work_order_id.label("work_order_id"),
            CompletionReceipt.receipt_no.label("receipt_no"),
            CompletionReceipt.receipt_time.label("receipt_time"),
            CompletionReceiptItem.lot_no.label("lot_no"),
            CompletionReceiptItem.receipt_qty.label("receipt_qty"),
            CompletionReceiptItem.receipt_weight_kg.label("receipt_weight_kg"),
            CompletionReceiptItem.unit_cost.label("unit_cost"),
            CompletionReceiptItem.source_material_sub_batch_no.label("source_material_sub_batch_no"),
            func.coalesce(
                source_material_lot.lot_no,
                CompletionReceiptItem.source_material_sub_batch_no,
            ).label("source_material_lot_no"),
            CompletionReceiptItem.source_material_summary.label("source_material_summary"),
            Warehouse.warehouse_name.label("warehouse_name"),
        )
        .join(CompletionReceipt, CompletionReceipt.id == CompletionReceiptItem.completion_receipt_id)
        .join(Warehouse, Warehouse.id == CompletionReceipt.warehouse_id)
        .outerjoin(source_material_lot, source_material_lot.id == CompletionReceiptItem.source_material_lot_id)
        .where(CompletionReceipt.work_order_id.in_(work_order_ids) if work_order_ids else false())
        .order_by(CompletionReceipt.receipt_time, CompletionReceiptItem.id)
    ).mappings().all()
    completion_item_ids = [row["completion_item_id"] for row in completion_items]

    for row in completion_items:
        node_id = f"fg-{row['completion_item_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="completion_receipt",
                title=row["receipt_no"],
                subtitle=row["lot_no"],
                status="POSTED",
                tone="success",
                stage="成品入库",
                level=5,
                summary="成品入库批次已记录来源库存批次号，后续发货可以从成品批次倒查到原材料入库成本。",
                metrics=[
                    ("入库数量", _fmt_qty(row["receipt_qty"])),
                    ("入库重量", _fmt_weight(row["receipt_weight_kg"])),
                    ("单位成本", _fmt_money(row["unit_cost"])),
                ],
                details=[
                    ("成品批次", row["lot_no"]),
                    ("仓库", row["warehouse_name"]),
                    ("入库时间", _fmt_datetime(row["receipt_time"])),
                    ("来源库存批次号", row["source_material_lot_no"]),
                    ("来源摘要", row["source_material_summary"]),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(f"wo-{row['work_order_id']}", node_id, "完工入库"))

    finished_lot = aliased(StockLot)
    order_customer = aliased(Customer)
    direct_customer = aliased(Customer)
    deliveries = db.execute(
        select(
            DeliveryItem.id.label("delivery_item_id"),
            finished_lot.source_line_id.label("completion_item_id"),
            Delivery.delivery_no.label("delivery_no"),
            SalesOrder.order_no.label("order_no"),
            func.coalesce(direct_customer.customer_name, order_customer.customer_name).label("customer_name"),
            Delivery.delivery_date.label("delivery_date"),
            DeliveryItem.delivery_qty.label("delivery_qty"),
            DeliveryItem.delivery_weight_kg.label("delivery_weight_kg"),
            DeliveryItem.line_amount.label("line_amount"),
            DeliveryItem.status.label("status"),
            finished_lot.lot_no.label("lot_no"),
        )
        .join(finished_lot, finished_lot.id == DeliveryItem.lot_id)
        .join(Delivery, Delivery.id == DeliveryItem.delivery_id)
        .outerjoin(SalesOrder, SalesOrder.id == Delivery.sales_order_id)
        .outerjoin(order_customer, order_customer.id == SalesOrder.customer_id)
        .outerjoin(direct_customer, direct_customer.id == Delivery.customer_id)
        .where(finished_lot.source_line_id.in_(completion_item_ids) if completion_item_ids else false())
        .order_by(Delivery.delivery_date, DeliveryItem.id)
    ).mappings().all()

    for row in deliveries:
        node_id = f"delivery-{row['delivery_item_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="delivery",
                title=row["delivery_no"],
                subtitle=row["order_no"],
                status=row["status"],
                tone=_tone_for_status(row["status"]),
                stage="发货",
                level=6,
                summary="成品库存发给客户。即使订单晚于生产发生，也能从发货批次倒查到原材料入库批次和成本。",
                metrics=[
                    ("发货数量", _fmt_qty(row["delivery_qty"])),
                    ("发货重量", _fmt_weight(row["delivery_weight_kg"])),
                    ("发货金额", _fmt_money(row["line_amount"])),
                ],
                details=[
                    ("订单", row["order_no"]),
                    ("客户", row["customer_name"]),
                    ("库存批次号", row["lot_no"]),
                    ("发货时间", _fmt_datetime(row["delivery_date"])),
                    ("状态", _status_label(row["status"])),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(f"fg-{row['completion_item_id']}", node_id, "库存发货"))

    return MaterialLotLifecycleRead(lot=lot, nodes=nodes, edges=edges)


@router.get("/sales-orders/{sales_order_id}/lifecycle", response_model=SalesOrderLifecycleRead)
def get_sales_order_lifecycle(
    sales_order_id: int,
    db: Session = Depends(get_db),
) -> SalesOrderLifecycleRead:
    order_row = db.execute(_sales_order_rows(limit=1).where(SalesOrder.id == sales_order_id)).mappings().first()
    if not order_row:
        raise HTTPException(status_code=404, detail="订单不存在")

    order = DashboardSalesOrderRead.model_validate(dict(order_row))
    nodes: list[LifecycleNode] = []
    edges: list[LifecycleEdge] = []
    node_ids: set[str] = set()
    edge_ids: set[str] = set()

    def add_node(node: LifecycleNode) -> None:
        if node.id not in node_ids:
            node_ids.add(node.id)
            nodes.append(node)

    def add_edge(edge: LifecycleEdge) -> None:
        if edge.source in node_ids and edge.target in node_ids and edge.id not in edge_ids:
            edge_ids.add(edge.id)
            edges.append(edge)

    order_node_id = f"order-{order.order_id}"
    add_node(
        _node(
            node_id=order_node_id,
            node_type="sales_order",
            title=order.order_no,
            subtitle=order.customer_name,
            status=order.status,
            tone=_tone_for_status(order.status),
            stage="订单",
            level=0,
            summary="订单生命周期起点，后续 MRP、采购、生产、入库、发货和售后均围绕此单展开。",
            metrics=[
                ("订单金额", _fmt_money(order.total_amount)),
                ("产品行数", order.line_count),
                ("订单数量", _fmt_qty(order.total_order_qty)),
                ("已发数量", _fmt_qty(order.total_delivered_qty)),
            ],
            details=[
                ("订单号", order.order_no),
                ("客户编码", order.customer_code),
                ("客户名称", order.customer_name),
                ("订单日期", _fmt_date(order.order_date)),
                ("承诺交期", _fmt_date(order.promised_date)),
                ("送货地址", order.delivery_address),
                ("MRP需求数", order.mrp_count),
                ("采购单数", order.purchase_order_count),
                ("生产工单数", order.work_order_count),
                ("成品入库单数", order.completion_receipt_count),
                ("发货单数", order.delivery_count),
                ("退货单数", order.return_count),
                ("最近更新", _fmt_datetime(order.updated_at)),
            ],
            raw=order.model_dump(mode="json"),
        )
    )

    order_items = db.execute(
        select(
            SalesOrderItem.id.label("sales_order_item_id"),
            SalesOrderItem.line_no.label("line_no"),
            SalesOrderItem.product_item_id.label("product_item_id"),
            Item.item_code.label("product_code"),
            Item.item_name.label("product_name"),
            Item.specification.label("specification"),
            Item.unit_weight_kg.label("unit_weight_kg"),
            SalesOrderItem.customer_part_no.label("customer_part_no"),
            SalesOrderItem.order_qty.label("order_qty"),
            SalesOrderItem.delivered_qty.label("delivered_qty"),
            SalesOrderItem.unit_price.label("unit_price"),
            SalesOrderItem.line_amount.label("line_amount"),
            SalesOrderItem.promised_date.label("promised_date"),
            SalesOrderItem.status.label("status"),
        )
        .join(Item, Item.id == SalesOrderItem.product_item_id)
        .where(SalesOrderItem.sales_order_id == sales_order_id)
        .order_by(SalesOrderItem.line_no)
    ).mappings().all()

    item_ids = [row["sales_order_item_id"] for row in order_items]
    product_ids = [row["product_item_id"] for row in order_items]

    demands = db.execute(
        select(
            MaterialDemand.id.label("demand_id"),
            MaterialDemand.demand_no.label("demand_no"),
            MaterialDemand.sales_order_item_id.label("sales_order_item_id"),
            MaterialDemand.material_item_id.label("material_item_id"),
            Item.item_code.label("material_code"),
            Item.item_name.label("material_name"),
            Item.specification.label("specification"),
            MaterialDemand.order_qty.label("order_qty"),
            MaterialDemand.theoretical_weight_kg.label("theoretical_weight_kg"),
            MaterialDemand.planned_weight_kg.label("planned_weight_kg"),
            MaterialDemand.shortage_weight_kg.label("shortage_weight_kg"),
            MaterialDemand.suggested_purchase_weight_kg.label("suggested_purchase_weight_kg"),
            MaterialDemand.status.label("status"),
            MaterialDemand.generated_at.label("generated_at"),
            MaterialDemand.remark.label("remark"),
        )
        .join(Item, Item.id == MaterialDemand.material_item_id)
        .where(MaterialDemand.sales_order_item_id.in_(item_ids) if item_ids else false())
        .where(MaterialDemand.status != "SUPERSEDED")
        .order_by(MaterialDemand.id)
    ).mappings().all()
    demand_ids = [row["demand_id"] for row in demands]

    po_items = db.execute(
        select(
            PurchaseOrderItem.id.label("purchase_order_item_id"),
            PurchaseOrderItem.purchase_order_id.label("purchase_order_id"),
            PurchaseOrderItem.source_demand_id.label("source_demand_id"),
            PurchaseOrderItem.line_no.label("line_no"),
            PurchaseOrderItem.order_weight_kg.label("order_weight_kg"),
            PurchaseOrderItem.received_weight_kg.label("received_weight_kg"),
            PurchaseOrderItem.unit_price.label("unit_price"),
            PurchaseOrderItem.line_amount.label("line_amount"),
            PurchaseOrderItem.status.label("status"),
            PurchaseOrder.po_no.label("po_no"),
            PurchaseOrder.order_date.label("order_date"),
            PurchaseOrder.expected_date.label("expected_date"),
            PurchaseOrder.total_amount.label("total_amount"),
            Supplier.supplier_name.label("supplier_name"),
        )
        .join(PurchaseOrder, PurchaseOrder.id == PurchaseOrderItem.purchase_order_id)
        .join(Supplier, Supplier.id == PurchaseOrder.supplier_id)
        .where(PurchaseOrderItem.source_demand_id.in_(demand_ids) if demand_ids else false())
        .order_by(PurchaseOrder.id, PurchaseOrderItem.line_no)
    ).mappings().all()
    po_item_ids = [row["purchase_order_item_id"] for row in po_items]

    receipt_items = db.execute(
        select(
            PurchaseReceiptItem.id.label("receipt_item_id"),
            PurchaseReceiptItem.purchase_order_item_id.label("purchase_order_item_id"),
            PurchaseReceiptItem.line_no.label("line_no"),
            PurchaseReceiptItem.lot_no.label("lot_no"),
            PurchaseReceiptItem.received_weight_kg.label("received_weight_kg"),
            PurchaseReceiptItem.accepted_weight_kg.label("accepted_weight_kg"),
            PurchaseReceiptItem.unit_cost.label("unit_cost"),
            PurchaseReceiptItem.status.label("status"),
            PurchaseReceipt.receipt_no.label("receipt_no"),
            PurchaseReceipt.receipt_date.label("receipt_date"),
            Warehouse.warehouse_name.label("warehouse_name"),
        )
        .join(PurchaseReceipt, PurchaseReceipt.id == PurchaseReceiptItem.receipt_id)
        .join(Warehouse, Warehouse.id == PurchaseReceipt.warehouse_id)
        .where(PurchaseReceiptItem.purchase_order_item_id.in_(po_item_ids) if po_item_ids else false())
        .order_by(PurchaseReceipt.id, PurchaseReceiptItem.line_no)
    ).mappings().all()

    work_orders = db.execute(
        select(
            WorkOrder.id.label("work_order_id"),
            WorkOrder.work_order_no.label("work_order_no"),
            WorkOrder.source_sales_order_item_id.label("source_sales_order_item_id"),
            WorkOrder.product_item_id.label("product_item_id"),
            Item.item_code.label("product_code"),
            Item.item_name.label("product_name"),
            WorkOrder.work_order_type.label("work_order_type"),
            WorkOrder.planned_qty.label("planned_qty"),
            WorkOrder.finished_qty.label("finished_qty"),
            WorkOrder.scrap_qty.label("scrap_qty"),
            WorkOrder.planned_start_time.label("planned_start_time"),
            WorkOrder.planned_end_time.label("planned_end_time"),
            WorkOrder.actual_start_time.label("actual_start_time"),
            WorkOrder.actual_end_time.label("actual_end_time"),
            WorkOrder.priority_level.label("priority_level"),
            WorkOrder.status.label("status"),
            WorkOrder.remark.label("remark"),
        )
        .join(Item, Item.id == WorkOrder.product_item_id)
        .where(WorkOrder.source_sales_order_item_id.in_(item_ids) if item_ids else false())
        .order_by(WorkOrder.id)
    ).mappings().all()
    work_order_ids = [row["work_order_id"] for row in work_orders]

    operations = db.execute(
        select(
            WorkOrderOperation.id.label("work_order_operation_id"),
            WorkOrderOperation.work_order_id.label("work_order_id"),
            WorkOrderOperation.seq_no.label("seq_no"),
            WorkOrderOperation.operation_name.label("operation_name"),
            WorkOrderOperation.planned_qty.label("planned_qty"),
            WorkOrderOperation.report_qty.label("report_qty"),
            WorkOrderOperation.good_qty.label("good_qty"),
            WorkOrderOperation.scrap_qty.label("scrap_qty"),
            WorkOrderOperation.rework_qty.label("rework_qty"),
            WorkOrderOperation.allowed_scrap_rate.label("allowed_scrap_rate"),
            WorkOrderOperation.is_abnormal.label("is_abnormal"),
            WorkOrderOperation.status.label("status"),
            WorkOrderOperation.remark.label("remark"),
        )
        .where(WorkOrderOperation.work_order_id.in_(work_order_ids) if work_order_ids else false())
        .order_by(WorkOrderOperation.work_order_id, WorkOrderOperation.seq_no)
    ).mappings().all()
    operation_ids = [row["work_order_operation_id"] for row in operations]

    reports = db.execute(
        select(
            OperationReport.id.label("report_id"),
            OperationReport.report_no.label("report_no"),
            OperationReport.work_order_id.label("work_order_id"),
            OperationReport.work_order_operation_id.label("work_order_operation_id"),
            Employee.employee_name.label("employee_name"),
            OperationReport.start_time.label("start_time"),
            OperationReport.end_time.label("end_time"),
            OperationReport.report_qty.label("report_qty"),
            OperationReport.good_qty.label("good_qty"),
            OperationReport.scrap_qty.label("scrap_qty"),
            OperationReport.rework_qty.label("rework_qty"),
            OperationReport.scrap_reason.label("scrap_reason"),
            OperationReport.downtime_minutes.label("downtime_minutes"),
            OperationReport.extra_cost_amount.label("extra_cost_amount"),
            OperationReport.is_abnormal.label("is_abnormal"),
        )
        .join(Employee, Employee.id == OperationReport.employee_id)
        .where(OperationReport.work_order_operation_id.in_(operation_ids) if operation_ids else false())
        .order_by(OperationReport.id)
    ).mappings().all()
    report_ids = [row["report_id"] for row in reports]

    scrap_records = db.execute(
        select(
            ScrapRecord.id.label("scrap_record_id"),
            ScrapRecord.scrap_no.label("scrap_no"),
            ScrapRecord.report_id.label("report_id"),
            ScrapRecord.work_order_id.label("work_order_id"),
            ScrapRecord.work_order_operation_id.label("work_order_operation_id"),
            ScrapRecord.scrap_qty.label("scrap_qty"),
            ScrapRecord.scrap_weight_kg.label("scrap_weight_kg"),
            ScrapRecord.disposal_type.label("disposal_type"),
            ScrapRecord.saleable_scrap_weight_kg.label("saleable_scrap_weight_kg"),
            ScrapRecord.scrap_cost_amount.label("scrap_cost_amount"),
            ScrapRecord.scrap_reason.label("scrap_reason"),
            ScrapRecord.status.label("status"),
        )
        .where(ScrapRecord.report_id.in_(report_ids) if report_ids else false())
        .order_by(ScrapRecord.id)
    ).mappings().all()

    source_material_lot = aliased(StockLot)
    completion_items = db.execute(
        select(
            CompletionReceiptItem.id.label("completion_item_id"),
            CompletionReceiptItem.completion_receipt_id.label("completion_receipt_id"),
            CompletionReceipt.work_order_id.label("work_order_id"),
            CompletionReceipt.receipt_no.label("receipt_no"),
            CompletionReceipt.receipt_time.label("receipt_time"),
            CompletionReceiptItem.lot_no.label("lot_no"),
            CompletionReceiptItem.receipt_qty.label("receipt_qty"),
            CompletionReceiptItem.receipt_weight_kg.label("receipt_weight_kg"),
            CompletionReceiptItem.unit_cost.label("unit_cost"),
            CompletionReceiptItem.source_material_lot_id.label("source_material_lot_id"),
            source_material_lot.lot_no.label("source_material_lot_no"),
            CompletionReceiptItem.source_material_sub_batch_no.label("source_material_sub_batch_no"),
            CompletionReceiptItem.status.label("status"),
            Warehouse.warehouse_name.label("warehouse_name"),
        )
        .join(CompletionReceipt, CompletionReceipt.id == CompletionReceiptItem.completion_receipt_id)
        .join(Warehouse, Warehouse.id == CompletionReceipt.warehouse_id)
        .outerjoin(source_material_lot, source_material_lot.id == CompletionReceiptItem.source_material_lot_id)
        .where(CompletionReceipt.work_order_id.in_(work_order_ids) if work_order_ids else false())
        .order_by(CompletionReceipt.id, CompletionReceiptItem.line_no)
    ).mappings().all()

    deliveries = db.execute(
        select(
            DeliveryItem.id.label("delivery_item_id"),
            DeliveryItem.delivery_id.label("delivery_id"),
            Delivery.delivery_no.label("delivery_no"),
            DeliveryItem.sales_order_item_id.label("sales_order_item_id"),
            DeliveryItem.product_item_id.label("product_item_id"),
            DeliveryItem.lot_id.label("lot_id"),
            StockLot.lot_no.label("lot_no"),
            DeliveryItem.delivery_qty.label("delivery_qty"),
            DeliveryItem.delivery_weight_kg.label("delivery_weight_kg"),
            DeliveryItem.unit_price.label("unit_price"),
            DeliveryItem.line_amount.label("line_amount"),
            DeliveryItem.status.label("status"),
            Delivery.delivery_date.label("delivery_date"),
            Delivery.consignee_name.label("consignee_name"),
            Delivery.consignee_phone.label("consignee_phone"),
            Delivery.delivery_address.label("delivery_address"),
        )
        .join(Delivery, Delivery.id == DeliveryItem.delivery_id)
        .outerjoin(StockLot, StockLot.id == DeliveryItem.lot_id)
        .where(Delivery.sales_order_id == sales_order_id)
        .order_by(Delivery.id, DeliveryItem.line_no)
    ).mappings().all()

    return_items = db.execute(
        select(
            ReturnItem.id.label("return_item_id"),
            ReturnItem.return_order_id.label("return_order_id"),
            ReturnOrder.return_no.label("return_no"),
            ReturnOrder.return_date.label("return_date"),
            ReturnOrder.return_reason.label("return_reason"),
            ReturnOrder.status.label("return_status"),
            ReturnItem.sales_order_item_id.label("sales_order_item_id"),
            ReturnItem.product_item_id.label("product_item_id"),
            ReturnItem.return_qty.label("return_qty"),
            ReturnItem.return_weight_kg.label("return_weight_kg"),
            ReturnItem.disposition_status.label("disposition_status"),
            ReturnItem.responsibility_type.label("responsibility_type"),
            ReturnItem.remark.label("remark"),
        )
        .join(ReturnOrder, ReturnOrder.id == ReturnItem.return_order_id)
        .where(
            (ReturnOrder.sales_order_id == sales_order_id)
            | (ReturnItem.sales_order_item_id.in_(item_ids) if item_ids else false())
        )
        .order_by(ReturnOrder.id, ReturnItem.line_no)
    ).mappings().all()
    return_item_ids = [row["return_item_id"] for row in return_items]

    rework_wo = aliased(WorkOrder)
    return_dispositions = db.execute(
        select(
            ReturnDisposition.id.label("return_disposition_id"),
            ReturnDisposition.disposition_no.label("disposition_no"),
            ReturnDisposition.return_item_id.label("return_item_id"),
            ReturnDisposition.disposition_type.label("disposition_type"),
            ReturnDisposition.scrap_weight_kg.label("scrap_weight_kg"),
            ReturnDisposition.extra_cost_amount.label("extra_cost_amount"),
            ReturnDisposition.scrap_sale_amount.label("scrap_sale_amount"),
            ReturnDisposition.closed_at.label("closed_at"),
            ReturnDisposition.status.label("status"),
            ReturnDisposition.remark.label("remark"),
            rework_wo.work_order_no.label("rework_work_order_no"),
        )
        .outerjoin(rework_wo, rework_wo.id == ReturnDisposition.rework_work_order_id)
        .where(ReturnDisposition.return_item_id.in_(return_item_ids) if return_item_ids else false())
        .order_by(ReturnDisposition.id)
    ).mappings().all()

    # Build nodes and edges from left to right.
    for row in order_items:
        node_id = f"item-{row['sales_order_item_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="sales_order_item",
                title=f"L{row['line_no']:02d} {row['product_name']}",
                subtitle=row["product_code"],
                status=row["status"],
                tone=_tone_for_status(row["status"]),
                stage="产品明细",
                level=1,
                summary="订单中的一个产品行，后续 MRP、工单、发货和退货都可追溯到该行。",
                metrics=[
                    ("订单数", _fmt_qty(row["order_qty"])),
                    ("已发数", _fmt_qty(row["delivered_qty"])),
                    ("行金额", _fmt_money(row["line_amount"])),
                ],
                details=[
                    ("产品编码", row["product_code"]),
                    ("产品名称", row["product_name"]),
                    ("规格", row["specification"]),
                    ("单重", _fmt_weight(row["unit_weight_kg"])),
                    ("客户料号", row["customer_part_no"]),
                    ("承诺交期", _fmt_date(row["promised_date"])),
                    ("订单单价", _fmt_money(row["unit_price"])),
                    ("状态", _status_label(row["status"])),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(order_node_id, node_id, "订单产品"))

    for row in demands:
        node_id = f"mrp-{row['demand_id']}"
        source_id = f"item-{row['sales_order_item_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="mrp_demand",
                title=row["demand_no"],
                subtitle=f"{row['material_code']} · {row['material_name']}",
                status=row["status"],
                tone=_tone_for_status(row["status"]),
                stage="MRP需求",
                level=2,
                summary="按产品 BOM、订单数量、损耗率和库存余额生成的原材料需求。",
                metrics=[
                    ("计划重量", _fmt_weight(row["planned_weight_kg"])),
                    ("缺口重量", _fmt_weight(row["shortage_weight_kg"])),
                    ("建议采购", _fmt_weight(row["suggested_purchase_weight_kg"])),
                ],
                details=[
                    ("需求单号", row["demand_no"]),
                    ("原材料编码", row["material_code"]),
                    ("原材料名称", row["material_name"]),
                    ("规格", row["specification"]),
                    ("理论重量", _fmt_weight(row["theoretical_weight_kg"])),
                    ("订单数量", _fmt_qty(row["order_qty"])),
                    ("生成时间", _fmt_datetime(row["generated_at"])),
                    ("备注", row["remark"]),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "BOM展开"))

    for row in po_items:
        node_id = f"po-item-{row['purchase_order_item_id']}"
        source_id = f"mrp-{row['source_demand_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="purchase_order_item",
                title=f"{row['po_no']} L{row['line_no']:02d}",
                subtitle=row["supplier_name"],
                status=row["status"],
                tone=_tone_for_status(row["status"]),
                stage="采购订单",
                level=3,
                summary="MRP需求转为采购执行，记录供应商、采购重量、到货进度和采购金额。",
                metrics=[
                    ("采购重量", _fmt_weight(row["order_weight_kg"])),
                    ("已收重量", _fmt_weight(row["received_weight_kg"])),
                    ("行金额", _fmt_money(row["line_amount"])),
                ],
                details=[
                    ("采购单号", row["po_no"]),
                    ("供应商", row["supplier_name"]),
                    ("采购日期", _fmt_date(row["order_date"])),
                    ("预计到货", _fmt_date(row["expected_date"])),
                    ("采购单价", f"{_fmt_money(row['unit_price'])}/kg"),
                    ("采购总额", _fmt_money(row["total_amount"])),
                    ("状态", _status_label(row["status"])),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "采购下单"))

    for row in receipt_items:
        node_id = f"receipt-item-{row['receipt_item_id']}"
        source_id = f"po-item-{row['purchase_order_item_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="purchase_receipt_item",
                title=f"{row['receipt_no']} L{row['line_no']:02d}",
                subtitle=row["lot_no"],
                status=row["status"],
                tone=_tone_for_status(row["status"]),
                stage="到货质检",
                level=4,
                summary="采购到货后进入仓库待检/放行，批次号成为后续库存追溯的关键。",
                metrics=[
                    ("收货重量", _fmt_weight(row["received_weight_kg"])),
                    ("合格重量", _fmt_weight(row["accepted_weight_kg"])),
                    ("入库单价", f"{_fmt_money(row['unit_cost'])}/kg"),
                ],
                details=[
                    ("入库单号", row["receipt_no"]),
                    ("批次号", row["lot_no"]),
                    ("仓库", row["warehouse_name"]),
                    ("收货时间", _fmt_datetime(row["receipt_date"])),
                    ("质检/入库状态", _status_label(row["status"])),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "到货入库"))

    for row in work_orders:
        node_id = f"wo-{row['work_order_id']}"
        source_id = f"item-{row['source_sales_order_item_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="work_order",
                title=row["work_order_no"],
                subtitle=row["product_name"],
                status=row["status"],
                tone=_tone_for_status(row["status"]),
                stage="生产工单",
                level=5,
                summary="订单产品进入车间执行，工单承接 BOM 用料、工艺路线、报工和完工入库。",
                metrics=[
                    ("计划数", _fmt_qty(row["planned_qty"])),
                    ("完工数", _fmt_qty(row["finished_qty"])),
                    ("报废数", _fmt_qty(row["scrap_qty"])),
                ],
                details=[
                    ("工单号", row["work_order_no"]),
                    ("工单类型", _label(WORK_ORDER_TYPE_LABELS, row["work_order_type"])),
                    ("产品编码", row["product_code"]),
                    ("产品名称", row["product_name"]),
                    ("计划开始", _fmt_datetime(row["planned_start_time"])),
                    ("计划完工", _fmt_datetime(row["planned_end_time"])),
                    ("实际开始", _fmt_datetime(row["actual_start_time"])),
                    ("实际完工", _fmt_datetime(row["actual_end_time"])),
                    ("优先级", row["priority_level"]),
                    ("备注", row["remark"]),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "工单下达"))

    operation_by_work_order: dict[int, list[dict[str, Any]]] = {}
    for row in operations:
        operation_by_work_order.setdefault(row["work_order_id"], []).append(dict(row))
        node_id = f"op-{row['work_order_operation_id']}"
        source_id = f"wo-{row['work_order_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="work_order_operation",
                title=f"OP{row['seq_no']:02d} {row['operation_name']}",
                subtitle="关键异常" if row["is_abnormal"] else "工序执行",
                status=row["status"],
                tone="danger" if row["is_abnormal"] else _tone_for_status(row["status"]),
                stage="工序执行",
                level=6,
                summary="工序节点反映计划数、报工数、合格数、报废返工数和异常情况。",
                metrics=[
                    ("计划数", _fmt_qty(row["planned_qty"])),
                    ("已报工", _fmt_qty(row["report_qty"])),
                    ("报废数", _fmt_qty(row["scrap_qty"])),
                ],
                details=[
                    ("工序序号", row["seq_no"]),
                    ("工序名称", row["operation_name"]),
                    ("合格数", _fmt_qty(row["good_qty"])),
                    ("返工数", _fmt_qty(row["rework_qty"])),
                    ("允许报废率", f"{_num(row['allowed_scrap_rate']) * 100:.2f}%"),
                    ("是否异常", "是" if row["is_abnormal"] else "否"),
                    ("备注", row["remark"]),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "工艺路线"))

    for row in reports:
        node_id = f"report-{row['report_id']}"
        source_id = f"op-{row['work_order_operation_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="operation_report",
                title=row["report_no"],
                subtitle=row["employee_name"],
                status="ABNORMAL" if row["is_abnormal"] else "NORMAL",
                tone="danger" if row["is_abnormal"] else "success",
                stage="工人报工",
                level=7,
                summary="一线员工扫码报工记录，直接影响工序进度、节拍、报废返工和成本。",
                metrics=[
                    ("报工数", _fmt_qty(row["report_qty"])),
                    ("合格数", _fmt_qty(row["good_qty"])),
                    ("报废数", _fmt_qty(row["scrap_qty"])),
                ],
                details=[
                    ("报工单号", row["report_no"]),
                    ("员工", row["employee_name"]),
                    ("开始时间", _fmt_datetime(row["start_time"])),
                    ("结束时间", _fmt_datetime(row["end_time"])),
                    ("返工数", _fmt_qty(row["rework_qty"])),
                    ("停机分钟", _fmt_qty(row["downtime_minutes"])),
                    ("额外成本", _fmt_money(row["extra_cost_amount"])),
                    ("异常原因", row["scrap_reason"] or ("节拍/报废异常" if row["is_abnormal"] else "-")),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "扫码报工"))

    for row in scrap_records:
        node_id = f"scrap-{row['scrap_record_id']}"
        source_id = f"report-{row['report_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="scrap_rework",
                title=row["scrap_no"],
                subtitle="返工" if row["disposal_type"] == "REWORK" else "报废",
                status=row["status"],
                tone="warning" if row["disposal_type"] == "REWORK" else "danger",
                stage="报废/返工",
                level=8,
                summary="对报工异常、不良品进行处置，返工进入再加工，报废进入废料成本/收益口径。",
                metrics=[
                    ("处置数", _fmt_qty(row["scrap_qty"])),
                    ("处置重量", _fmt_weight(row["scrap_weight_kg"])),
                    ("成本", _fmt_money(row["scrap_cost_amount"])),
                ],
                details=[
                    ("处置单号", row["scrap_no"]),
                    ("处置方式", _disposal_label(row["disposal_type"])),
                    ("可售废料重量", _fmt_weight(row["saleable_scrap_weight_kg"])),
                    ("处置原因", row["scrap_reason"]),
                    ("状态", _status_label(row["status"])),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "异常处置"))

    for row in completion_items:
        node_id = f"fg-{row['completion_item_id']}"
        source_id = f"wo-{row['work_order_id']}"
        source_material_lot_no = row["source_material_lot_no"] or row["source_material_sub_batch_no"] or "-"
        add_node(
            _node(
                node_id=node_id,
                node_type="completion_receipt",
                title=row["receipt_no"],
                subtitle=row["lot_no"],
                status=row["status"],
                tone=_tone_for_status(row["status"]),
                stage="成品入库",
                level=9,
                summary="工单全部工序完成后形成成品入库批次，后续发货从该批次扣减。",
                metrics=[
                    ("入库数量", _fmt_qty(row["receipt_qty"])),
                    ("入库重量", _fmt_weight(row["receipt_weight_kg"])),
                    ("单位成本", _fmt_money(row["unit_cost"])),
                ],
                details=[
                    ("成品入库单", row["receipt_no"]),
                    ("成品批次", row["lot_no"]),
                    ("来源库存批次号", source_material_lot_no),
                    ("仓库", row["warehouse_name"]),
                    ("入库时间", _fmt_datetime(row["receipt_time"])),
                    ("状态", _status_label(row["status"])),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "完工入库"))

    for row in deliveries:
        node_id = f"delivery-{row['delivery_item_id']}"
        source_id = f"item-{row['sales_order_item_id']}"
        lot_node = next((node for node in nodes if node.type == "completion_receipt" and node.subtitle == row["lot_no"]), None)
        if lot_node:
            source_id = lot_node.id
        add_node(
            _node(
                node_id=node_id,
                node_type="delivery",
                title=row["delivery_no"],
                subtitle=row["lot_no"] or "发货明细",
                status=row["status"],
                tone=_tone_for_status(row["status"]),
                stage="发货",
                level=10,
                summary="成品从库存批次发出，记录发货数量、金额、收货人与送货地址。",
                metrics=[
                    ("发货数量", _fmt_qty(row["delivery_qty"])),
                    ("发货重量", _fmt_weight(row["delivery_weight_kg"])),
                    ("金额", _fmt_money(row["line_amount"])),
                ],
                details=[
                    ("发货单号", row["delivery_no"]),
                    ("库存批次号", row["lot_no"]),
                    ("发货时间", _fmt_datetime(row["delivery_date"])),
                    ("收货人", row["consignee_name"]),
                    ("联系电话", row["consignee_phone"]),
                    ("送货地址", row["delivery_address"]),
                    ("单价", _fmt_money(row["unit_price"])),
                    ("状态", _status_label(row["status"])),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "发货出库"))

    for row in return_items:
        node_id = f"return-{row['return_item_id']}"
        source_id = f"item-{row['sales_order_item_id']}" if row["sales_order_item_id"] else order_node_id
        add_node(
            _node(
                node_id=node_id,
                node_type="return",
                title=row["return_no"],
                subtitle=row["return_reason"] or "退货",
                status=row["disposition_status"],
                tone=_tone_for_status(row["disposition_status"]),
                stage="退货售后",
                level=11,
                summary="客户退货登记，进入待处置状态后可选择返工或报废。",
                metrics=[
                    ("退货数量", _fmt_qty(row["return_qty"])),
                    ("退货重量", _fmt_weight(row["return_weight_kg"])),
                    ("处置状态", _status_label(row["disposition_status"])),
                ],
                details=[
                    ("退货单号", row["return_no"]),
                    ("退货时间", _fmt_datetime(row["return_date"])),
                    ("退货原因", row["return_reason"]),
                    ("责任归属", row["responsibility_type"]),
                    ("退货单状态", _status_label(row["return_status"])),
                    ("备注", row["remark"]),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "退货登记"))

    for row in return_dispositions:
        node_id = f"return-disposition-{row['return_disposition_id']}"
        source_id = f"return-{row['return_item_id']}"
        add_node(
            _node(
                node_id=node_id,
                node_type="return_disposition",
                title=row["disposition_no"],
                subtitle="返工" if row["disposition_type"] == "REWORK" else "报废",
                status=row["status"],
                tone="warning" if row["disposition_type"] == "REWORK" else "danger",
                stage="退货处置",
                level=12,
                summary="退货最终处置结果。返工会产生返工工单和额外人工成本，报废会形成废料销售金额。",
                metrics=[
                    ("报废重量", _fmt_weight(row["scrap_weight_kg"])),
                    ("额外人工", _fmt_money(row["extra_cost_amount"])),
                    ("废料销售", _fmt_money(row["scrap_sale_amount"])),
                ],
                details=[
                    ("处置单号", row["disposition_no"]),
                    ("处置方式", _disposal_label(row["disposition_type"])),
                    ("返工工单", row["rework_work_order_no"]),
                    ("完成时间", _fmt_datetime(row["closed_at"])),
                    ("备注", row["remark"]),
                    ("状态", _status_label(row["status"])),
                ],
                raw=dict(row),
            )
        )
        add_edge(_edge(source_id, node_id, "售后处置"))

    return SalesOrderLifecycleRead(order=order, nodes=nodes, edges=edges)
