from __future__ import annotations

from datetime import datetime
from decimal import Decimal, ROUND_FLOOR
from uuid import uuid4

from fastapi import HTTPException
from sqlalchemy import func, select
from sqlalchemy.orm import Session

from app.models.master_data import BomItem, Item, Warehouse
from app.models.operations import (
    CompletionReceipt,
    CompletionReceiptItem,
    InventoryTxn,
    StockLot,
    WorkOrder,
    WorkOrderMaterial,
    WorkOrderMaterialIssue,
)
from app.schemas.operations import (
    ProductionWorkOrderInboundCreate,
    ProductionWorkOrderInboundPreviewRead,
    ProductionWorkOrderInboundRead,
)
from app.services.document_archives import DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT, generate_document_archive
from app.services.operations import create_inventory_txn, to_decimal, upsert_stock_balance
from app.services.production_work_order_ledger import get_work_order_limit_snapshot
from app.services.sales_planning import code_token, next_doc_no
from app.services.stocktake import ensure_warehouses_unlocked


DEVIATION_RATE = Decimal("0.15")
FINISHED_QTY_UPPER_MULTIPLIER = Decimal("1.15")


def _production_document_batch_no(now: datetime) -> str:
    return f"生产结算{now.strftime('%Y%m%d%H%M%S')}{uuid4().int % 1_000_000:06d}"


def _with_document_batch_no(remark: str, batch_no: str | None) -> str:
    if not batch_no:
        return remark
    return f"{remark}；单据批次号：{batch_no}"


def _round_decimal(value: Decimal, places: str = "0.001") -> Decimal:
    return to_decimal(value, places)


def _non_negative(value: Decimal) -> Decimal:
    return max(to_decimal(value), Decimal("0"))


def _deviation_exceeds_limit(actual: Decimal, theoretical: Decimal) -> bool:
    actual = to_decimal(actual)
    theoretical = to_decimal(theoretical)
    if theoretical == 0:
        return actual != 0
    return abs(actual - theoretical) / abs(theoretical) > DEVIATION_RATE


def _floor_qty(value: Decimal) -> Decimal:
    return to_decimal(value).to_integral_value(rounding=ROUND_FLOOR)


def _reference_finished_qty(issued_weight: Decimal, gross_weight: Decimal) -> Decimal:
    issued_weight = to_decimal(issued_weight)
    gross_weight = to_decimal(gross_weight)
    if issued_weight <= 0 or gross_weight <= 0:
        return Decimal("0")
    return _floor_qty(issued_weight / gross_weight)


def _finished_qty_exceeds_reference_limit(total_finished_qty: Decimal, reference_finished_qty: Decimal) -> bool:
    total_finished_qty = to_decimal(total_finished_qty)
    reference_finished_qty = to_decimal(reference_finished_qty)
    if reference_finished_qty <= 0:
        return total_finished_qty > 0
    return total_finished_qty > reference_finished_qty * FINISHED_QTY_UPPER_MULTIPLIER


def _remark_with_deviation(base: str, deviation_remark: str | None) -> str:
    deviation = str(deviation_remark or "").strip()
    if not deviation:
        return base
    return f"{base}；偏差说明：{deviation}"


def _source_issue(db: Session, work_order_id: int) -> WorkOrderMaterialIssue:
    issues = db.scalars(
        select(WorkOrderMaterialIssue)
        .where(WorkOrderMaterialIssue.work_order_id == work_order_id)
        .order_by(WorkOrderMaterialIssue.id)
    ).all()
    if not issues:
        raise HTTPException(status_code=400, detail="该工单没有材料库存批次领用记录，不能进行生产工单入库")
    source_lot_ids = {int(issue.source_lot_id) for issue in issues if issue.source_lot_id is not None}
    if not source_lot_ids:
        raise HTTPException(status_code=400, detail="该工单没有材料来源库存批次，不能进行生产工单入库")
    if len(source_lot_ids) > 1:
        raise HTTPException(status_code=400, detail="该工单存在多个材料来源库存批次，不能进行生产工单入库")
    source_lot_id = next(iter(source_lot_ids))
    return next(issue for issue in issues if issue.source_lot_id is not None and int(issue.source_lot_id) == source_lot_id)


