from __future__ import annotations

from io import BytesIO
from datetime import date, datetime
from decimal import Decimal

from fastapi import HTTPException
from openpyxl import Workbook, load_workbook
from sqlalchemy import func, select
from sqlalchemy.orm import Session

from app.models.master_data import Item, Warehouse
from app.models.operations import StockLot, Stocktake, StocktakeAdjustment, StocktakeLine, StocktakeWarehouse, WarehouseLocation
from app.services.operations import create_inventory_txn, to_decimal, upsert_stock_balance

ACTIVE_STOCKTAKE_STATUSES = {"LOCKED", "EXPORTED", "IMPORTED"}

STOCKTAKE_EXCEL_HEADERS = [
    "盘库单号",
    "盘库校验码",
    "仓库ID",
    "快照行ID",
    "仓库",
    "库位",
    "物料编码",
    "物料名称",
    "批次号",
    "来源库存批次号",
    "系统数量",
    "系统重量(kg)",
    "系统单价",
    "盘点数量",
    "盘点重量(kg)",
    "盘点备注",
]
STOCKTAKE_QUANTITY_HEADERS = {"系统数量", "盘点数量"}

EMPTY_STOCKTAKE_MARKER_REMARK = "空库盘点校验行，请保留此行用于校验盘库单号"


def build_stocktake_no(db: Session) -> str:
    date_part = date.today().strftime("%Y%m%d")
    prefix = f"PK-{date_part}"
    count = db.scalar(select(func.count(Stocktake.id)).where(Stocktake.stocktake_no.like(f"{prefix}-%"))) or 0
    return f"{prefix}-{count + 1:03d}"


def stocktake_warehouse_type_label(value: str | None) -> str:
    return {
        "RAW": "原材料库",
        "SEMI": "半成品库",
        "FINISHED": "成品库",
        "AUX": "辅料库",
        "SCRAP": "废料库",
        "RETURN": "退货库",
    }.get(str(value or "").upper(), str(value or "未知仓库"))


def stocktake_validation_token(stocktake_no: str, warehouse_id: int, warehouse_type: str | None) -> str:
    return f"{stocktake_no}|{int(warehouse_id)}|{str(warehouse_type or '').upper()}"


def stocktake_weight_only(warehouse_type: str | None, item_type: str | None) -> bool:
    _ = item_type
    return str(warehouse_type or "").upper() in {"RAW", "SCRAP"}


def stocktake_excel_headers_for_type(warehouse_type: str | None) -> list[str]:
    if stocktake_weight_only(warehouse_type, None):
        return [header for header in STOCKTAKE_EXCEL_HEADERS if header not in STOCKTAKE_QUANTITY_HEADERS]
    return list(STOCKTAKE_EXCEL_HEADERS)


def stocktake_excel_row_for_headers(payload: dict[str, object], headers: list[str]) -> list[object]:
    return [payload.get(header, "") for header in headers]


def get_locked_warehouse_ids(db: Session, warehouse_ids: list[int] | set[int]) -> set[int]:
    ids = {int(item) for item in warehouse_ids if item}
    if not ids:
        return set()
    rows = db.scalars(
        select(StocktakeWarehouse.warehouse_id)
        .join(Stocktake, Stocktake.id == StocktakeWarehouse.stocktake_id)
        .where(
            StocktakeWarehouse.warehouse_id.in_(ids),
            StocktakeWarehouse.status == "LOCKED",
            Stocktake.status.in_(ACTIVE_STOCKTAKE_STATUSES),
        )
    ).all()
    return {int(row) for row in rows}


def ensure_warehouses_unlocked(db: Session, warehouse_ids: list[int] | set[int], action_label: str) -> None:
    locked_ids = get_locked_warehouse_ids(db, warehouse_ids)
    if not locked_ids:
        return
    rows = db.scalars(select(Warehouse).where(Warehouse.id.in_(locked_ids)).order_by(Warehouse.id.asc())).all()
    labels = "、".join(row.warehouse_name for row in rows)
    raise HTTPException(status_code=423, detail=f"{labels}正在盘库，暂不能执行{action_label}")


def decimal_value(value: object, places: str = "0.000001") -> Decimal:
    return Decimal(str(value or 0)).quantize(Decimal(places))


def current_time() -> datetime:
    return datetime.now()


def start_stocktake(db: Session, *, warehouse_ids: list[int], user_id: int | None, remark: str | None = None) -> Stocktake:
    ids = [int(item) for item in warehouse_ids if item]
    if not ids:
        raise HTTPException(status_code=400, detail="请选择需要盘库的仓库")
    if len(set(ids)) != 1:
        raise HTTPException(status_code=400, detail="一次只能盘一个仓库，请先在仓库栏选中单个仓库")
    ensure_warehouses_unlocked(db, ids, "开始盘库")
    warehouses = db.scalars(select(Warehouse).where(Warehouse.id.in_(ids)).order_by(Warehouse.id.asc())).all()
    if len(warehouses) != len(set(ids)):
        raise HTTPException(status_code=404, detail="存在无效仓库，无法开始盘库")

    now = current_time()
    stocktake = Stocktake(
        stocktake_no=build_stocktake_no(db),
        status="LOCKED",
        started_by_user_id=user_id,
        started_at=now,
        remark=remark,
    )
    db.add(stocktake)
    db.flush()

    warehouses_by_id = {warehouse.id: warehouse for warehouse in warehouses}
    stocktake_warehouses_by_id: dict[int, StocktakeWarehouse] = {}
    for warehouse in warehouses:
        row = StocktakeWarehouse(
            stocktake_id=stocktake.id,
            warehouse_id=warehouse.id,
            warehouse_type=str(warehouse.warehouse_type or "").upper(),
            warehouse_name=warehouse.warehouse_name,
            status="LOCKED",
        )
        db.add(row)
        db.flush()
        stocktake_warehouses_by_id[warehouse.id] = row

    lot_rows = db.execute(
        select(StockLot, Item)
        .join(Item, Item.id == StockLot.item_id)
        .where(
            StockLot.warehouse_id.in_(ids),
            StockLot.status == "AVAILABLE",
            StockLot.remaining_weight_kg > 0,
        )
        .order_by(StockLot.warehouse_id.asc(), Item.item_code.asc(), StockLot.lot_no.asc(), StockLot.id.asc())
    ).all()

    line_no = 1
    for lot, item in lot_rows:
        warehouse = warehouses_by_id.get(lot.warehouse_id)
        if not warehouse:
            continue
        weight_only = stocktake_weight_only(warehouse.warehouse_type, item.item_type)
        location_name = None
        if lot.location_id:
            location_name = db.scalar(select(WarehouseLocation.location_name).where(WarehouseLocation.id == lot.location_id))
        db.add(
            StocktakeLine(
                stocktake_id=stocktake.id,
                stocktake_warehouse_id=stocktake_warehouses_by_id[warehouse.id].id,
                line_no=line_no,
                warehouse_id=warehouse.id,
                warehouse_name=warehouse.warehouse_name,
                warehouse_type=str(warehouse.warehouse_type or "").upper(),
                location_id=lot.location_id,
                location_name=location_name,
                item_id=item.id,
                item_code=item.item_code,
                item_name=item.item_name,
                item_type=item.item_type,
                lot_id=lot.id,
                lot_no=lot.lot_no,
                source_material_sub_batch_no=lot.source_material_sub_batch_no or lot.material_sub_batch_no,
                snapshot_qty=Decimal("0") if weight_only else to_decimal(lot.remaining_qty),
                snapshot_weight_kg=to_decimal(lot.remaining_weight_kg),
                snapshot_unit_cost=to_decimal(lot.unit_cost, "0.0001"),
                counted_qty=None,
                counted_weight_kg=None,
                diff_qty=Decimal("0"),
                diff_weight_kg=Decimal("0"),
                diff_amount=Decimal("0"),
                diff_type="MATCH",
                row_status="SNAPSHOT",
                remark=None,
            )
        )
        line_no += 1

    db.commit()
    db.refresh(stocktake)
    return stocktake