def _get_work_order(db: Session, work_order_id: int) -> WorkOrder:
    work_order = db.get(WorkOrder, work_order_id)
    if not work_order:
        raise HTTPException(status_code=404, detail="生产工单不存在")
    if str(work_order.status or "").upper() == "SETTLED":
        raise HTTPException(status_code=400, detail="该生产工单已结单，不能进行生产工单入库")
    return work_order


def _get_warehouse(db: Session, warehouse_type: str) -> Warehouse:
    warehouse = db.scalar(
        select(Warehouse)
        .where(
            Warehouse.warehouse_type == warehouse_type,
            Warehouse.status == "ACTIVE",
        )
        .order_by(Warehouse.id)
        .limit(1)
    )
    if not warehouse:
        warehouse_type_name = {"FINISHED": "成品", "SCRAP": "废料"}.get(warehouse_type, "目标")
        raise HTTPException(status_code=400, detail=f"缺少启用的{warehouse_type_name}仓库，不能进行生产工单入库")
    return warehouse


def _get_bom_item(db: Session, work_order: WorkOrder, material_item_id: int) -> BomItem:
    bom_item = db.scalar(
        select(BomItem)
        .where(
            BomItem.bom_id == work_order.bom_id,
            BomItem.material_item_id == material_item_id,
        )
        .order_by(BomItem.seq_no, BomItem.id)
        .limit(1)
    )
    if not bom_item:
        raise HTTPException(status_code=400, detail="工单需用清单缺少领用材料，不能进行生产工单入库")
    return bom_item


def _build_receipt_no(db: Session, work_order: WorkOrder, now: datetime) -> str:
    work_order_part = code_token(work_order.work_order_no, f"WO{work_order.id:04d}", 18)
    return next_doc_no(db, CompletionReceipt, "receipt_no", f"WOI-NB-{now.strftime('%Y%m%d')}-{work_order_part}")


def _build_lot_no(db: Session, prefix: str, work_order: WorkOrder, item: Item, now: datetime) -> str:
    item_part = code_token(item.item_code or item.item_name, f"ITEM{item.id:04d}", 12)
    work_order_part = code_token(work_order.work_order_no, f"WO{work_order.id:04d}", 10)
    return next_doc_no(db, StockLot, "lot_no", f"{prefix}-NB-{now.strftime('%Y%m%d')}-{item_part}-{work_order_part}")


def _append_settle_remark(work_order: WorkOrder, deviation_remark: str | None, remark: str | None) -> None:
    parts = [part.strip() for part in str(work_order.remark or "").replace(";", "；").split("；") if part.strip()]
    parts.append("生产工单入库已结单")
    if str(deviation_remark or "").strip():
        parts.append(f"偏差说明：{str(deviation_remark).strip()}")
    if str(remark or "").strip():
        parts.append(f"入库说明：{str(remark).strip()}")
    work_order.remark = "；".join(dict.fromkeys(parts))[-255:]


def preview_production_work_order_inbound(
    db: Session,
    *,
    work_order_id: int,
    finished_qty: float | Decimal | None = None,
) -> ProductionWorkOrderInboundPreviewRead:
    work_order = _get_work_order(db, work_order_id)
    issue = _source_issue(db, work_order_id)
    source_lot = db.get(StockLot, issue.source_lot_id)
    product = db.get(Item, work_order.product_item_id)
    material = db.get(Item, issue.material_item_id)
    if not source_lot:
        raise HTTPException(status_code=400, detail="来源库存批次不存在，不能进行生产工单入库")
    if not product:
        raise HTTPException(status_code=400, detail="工单产品不存在，不能进行生产工单入库")
    if not material:
        raise HTTPException(status_code=400, detail="领用材料不存在，不能进行生产工单入库")

    snapshot = get_work_order_limit_snapshot(db, work_order_id)
    bom_item = _get_bom_item(db, work_order, issue.material_item_id)
    gross = to_decimal(bom_item.gross_weight_per_piece_kg or snapshot.gross_weight_per_piece_kg)
    net = to_decimal(bom_item.net_weight_per_piece_kg or snapshot.net_weight_per_piece_kg)
    if gross <= 0:
        gross = to_decimal(snapshot.gross_weight_per_piece_kg)
    if net <= 0:
        net = to_decimal(snapshot.net_weight_per_piece_kg)
    if gross <= 0:
        raise HTTPException(status_code=400, detail="产品需用清单缺少单件毛重，不能进行生产工单入库")
    if gross < net:
        net = gross

    reference_finished_qty = _reference_finished_qty(to_decimal(snapshot.issued_weight_kg), gross)
    finished_received_qty = to_decimal(snapshot.finished_received_qty)
    remaining_reference_finished_qty = _non_negative(reference_finished_qty - finished_received_qty)
    selected_finished_qty = to_decimal(finished_qty) if finished_qty is not None else remaining_reference_finished_qty
    if selected_finished_qty < 0:
        raise HTTPException(status_code=400, detail="成品入库数量不能小于0")
    total_finished_qty = finished_received_qty + selected_finished_qty
    theoretical_surplus = _non_negative(
        to_decimal(snapshot.issued_weight_kg) - to_decimal(snapshot.returned_material_weight_kg) - total_finished_qty * gross
    )
    theoretical_scrap = _non_negative(to_decimal(snapshot.max_scrap_inbound_weight_kg))

    return ProductionWorkOrderInboundPreviewRead(
        work_order_id=work_order.id,
        work_order_no=work_order.work_order_no,
        product_item_id=product.id,
        product_code=product.item_code,
        product_name=product.item_name,
        source_lot_id=source_lot.id,
        source_lot_no=source_lot.lot_no,
        material_item_id=material.id,
        material_code=material.item_code,
        material_name=material.item_name,
        gross_weight_per_piece_kg=float(_round_decimal(gross)),
        net_weight_per_piece_kg=float(_round_decimal(net)),
        issued_weight_kg=float(_round_decimal(to_decimal(snapshot.issued_weight_kg))),
        returned_weight_kg=float(_round_decimal(to_decimal(snapshot.returned_material_weight_kg))),
        final_operation_good_qty=float(_round_decimal(to_decimal(snapshot.final_operation_good_qty))),
        finished_inbound_qty=float(_round_decimal(to_decimal(snapshot.finished_received_qty))),
        reported_scrap_qty=float(_round_decimal(to_decimal(snapshot.total_operation_scrap_qty))),
        scrap_inbound_weight_kg=float(_round_decimal(to_decimal(snapshot.scrap_inbound_weight_kg))),
        reference_finished_qty=float(_round_decimal(reference_finished_qty)),
        remaining_reference_finished_qty=float(_round_decimal(remaining_reference_finished_qty)),
        finished_qty_upper_limit=float(_round_decimal(reference_finished_qty * FINISHED_QTY_UPPER_MULTIPLIER)),
        finished_qty=float(_round_decimal(selected_finished_qty)),
        theoretical_surplus_weight_kg=float(_round_decimal(theoretical_surplus)),
        theoretical_scrap_weight_kg=float(_round_decimal(theoretical_scrap)),
    )