def _stocktake_lines(db: Session, stocktake_id: int) -> list[StocktakeLine]:
    return db.scalars(
        select(StocktakeLine)
        .where(StocktakeLine.stocktake_id == stocktake_id)
        .order_by(StocktakeLine.warehouse_id.asc(), StocktakeLine.line_no.asc())
    ).all()


def _single_stocktake_warehouse(db: Session, stocktake_id: int) -> StocktakeWarehouse:
    rows = db.scalars(
        select(StocktakeWarehouse)
        .where(StocktakeWarehouse.stocktake_id == stocktake_id)
        .order_by(StocktakeWarehouse.id.asc())
    ).all()
    if len(rows) != 1:
        raise HTTPException(status_code=400, detail="当前盘库单不是单仓库盘库，无法继续处理")
    return rows[0]


def build_stocktake_workbook(db: Session, stocktake_id: int) -> Workbook:
    stocktake = db.get(Stocktake, stocktake_id)
    if not stocktake:
        raise HTTPException(status_code=404, detail="盘库单不存在")
    lines = _stocktake_lines(db, stocktake_id)
    stocktake_warehouses = db.scalars(
        select(StocktakeWarehouse).where(StocktakeWarehouse.stocktake_id == stocktake_id).order_by(StocktakeWarehouse.id.asc())
    ).all()
    workbook = Workbook()
    default_sheet = workbook.active
    workbook.remove(default_sheet)
    grouped: dict[int, list[StocktakeLine]] = {}
    for line in lines:
        grouped.setdefault(int(line.warehouse_id), []).append(line)
    for warehouse in stocktake_warehouses:
        grouped.setdefault(int(warehouse.warehouse_id), [])
    warehouses_by_id = {int(warehouse.warehouse_id): warehouse for warehouse in stocktake_warehouses}
    for warehouse_id, sheet_lines in grouped.items():
        warehouse = warehouses_by_id.get(int(warehouse_id))
        if not warehouse:
            continue
        sheet_name = stocktake_warehouse_type_label(warehouse.warehouse_type)
        worksheet = workbook.create_sheet(title=sheet_name[:31])
        worksheet.freeze_panes = "A2"
        headers = stocktake_excel_headers_for_type(warehouse.warehouse_type)
        worksheet.append(headers)
        if not sheet_lines:
            token = stocktake_validation_token(stocktake.stocktake_no, warehouse.warehouse_id, warehouse.warehouse_type)
            worksheet.append(
                stocktake_excel_row_for_headers(
                    {
                        "盘库单号": stocktake.stocktake_no,
                        "盘库校验码": token,
                        "仓库ID": warehouse.warehouse_id,
                        "快照行ID": "",
                        "仓库": sheet_name,
                        "库位": "",
                        "物料编码": "",
                        "物料名称": "",
                        "批次号": "",
                        "来源库存批次号": "",
                        "系统数量": 0,
                        "系统重量(kg)": 0,
                        "系统单价": 0,
                        "盘点数量": 0,
                        "盘点重量(kg)": 0,
                        "盘点备注": EMPTY_STOCKTAKE_MARKER_REMARK,
                    },
                    headers,
                )
            )
        for line in sheet_lines:
            worksheet.append(
                stocktake_excel_row_for_headers(
                    {
                        "盘库单号": stocktake.stocktake_no,
                        "盘库校验码": stocktake_validation_token(stocktake.stocktake_no, line.warehouse_id, line.warehouse_type),
                        "仓库ID": line.warehouse_id,
                        "快照行ID": line.id,
                        "仓库": line.warehouse_name,
                        "库位": line.location_name or "",
                        "物料编码": line.item_code,
                        "物料名称": line.item_name,
                        "批次号": line.lot_no or "",
                        "来源库存批次号": line.source_material_sub_batch_no or "",
                        "系统数量": float(line.snapshot_qty or 0),
                        "系统重量(kg)": float(line.snapshot_weight_kg or 0),
                        "系统单价": float(line.snapshot_unit_cost or 0),
                        "盘点数量": float(line.snapshot_qty or 0),
                        "盘点重量(kg)": float(line.snapshot_weight_kg or 0),
                        "盘点备注": "",
                    },
                    headers,
                )
            )
        widths_by_header = {
            "盘库单号": 18,
            "盘库校验码": 30,
            "仓库ID": 10,
            "快照行ID": 12,
            "仓库": 14,
            "库位": 14,
            "物料编码": 18,
            "物料名称": 28,
            "批次号": 22,
            "来源库存批次号": 24,
            "系统数量": 14,
            "系统重量(kg)": 16,
            "系统单价": 12,
            "盘点数量": 14,
            "盘点重量(kg)": 16,
            "盘点备注": 24,
        }
        for index, header in enumerate(headers, start=1):
            width = widths_by_header.get(header, 14)
            worksheet.column_dimensions[worksheet.cell(row=1, column=index).column_letter].width = width
    stocktake.exported_at = current_time()
    stocktake.status = "EXPORTED"
    db.add(stocktake)
    db.commit()
    return workbook


def _clean_text(value: object) -> str:
    return str(value or "").strip()


def _read_workbook_rows(content: bytes, warehouse_type: str | None = None) -> list[dict[str, object]]:
    try:
        workbook = load_workbook(BytesIO(content), read_only=True, data_only=True)
    except Exception as exc:
        raise ValueError("Excel 文件读取失败，请确认上传的是盘库清单 xlsx 文件") from exc
    rows: list[dict[str, object]] = []
    for worksheet in workbook.worksheets:
        values = list(worksheet.iter_rows(values_only=True))
        if not values:
            continue
        headers = [_clean_text(item) for item in values[0]]
        required_headers = stocktake_excel_headers_for_type(warehouse_type)
        missing = [header for header in required_headers if header not in headers]
        if missing:
            raise ValueError(f"{worksheet.title} 缺少必要列：{'、'.join(missing)}")
        for row in values[1:]:
            if not any(_clean_text(item) for item in row):
                continue
            payload = {headers[index]: value for index, value in enumerate(row) if index < len(headers)}
            payload["_sheet_name"] = worksheet.title
            rows.append(payload)
    return rows


def _is_empty_stocktake_marker_row(row: dict[str, object]) -> bool:
    remark = _clean_text(row.get("盘点备注"))
    if "空库" not in remark:
        return False
    if _clean_text(row.get("快照行ID")):
        return False
    if any(_clean_text(row.get(label)) for label in ["物料编码", "物料名称", "批次号", "来源库存批次号"]):
        return False
    return all(
        decimal_value(row.get(label)) == 0
        for label in ["系统数量", "系统重量(kg)", "系统单价", "盘点数量", "盘点重量(kg)"]
    )


def _validate_stocktake_row_marker(row: dict[str, object], stocktake: Stocktake, warehouse: StocktakeWarehouse) -> None:
    row_stocktake_no = _clean_text(row.get("盘库单号"))
    if row_stocktake_no != stocktake.stocktake_no:
        raise ValueError(f"存在不属于当前盘库单的行：{row_stocktake_no or '空盘库单号'}")

    row_warehouse_id = _clean_text(row.get("仓库ID"))
    if row_warehouse_id and int(float(row_warehouse_id)) != int(warehouse.warehouse_id):
        raise ValueError("导入清单不属于当前仓库，请确认仓库栏选中项和盘库清单一致")

    row_warehouse_name = _clean_text(row.get("仓库"))
    if row_warehouse_name and row_warehouse_name != warehouse.warehouse_name:
        raise ValueError("导入清单不属于当前仓库，请确认仓库栏选中项和盘库清单一致")

    expected_token = stocktake_validation_token(stocktake.stocktake_no, warehouse.warehouse_id, warehouse.warehouse_type)
    row_token = _clean_text(row.get("盘库校验码"))
    if row_token != expected_token:
        raise ValueError("盘库校验码不匹配，请导入本次盘库导出的仓库清单")