def create_production_work_order_inbound(
    db: Session,
    *,
    payload: ProductionWorkOrderInboundCreate,
    operator_user_id: int | None,
) -> ProductionWorkOrderInboundRead:
    work_order = _get_work_order(db, payload.work_order_id)
    issue = _source_issue(db, payload.work_order_id)
    source_lot = db.get(StockLot, issue.source_lot_id)
    product = db.get(Item, work_order.product_item_id)
    material = db.get(Item, issue.material_item_id)
    if not source_lot or not product or not material:
        raise HTTPException(status_code=400, detail="工单入库来源数据不完整")

    finished_qty = to_decimal(payload.finished_qty)
    surplus_weight = to_decimal(payload.surplus_weight_kg)
    scrap_weight = to_decimal(payload.scrap_weight_kg)
    if min(finished_qty, surplus_weight, scrap_weight) < 0:
        raise HTTPException(status_code=400, detail="入库数量或重量不能小于0")
    if finished_qty <= 0 and surplus_weight <= 0 and scrap_weight <= 0:
        raise HTTPException(status_code=400, detail="生产工单入库至少需要填写成品、余料或废料中的一项")

    preview = preview_production_work_order_inbound(db, work_order_id=payload.work_order_id, finished_qty=finished_qty)
    theoretical_surplus = to_decimal(preview.theoretical_surplus_weight_kg)
    theoretical_scrap = to_decimal(preview.theoretical_scrap_weight_kg)
    total_finished_qty = to_decimal(preview.finished_inbound_qty) + finished_qty
    reference_finished_qty = to_decimal(preview.reference_finished_qty)
    if _finished_qty_exceeds_reference_limit(total_finished_qty, reference_finished_qty) and not str(payload.deviation_remark or "").strip():
        raise HTTPException(status_code=400, detail="成品入库数量超过本工单参考生产数量上限15%，请填写偏差说明")
    if (
        _deviation_exceeds_limit(surplus_weight, theoretical_surplus)
        or _deviation_exceeds_limit(scrap_weight, theoretical_scrap)
    ) and not str(payload.deviation_remark or "").strip():
        raise HTTPException(status_code=400, detail="余料或废料入库重量与理论值偏差超过上下15%，请填写偏差说明")
    if payload.settle_work_order and not payload.settle_confirmed:
        raise HTTPException(status_code=400, detail="结单前必须确认本次入库明细及结单影响")

    now = datetime.now()
    bom_item = _get_bom_item(db, work_order, issue.material_item_id)
    gross = to_decimal(bom_item.gross_weight_per_piece_kg or preview.gross_weight_per_piece_kg)
    product_weight = to_decimal(product.unit_weight_kg or bom_item.net_weight_per_piece_kg or preview.net_weight_per_piece_kg)
    finished_warehouse = _get_warehouse(db, "FINISHED") if finished_qty > 0 else None
    scrap_warehouse = _get_warehouse(db, "SCRAP") if scrap_weight > 0 else None
    material_row = db.get(WorkOrderMaterial, issue.work_order_material_id)
    if surplus_weight > 0 and not material_row:
        raise HTTPException(status_code=400, detail="工单领料明细不存在，不能进行余料入库")
    locked_check_warehouse_ids: set[int] = set()
    if finished_qty > 0 and finished_warehouse:
        locked_check_warehouse_ids.add(int(finished_warehouse.id))
    if surplus_weight > 0:
        locked_check_warehouse_ids.add(int(source_lot.warehouse_id))
    if scrap_weight > 0 and scrap_warehouse:
        locked_check_warehouse_ids.add(int(scrap_warehouse.id))
    ensure_warehouses_unlocked(db, locked_check_warehouse_ids, "生产工单入库")

    finished_lot_id = surplus_lot_id = scrap_lot_id = None
    finished_txn_id = surplus_txn_id = scrap_txn_id = None
    document_batch_no = _production_document_batch_no(now)

    if finished_qty > 0 and finished_warehouse:
        receipt = CompletionReceipt(
            receipt_no=_build_receipt_no(db, work_order, now),
            work_order_id=work_order.id,
            warehouse_id=finished_warehouse.id,
            receipt_time=now,
            receiver_employee_id=None,
            status="POSTED",
            remark=_remark_with_deviation(payload.remark or "生产工单入库", payload.deviation_remark),
        )
        db.add(receipt)
        db.flush()
        lot_no = _build_lot_no(db, "FGI", work_order, product, now)
        receipt_weight = finished_qty * product_weight
        unit_cost = to_decimal(to_decimal(source_lot.unit_cost) * gross, "0.0001")
        material_summary = f"{material.item_name}：{source_lot.lot_no}"
        receipt_item = CompletionReceiptItem(
            completion_receipt_id=receipt.id,
            line_no=1,
            product_item_id=product.id,
            location_id=None,
            lot_no=lot_no,
            source_material_lot_id=source_lot.id,
            source_material_sub_batch_no=issue.material_sub_batch_no,
            source_material_summary=material_summary,
            receipt_qty=finished_qty,
            receipt_weight_kg=receipt_weight,
            unit_cost=unit_cost,
            status="POSTED",
            remark=_remark_with_deviation(payload.remark or "生产工单入库", payload.deviation_remark),
        )
        db.add(receipt_item)
        db.flush()
        finished_lot = StockLot(
            lot_no=lot_no,
            parent_lot_id=source_lot.id,
            lot_role="FINISHED_FROM_RAW_BATCH",
            material_sub_batch_no=None,
            item_id=product.id,
            warehouse_id=finished_warehouse.id,
            location_id=None,
            source_doc_type="FG_RECEIPT",
            source_doc_id=receipt.id,
            source_line_id=receipt_item.id,
            source_material_lot_id=source_lot.id,
            source_material_sub_batch_no=issue.material_sub_batch_no,
            source_material_summary=material_summary,
            inbound_qty=finished_qty,
            inbound_weight_kg=receipt_weight,
            remaining_qty=finished_qty,
            remaining_weight_kg=receipt_weight,
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=unit_cost,
            production_date=now.date(),
            expire_date=None,
            quality_status="PASS",
            status="AVAILABLE",
            remark=_with_document_batch_no(
                _remark_with_deviation("生产工单入库；来源库存批次已记录", payload.deviation_remark),
                document_batch_no,
            ),
        )
        db.add(finished_lot)
        db.flush()
        upsert_stock_balance(
            db,
            item_id=product.id,
            warehouse_id=finished_warehouse.id,
            location_id=None,
            qty_delta=finished_qty,
            weight_delta=receipt_weight,
            available_qty_delta=finished_qty,
            available_weight_delta=receipt_weight,
            unit_cost=unit_cost,
        )
        txn = create_inventory_txn(
            db,
            txn_type="FG_IN",
            item_id=product.id,
            warehouse_id=finished_warehouse.id,
            location_id=None,
            lot_id=finished_lot.id,
            qty_change=finished_qty,
            weight_change=receipt_weight,
            unit_cost=unit_cost,
            source_doc_type="FG_RECEIPT",
            source_doc_id=receipt.id,
            source_line_id=receipt_item.id,
            biz_time=now,
            operator_user_id=operator_user_id,
            remark=_with_document_batch_no(_remark_with_deviation("生产工单入库", payload.deviation_remark), document_batch_no),
            amount_basis="QTY",
        )
        finished_lot_id = finished_lot.id
        finished_txn_id = txn.id
        work_order.finished_qty = to_decimal(work_order.finished_qty) + finished_qty

    if surplus_weight > 0:
        source_lot.remaining_weight_kg = to_decimal(source_lot.remaining_weight_kg) + surplus_weight
        source_lot.status = "AVAILABLE"
        db.add(source_lot)
        upsert_stock_balance(
            db,
            item_id=material.id,
            warehouse_id=source_lot.warehouse_id,
            location_id=source_lot.location_id,
            qty_delta=Decimal("0"),
            weight_delta=surplus_weight,
            available_qty_delta=Decimal("0"),
            available_weight_delta=surplus_weight,
            unit_cost=to_decimal(source_lot.unit_cost),
        )
        txn = create_inventory_txn(
            db,
            txn_type="PRODUCTION_SURPLUS_IN",
            item_id=material.id,
            warehouse_id=source_lot.warehouse_id,
            location_id=source_lot.location_id,
            lot_id=source_lot.id,
            qty_change=Decimal("0"),
            weight_change=surplus_weight,
            unit_cost=to_decimal(source_lot.unit_cost),
            source_doc_type="WORK_ORDER",
            source_doc_id=work_order.id,
            source_line_id=material_row.id if material_row else None,
            biz_time=now,
            operator_user_id=operator_user_id,
            remark=_with_document_batch_no(_remark_with_deviation("生产余料入库", payload.deviation_remark), document_batch_no),
            amount_basis="WEIGHT",
        )
        if material_row:
            material_row.returned_weight_kg = to_decimal(material_row.returned_weight_kg) + surplus_weight
            db.add(material_row)
        surplus_lot_id = source_lot.id
        surplus_txn_id = txn.id

    if scrap_weight > 0 and scrap_warehouse:
        scrap_lot_no = _build_lot_no(db, "SCRAPI", work_order, material, now)
        scrap_lot = StockLot(
            lot_no=scrap_lot_no,
            parent_lot_id=source_lot.id,
            lot_role="PRODUCTION_SCRAP",
            material_sub_batch_no=None,
            item_id=material.id,
            warehouse_id=scrap_warehouse.id,
            location_id=None,
            source_doc_type="PRODUCTION_SCRAP_IN",
            source_doc_id=work_order.id,
            source_line_id=None,
            source_material_lot_id=source_lot.id,
            source_material_sub_batch_no=issue.material_sub_batch_no,
            source_material_summary=f"{material.item_name}：{source_lot.lot_no}",
            inbound_qty=Decimal("0"),
            inbound_weight_kg=scrap_weight,
            remaining_qty=Decimal("0"),
            remaining_weight_kg=scrap_weight,
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=to_decimal(source_lot.unit_cost),
            production_date=now.date(),
            expire_date=None,
            quality_status="SCRAP",
            status="AVAILABLE",
            remark=_with_document_batch_no(
                _remark_with_deviation("生产废料入库；来源库存批次已记录", payload.deviation_remark),
                document_batch_no,
            ),
        )
        db.add(scrap_lot)
        db.flush()
        upsert_stock_balance(
            db,
            item_id=material.id,
            warehouse_id=scrap_warehouse.id,
            location_id=None,
            qty_delta=Decimal("0"),
            weight_delta=scrap_weight,
            available_qty_delta=Decimal("0"),
            available_weight_delta=scrap_weight,
            unit_cost=to_decimal(source_lot.unit_cost),
        )
        txn = create_inventory_txn(
            db,
            txn_type="PRODUCTION_SCRAP_IN",
            item_id=material.id,
            warehouse_id=scrap_warehouse.id,
            location_id=None,
            lot_id=scrap_lot.id,
            qty_change=Decimal("0"),
            weight_change=scrap_weight,
            unit_cost=to_decimal(source_lot.unit_cost),
            source_doc_type="WORK_ORDER",
            source_doc_id=work_order.id,
            source_line_id=None,
            biz_time=now,
            operator_user_id=operator_user_id,
            remark=_with_document_batch_no(_remark_with_deviation("生产废料入库", payload.deviation_remark), document_batch_no),
            amount_basis="WEIGHT",
        )
        scrap_lot_id = scrap_lot.id
        scrap_txn_id = txn.id

    settled = False
    if payload.settle_work_order:
        work_order.status = "SETTLED"
        work_order.actual_end_time = work_order.actual_end_time or now
        _append_settle_remark(work_order, payload.deviation_remark, payload.remark)
        settled = True
    response_work_order_id = int(work_order.id)
    response_work_order_no = str(work_order.work_order_no)
    response_status = str(work_order.status)
    response_remark = work_order.remark
    main_txn_id = finished_txn_id or surplus_txn_id or scrap_txn_id
    db.add(work_order)
    db.commit()

    archive_status = "未生成"
    archive_business_id = int(main_txn_id) if main_txn_id else None
    archive_document_type = DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT if main_txn_id else None
    archive_error_message = None
    if main_txn_id:
        try:
            archive_result = generate_document_archive(
                db,
                DOCUMENT_TYPE_PRODUCTION_INBOUND_SETTLEMENT,
                int(main_txn_id),
                created_by=operator_user_id,
            )
            archive_status = archive_result.archive_status
            archive_error_message = archive_result.archive_error_message
        except Exception as exc:
            _ = exc
            archive_status = "归档失败"
            archive_error_message = "归档失败：PDF归档服务异常，请稍后重新生成"

    return ProductionWorkOrderInboundRead(
        work_order_id=response_work_order_id,
        work_order_no=response_work_order_no,
        finished_qty=float(_round_decimal(finished_qty)),
        surplus_weight_kg=float(_round_decimal(surplus_weight)),
        scrap_weight_kg=float(_round_decimal(scrap_weight)),
        finished_lot_id=finished_lot_id,
        surplus_lot_id=surplus_lot_id,
        scrap_lot_id=scrap_lot_id,
        finished_txn_id=finished_txn_id,
        surplus_txn_id=surplus_txn_id,
        scrap_txn_id=scrap_txn_id,
        settled=settled,
        status=response_status,
        remark=response_remark,
        archive_status=archive_status,
        archive_business_id=archive_business_id,
        archive_document_type=archive_document_type,
        archive_error_message=archive_error_message,
    )