def _reset_stocktake_preview_for_reimport(db: Session, stocktake_id: int, warehouse_id: int) -> dict[int, StocktakeLine]:
    lines = db.scalars(
        select(StocktakeLine)
        .where(StocktakeLine.stocktake_id == stocktake_id, StocktakeLine.warehouse_id == warehouse_id)
        .order_by(StocktakeLine.line_no.asc())
    ).all()
    existing_lines: dict[int, StocktakeLine] = {}
    for line in lines:
        if line.lot_id is None and line.diff_type == "NEW_LOT":
            db.delete(line)
            continue
        line.counted_qty = None
        line.counted_weight_kg = None
        line.diff_qty = Decimal("0")
        line.diff_weight_kg = Decimal("0")
        line.diff_amount = Decimal("0")
        line.diff_type = "MATCH"
        line.row_status = "SNAPSHOT"
        line.error_message = None
        line.remark = None
        db.add(line)
        existing_lines[line.id] = line
    db.flush()
    return existing_lines


def _line_diff_type(qty_diff: Decimal, weight_diff: Decimal, *, is_new: bool = False, is_missing: bool = False) -> str:
    if is_new:
        return "NEW_LOT"
    if is_missing:
        return "MISSING"
    basis = weight_diff if weight_diff != 0 else qty_diff
    if basis > 0:
        return "GAIN"
    if basis < 0:
        return "LOSS"
    return "MATCH"


def _stocktake_summary(lines: list[StocktakeLine]) -> dict[str, int | float]:
    summary = {
        "match_count": 0,
        "gain_count": 0,
        "loss_count": 0,
        "new_lot_count": 0,
        "missing_count": 0,
        "error_count": 0,
        "diff_amount": 0.0,
    }
    for line in lines:
        key = {
            "MATCH": "match_count",
            "GAIN": "gain_count",
            "LOSS": "loss_count",
            "NEW_LOT": "new_lot_count",
            "MISSING": "missing_count",
        }.get(line.diff_type)
        if key:
            summary[key] += 1
        if line.row_status == "ERROR":
            summary["error_count"] += 1
        summary["diff_amount"] += float(line.diff_amount or 0)
    return summary


def import_stocktake_workbook(db: Session, stocktake_id: int, content: bytes, user_id: int | None) -> dict[str, object]:
    stocktake = db.get(Stocktake, stocktake_id)
    if not stocktake:
        raise HTTPException(status_code=404, detail="盘库单不存在")
    if stocktake.status not in {"LOCKED", "EXPORTED", "IMPORTED"}:
        raise HTTPException(status_code=400, detail="当前盘库单状态不允许导入")
    stocktake_warehouse = _single_stocktake_warehouse(db, stocktake_id)

    rows = _read_workbook_rows(content, stocktake_warehouse.warehouse_type)

    existing_lines = _reset_stocktake_preview_for_reimport(db, stocktake_id, stocktake_warehouse.warehouse_id)
    seen_line_ids: set[int] = set()
    next_line_no = max((line.line_no for line in existing_lines.values()), default=0) + 1

    for row in rows:
        _validate_stocktake_row_marker(row, stocktake, stocktake_warehouse)
        if _is_empty_stocktake_marker_row(row):
            continue
        line_id_text = _clean_text(row.get("快照行ID"))
        counted_qty = decimal_value(row.get("盘点数量"))
        counted_weight = decimal_value(row.get("盘点重量(kg)"))
        remark = _clean_text(row.get("盘点备注")) or None
        if line_id_text:
            line_id = int(float(line_id_text))
            line = existing_lines.get(line_id)
            if not line:
                raise ValueError(f"快照行ID {line_id} 不存在于当前盘库单")
            line.error_message = None
            if line_id in seen_line_ids:
                line.error_message = "导入清单中该快照行重复"
            seen_line_ids.add(line_id)
            snapshot_qty = decimal_value(line.snapshot_qty)
            snapshot_weight = decimal_value(line.snapshot_weight_kg)
            weight_only = stocktake_weight_only(line.warehouse_type, line.item_type)
            qty_diff = Decimal("0") if weight_only else counted_qty - snapshot_qty
            weight_diff = counted_weight - snapshot_weight
            line.counted_qty = Decimal("0") if weight_only else counted_qty
            line.counted_weight_kg = counted_weight
            line.diff_qty = qty_diff
            line.diff_weight_kg = weight_diff
            line.diff_amount = (weight_diff * decimal_value(line.snapshot_unit_cost, "0.0001")).quantize(Decimal("0.01"))
            line.diff_type = _line_diff_type(qty_diff, weight_diff)
            line.row_status = "ERROR" if line.error_message else "READY"
            line.remark = remark
            db.add(line)
            continue

        item_code = _clean_text(row.get("物料编码"))
        item = db.scalar(select(Item).where(Item.item_code == item_code).limit(1))
        warehouse = db.get(Warehouse, stocktake_warehouse.warehouse_id)
        if not item or not warehouse:
            raise ValueError(f"新增批次行物料或仓库不存在：{stocktake_warehouse.warehouse_name} / {item_code}")
        unit_cost = decimal_value(row.get("系统单价"), "0.0001")
        weight_only = stocktake_weight_only(warehouse.warehouse_type, item.item_type)
        db.add(
            StocktakeLine(
                stocktake_id=stocktake.id,
                stocktake_warehouse_id=stocktake_warehouse.id,
                line_no=next_line_no,
                warehouse_id=warehouse.id,
                warehouse_name=warehouse.warehouse_name,
                warehouse_type=str(warehouse.warehouse_type or "").upper(),
                location_id=None,
                location_name=_clean_text(row.get("库位")) or None,
                item_id=item.id,
                item_code=item.item_code,
                item_name=item.item_name,
                item_type=item.item_type,
                lot_id=None,
                lot_no=_clean_text(row.get("批次号")) or None,
                source_material_sub_batch_no=_clean_text(row.get("来源库存批次号")) or None,
                snapshot_qty=Decimal("0"),
                snapshot_weight_kg=Decimal("0"),
                snapshot_unit_cost=unit_cost,
                counted_qty=Decimal("0") if weight_only else counted_qty,
                counted_weight_kg=counted_weight,
                diff_qty=Decimal("0") if weight_only else counted_qty,
                diff_weight_kg=counted_weight,
                diff_amount=(counted_weight * unit_cost).quantize(Decimal("0.01")),
                diff_type="NEW_LOT",
                row_status="READY",
                remark=remark,
            )
        )
        next_line_no += 1

    for line_id, line in existing_lines.items():
        if line_id in seen_line_ids:
            continue
        line.counted_qty = Decimal("0")
        line.counted_weight_kg = Decimal("0")
        line.diff_qty = -decimal_value(line.snapshot_qty)
        line.diff_weight_kg = -decimal_value(line.snapshot_weight_kg)
        line.diff_amount = (decimal_value(line.diff_weight_kg) * decimal_value(line.snapshot_unit_cost, "0.0001")).quantize(Decimal("0.01"))
        line.diff_type = "MISSING"
        line.row_status = "READY"
        line.error_message = None
        line.remark = "导入清单缺失，按盘点为0处理"
        db.add(line)

    stocktake.status = "IMPORTED"
    stocktake.imported_by_user_id = user_id
    stocktake.imported_at = current_time()
    db.add(stocktake)
    db.commit()

    lines = db.scalars(
        select(StocktakeLine)
        .where(StocktakeLine.stocktake_id == stocktake_id, StocktakeLine.warehouse_id == stocktake_warehouse.warehouse_id)
        .order_by(StocktakeLine.line_no.asc())
    ).all()
    return {"stocktake": stocktake, "lines": lines, "summary": _stocktake_summary(lines)}


def _stocktake_txn_type(diff_type: str) -> str:
    return {
        "GAIN": "STOCKTAKE_GAIN",
        "LOSS": "STOCKTAKE_LOSS",
        "MISSING": "STOCKTAKE_LOSS",
        "NEW_LOT": "STOCKTAKE_NEW_LOT",
    }.get(diff_type, "STOCKTAKE_GAIN")


def confirm_stocktake(
    db: Session,
    stocktake_id: int,
    *,
    user_id: int | None,
    confirm_text: str | None = None,
    remark: str | None = None,
) -> Stocktake:
    _ = confirm_text
    stocktake = db.get(Stocktake, stocktake_id)
    if not stocktake:
        raise HTTPException(status_code=404, detail="盘库单不存在")
    lines = db.scalars(select(StocktakeLine).where(StocktakeLine.stocktake_id == stocktake_id).order_by(StocktakeLine.line_no.asc())).all()
    now = current_time()
    if stocktake.status != "IMPORTED":
        if stocktake.status in {"LOCKED", "EXPORTED"} and not lines:
            stocktake.status = "IMPORTED"
            stocktake.imported_by_user_id = user_id
            stocktake.imported_at = now
            db.add(stocktake)
            db.flush()
        else:
            raise HTTPException(status_code=400, detail="请先导入盘点清单并确认差异后再过账")
    error_lines = [line for line in lines if line.row_status == "ERROR"]
    if error_lines:
        raise HTTPException(status_code=400, detail=f"存在 {len(error_lines)} 行异常，不能确认盘库")

    for line in lines:
        if line.diff_type == "MATCH":
            continue
        weight_only = stocktake_weight_only(line.warehouse_type, line.item_type)
        unit_cost = decimal_value(line.snapshot_unit_cost, "0.0001")
        qty_change = decimal_value(line.diff_qty)
        weight_change = decimal_value(line.diff_weight_kg)
        if line.diff_type == "NEW_LOT":
            counted_qty = Decimal("0") if weight_only else max(Decimal("0"), decimal_value(line.counted_qty))
            counted_weight = max(Decimal("0"), decimal_value(line.counted_weight_kg))
            lot = StockLot(
                lot_no=line.lot_no or f"{stocktake.stocktake_no}-NEW-{line.line_no:04d}",
                lot_role="STOCKTAKE_NEW",
                item_id=line.item_id,
                warehouse_id=line.warehouse_id,
                location_id=line.location_id,
                source_doc_type="STOCKTAKE",
                source_doc_id=stocktake.id,
                source_line_id=line.id,
                source_material_sub_batch_no=line.source_material_sub_batch_no,
                source_material_summary="盘库新增批次",
                inbound_qty=counted_qty,
                inbound_weight_kg=counted_weight,
                remaining_qty=counted_qty,
                remaining_weight_kg=counted_weight,
                locked_qty=Decimal("0"),
                locked_weight_kg=Decimal("0"),
                unit_cost=unit_cost,
                production_date=now.date(),
                quality_status="PASS",
                status="AVAILABLE",
                remark=line.remark or "盘库新增",
            )
            db.add(lot)
            db.flush()
            lot_id = lot.id
            before_qty = Decimal("0")
            before_weight = Decimal("0")
            after_qty = counted_qty
            after_weight = counted_weight
            qty_change = after_qty
            weight_change = after_weight
        else:
            lot = db.get(StockLot, line.lot_id)
            if not lot:
                raise HTTPException(status_code=400, detail=f"盘库行 {line.line_no} 对应批次不存在")
            before_qty = Decimal("0") if weight_only else decimal_value(lot.remaining_qty)
            before_weight = decimal_value(lot.remaining_weight_kg)
            after_qty = Decimal("0") if weight_only else max(Decimal("0"), decimal_value(line.counted_qty))
            after_weight = max(Decimal("0"), decimal_value(line.counted_weight_kg))
            qty_change = after_qty - before_qty
            weight_change = after_weight - before_weight
            lot.remaining_qty = after_qty
            lot.remaining_weight_kg = after_weight
            lot.status = "AVAILABLE" if after_qty > 0 or after_weight > 0 else "DEPLETED"
            db.add(lot)
            lot_id = lot.id

        upsert_stock_balance(
            db,
            item_id=line.item_id,
            warehouse_id=line.warehouse_id,
            location_id=line.location_id,
            qty_delta=qty_change,
            weight_delta=weight_change,
            available_qty_delta=qty_change,
            available_weight_delta=weight_change,
            unit_cost=unit_cost,
        )
        txn = create_inventory_txn(
            db,
            txn_type=_stocktake_txn_type(line.diff_type),
            item_id=line.item_id,
            warehouse_id=line.warehouse_id,
            location_id=line.location_id,
            lot_id=lot_id,
            qty_change=qty_change,
            weight_change=weight_change,
            unit_cost=unit_cost,
            source_doc_type="STOCKTAKE",
            source_doc_id=stocktake.id,
            source_line_id=line.id,
            biz_time=now,
            operator_user_id=user_id,
            remark=line.remark or remark or "盘库调整",
            amount_basis="WEIGHT",
        )
        db.flush()
        db.add(
            StocktakeAdjustment(
                stocktake_id=stocktake.id,
                stocktake_line_id=line.id,
                warehouse_id=line.warehouse_id,
                item_id=line.item_id,
                lot_id=line.lot_id,
                adjustment_lot_id=lot_id,
                inventory_txn_id=txn.id,
                adjustment_type=line.diff_type,
                before_qty=before_qty,
                after_qty=after_qty,
                before_weight_kg=before_weight,
                after_weight_kg=after_weight,
                qty_change=qty_change,
                weight_change_kg=weight_change,
                unit_cost=unit_cost,
                amount=(abs(weight_change) * unit_cost).quantize(Decimal("0.01")),
            )
        )

    for warehouse in db.scalars(select(StocktakeWarehouse).where(StocktakeWarehouse.stocktake_id == stocktake_id)).all():
        warehouse.status = "UNLOCKED"
        db.add(warehouse)
    stocktake.status = "CONFIRMED"
    stocktake.confirmed_by_user_id = user_id
    stocktake.confirmed_at = now
    if remark:
        stocktake.remark = remark
    db.add(stocktake)
    db.commit()
    db.refresh(stocktake)
    return stocktake


def cancel_stocktake(db: Session, stocktake_id: int, *, user_id: int | None, remark: str | None = None) -> Stocktake:
    stocktake = db.get(Stocktake, stocktake_id)
    if not stocktake:
        raise HTTPException(status_code=404, detail="盘库单不存在")
    if stocktake.status not in ACTIVE_STOCKTAKE_STATUSES:
        raise HTTPException(status_code=400, detail="当前盘库单不能取消")
    now = current_time()
    for warehouse in db.scalars(select(StocktakeWarehouse).where(StocktakeWarehouse.stocktake_id == stocktake_id)).all():
        warehouse.status = "UNLOCKED"
        db.add(warehouse)
    stocktake.status = "CANCELED"
    stocktake.canceled_by_user_id = user_id
    stocktake.canceled_at = now
    stocktake.remark = remark or stocktake.remark
    db.add(stocktake)
    db.commit()
    db.refresh(stocktake)
    return stocktake


def stocktake_lines_for_read(db: Session, stocktake_id: int) -> list[StocktakeLine]:
    return db.scalars(
        select(StocktakeLine)
        .where(StocktakeLine.stocktake_id == stocktake_id)
        .order_by(StocktakeLine.warehouse_id.asc(), StocktakeLine.line_no.asc())
    ).all()


def stocktake_warehouses_for_read(db: Session, stocktake_id: int) -> list[StocktakeWarehouse]:
    return db.scalars(
        select(StocktakeWarehouse)
        .where(StocktakeWarehouse.stocktake_id == stocktake_id)
        .order_by(StocktakeWarehouse.id.asc())
    ).all()
