from __future__ import annotations

from decimal import Decimal

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

from app.models.master_data import BomItem, Item
from app.models.operations import (
    CompletionReceipt,
    CompletionReceiptItem,
    InventoryTxn,
    StockLot,
    WorkOrder,
    WorkOrderMaterial,
    WorkOrderMaterialIssue,
    WorkOrderOperation,
)
from app.services.operations import to_decimal


INBOUND_TOLERANCE_MULTIPLIER = Decimal("1.15")


class WorkOrderLimitSnapshot(BaseModel):
    work_order_id: int
    gross_weight_per_piece_kg: Decimal
    net_weight_per_piece_kg: Decimal
    issued_weight_kg: Decimal
    used_material_weight_kg: Decimal
    returned_material_weight_kg: Decimal
    final_operation_good_qty: Decimal
    finished_received_qty: Decimal
    total_operation_scrap_qty: Decimal
    scrap_inbound_weight_kg: Decimal
    standard_scrap_weight_kg: Decimal
    max_finished_inbound_qty: Decimal
    max_scrap_inbound_weight_kg: Decimal
    max_surplus_inbound_weight_kg: Decimal
    tolerance_finished_inbound_qty: Decimal
    tolerance_scrap_inbound_weight_kg: Decimal
    tolerance_surplus_inbound_weight_kg: Decimal


class WorkOrderLedgerMaterialRow(BaseModel):
    material_item_id: int
    material_code: str
    material_name: str
    source_lot_id: int
    source_lot_no: str
    issued_weight_kg: Decimal
    returned_weight_kg: Decimal = Decimal("0")
    unit_cost: Decimal = Decimal("0")
    amount: Decimal = Decimal("0")


class WorkOrderLedgerOperationRow(BaseModel):
    work_order_operation_id: int
    seq_no: int
    operation_name: str
    report_qty: Decimal
    good_qty: Decimal
    scrap_qty: Decimal
    status: str


class WorkOrderLedgerRow(BaseModel):
    work_order_id: int
    work_order_no: str
    product_item_id: int
    product_code: str
    product_name: str
    planned_qty: Decimal
    status: str
    remark: str | None = None
    material_summary: str
    source_lot_summary: str
    issued_weight_kg: Decimal
    used_material_weight_kg: Decimal
    returned_material_weight_kg: Decimal
    finished_received_qty: Decimal
    max_finished_inbound_qty: Decimal
    max_scrap_inbound_weight_kg: Decimal
    max_surplus_inbound_weight_kg: Decimal
    limits: WorkOrderLimitSnapshot
    materials: list[WorkOrderLedgerMaterialRow]
    operations: list[WorkOrderLedgerOperationRow]


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


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


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="生产工单不存在")
    return work_order


def _get_weight_profile(db: Session, work_order: WorkOrder) -> tuple[Decimal, Decimal]:
    bom_item = db.scalar(
        select(BomItem)
        .where(
            BomItem.bom_id == work_order.bom_id,
            BomItem.gross_weight_per_piece_kg > 0,
        )
        .order_by(BomItem.seq_no, BomItem.id)
        .limit(1)
    )
    if not bom_item:
        bom_item = db.scalar(
            select(BomItem)
            .where(BomItem.bom_id == work_order.bom_id)
            .order_by(BomItem.seq_no, BomItem.id)
            .limit(1)
        )
    product = db.get(Item, work_order.product_item_id)
    gross = to_decimal(bom_item.gross_weight_per_piece_kg if bom_item else 0)
    net = to_decimal(bom_item.net_weight_per_piece_kg if bom_item else 0)
    if net <= 0 and product:
        net = to_decimal(product.unit_weight_kg)
    if gross <= 0:
        gross = net
    if gross <= 0:
        raise HTTPException(status_code=400, detail="产品需用清单缺少产品毛重，无法计算工单入库上限")
    if net < 0:
        net = Decimal("0")
    if gross < net:
        gross = net
    return gross, net


def _get_operations(db: Session, work_order_id: int) -> list[WorkOrderOperation]:
    return list(
        db.scalars(
            select(WorkOrderOperation)
            .where(WorkOrderOperation.work_order_id == work_order_id)
            .order_by(WorkOrderOperation.seq_no, WorkOrderOperation.id)
        ).all()
    )


def _get_finished_received_qty(db: Session, work_order_id: int) -> Decimal:
    value = db.scalar(
        select(func.coalesce(func.sum(CompletionReceiptItem.receipt_qty), 0))
        .join(CompletionReceipt, CompletionReceipt.id == CompletionReceiptItem.completion_receipt_id)
        .where(CompletionReceipt.work_order_id == work_order_id)
    )
    return to_decimal(value)


def _get_scrap_inbound_weight(db: Session, work_order_id: int) -> Decimal:
    value = db.scalar(
        select(func.coalesce(func.sum(InventoryTxn.weight_change_kg), 0)).where(
            InventoryTxn.txn_type == "PRODUCTION_SCRAP_IN",
            InventoryTxn.source_doc_type.in_(("WORK_ORDER", "PRODUCTION_SCRAP_IN")),
            InventoryTxn.source_doc_id == work_order_id,
        )
    )
    return _non_negative(to_decimal(value))


def get_work_order_limit_snapshot(db: Session, work_order_id: int) -> WorkOrderLimitSnapshot:
    work_order = _get_work_order(db, work_order_id)
    gross_weight, net_weight = _get_weight_profile(db, work_order)
    operations = _get_operations(db, work_order_id)
    issued_weight = to_decimal(
        db.scalar(
            select(func.coalesce(func.sum(WorkOrderMaterialIssue.issued_weight_kg), 0)).where(
                WorkOrderMaterialIssue.work_order_id == work_order_id
            )
        )
    )
    returned_weight = to_decimal(
        db.scalar(
            select(func.coalesce(func.sum(WorkOrderMaterial.returned_weight_kg), 0)).where(
                WorkOrderMaterial.work_order_id == work_order_id
            )
        )
    )
    max_report_qty = max((to_decimal(operation.report_qty) for operation in operations), default=Decimal("0"))
    used_material_weight = _round_decimal(max_report_qty * gross_weight)
    final_good_qty = to_decimal(operations[-1].good_qty) if operations else Decimal("0")
    finished_received_qty = _get_finished_received_qty(db, work_order_id)
    total_scrap_qty = sum((to_decimal(operation.scrap_qty) for operation in operations), Decimal("0"))
    scrap_inbound_weight = _get_scrap_inbound_weight(db, work_order_id)
    edge_scrap_weight = _non_negative((max_report_qty - total_scrap_qty) * _non_negative(gross_weight - net_weight))
    rejected_product_scrap_weight = total_scrap_qty * gross_weight
    standard_scrap_weight = edge_scrap_weight + rejected_product_scrap_weight
    max_finished_inbound_qty = _non_negative(final_good_qty - finished_received_qty)
    max_scrap_inbound_weight = _non_negative(standard_scrap_weight - scrap_inbound_weight)
    max_surplus_inbound_weight = _non_negative(issued_weight - used_material_weight - returned_weight)
    tolerance_scrap_inbound_weight = _non_negative(
        standard_scrap_weight * INBOUND_TOLERANCE_MULTIPLIER - scrap_inbound_weight
    )
    tolerance_surplus_inbound_weight = _non_negative(
        (issued_weight - used_material_weight) * INBOUND_TOLERANCE_MULTIPLIER - returned_weight
    )

    return WorkOrderLimitSnapshot(
        work_order_id=work_order_id,
        gross_weight_per_piece_kg=_round_decimal(gross_weight),
        net_weight_per_piece_kg=_round_decimal(net_weight),
        issued_weight_kg=_round_decimal(issued_weight),
        used_material_weight_kg=_round_decimal(used_material_weight),
        returned_material_weight_kg=_round_decimal(returned_weight),
        final_operation_good_qty=_round_decimal(final_good_qty),
        finished_received_qty=_round_decimal(finished_received_qty),
        total_operation_scrap_qty=_round_decimal(total_scrap_qty),
        scrap_inbound_weight_kg=_round_decimal(scrap_inbound_weight),
        standard_scrap_weight_kg=_round_decimal(standard_scrap_weight),
        max_finished_inbound_qty=_round_decimal(max_finished_inbound_qty),
        max_scrap_inbound_weight_kg=_round_decimal(max_scrap_inbound_weight),
        max_surplus_inbound_weight_kg=_round_decimal(max_surplus_inbound_weight),
        tolerance_finished_inbound_qty=_round_decimal(max_finished_inbound_qty * INBOUND_TOLERANCE_MULTIPLIER),
        tolerance_scrap_inbound_weight_kg=_round_decimal(tolerance_scrap_inbound_weight),
        tolerance_surplus_inbound_weight_kg=_round_decimal(tolerance_surplus_inbound_weight),
    )


def validate_inbound_with_tolerance(label: str, requested: Decimal, calculated_max: Decimal) -> None:
    if requested <= 0:
        raise HTTPException(status_code=400, detail=f"{label}必须大于0")
    allowed = _round_decimal(_non_negative(calculated_max) * INBOUND_TOLERANCE_MULTIPLIER)
    if requested > allowed:
        raise HTTPException(status_code=400, detail=f"{label}超过允许入库上限 {allowed}")


def validate_work_order_source_lot(
    db: Session,
    work_order_id: int,
    source_lot_id: int,
    item_id: int,
) -> WorkOrderMaterial:
    row = db.execute(
        select(WorkOrderMaterial)
        .join(WorkOrderMaterialIssue, WorkOrderMaterialIssue.work_order_material_id == WorkOrderMaterial.id)
        .where(
            WorkOrderMaterial.work_order_id == work_order_id,
            WorkOrderMaterial.material_item_id == item_id,
            WorkOrderMaterialIssue.source_lot_id == source_lot_id,
        )
        .limit(1)
    ).scalar_one_or_none()
    if not row:
        raise HTTPException(status_code=400, detail="来源库存批次号不属于该生产工单")
    return row


def get_work_order_ledger_rows(
    db: Session,
    limit: int = 200,
    work_order_id: int | None = None,
) -> list[WorkOrderLedgerRow]:
    stmt = select(WorkOrder).order_by(WorkOrder.id.desc()).limit(limit)
    if work_order_id:
        stmt = stmt.where(WorkOrder.id == work_order_id)
    work_orders = db.scalars(stmt).all()
    rows: list[WorkOrderLedgerRow] = []

    for work_order in work_orders:
        product = db.get(Item, work_order.product_item_id)
        limits = get_work_order_limit_snapshot(db, work_order.id)
        surplus_return_subquery = (
            select(
                InventoryTxn.lot_id.label("source_lot_id"),
                func.coalesce(func.sum(InventoryTxn.weight_change_kg), 0).label("returned_weight_kg"),
            )
            .where(
                InventoryTxn.txn_type == "PRODUCTION_SURPLUS_IN",
                InventoryTxn.source_doc_type == "WORK_ORDER",
                InventoryTxn.source_doc_id == work_order.id,
            )
            .group_by(InventoryTxn.lot_id)
            .subquery()
        )
        issue_rows = db.execute(
            select(
                WorkOrderMaterialIssue.material_item_id,
                Item.item_code,
                Item.item_name,
                WorkOrderMaterialIssue.source_lot_id,
                StockLot.lot_no,
                WorkOrderMaterialIssue.issued_weight_kg,
                func.coalesce(surplus_return_subquery.c.returned_weight_kg, 0).label("returned_weight_kg"),
                WorkOrderMaterialIssue.unit_cost,
                WorkOrderMaterialIssue.amount,
            )
            .join(Item, Item.id == WorkOrderMaterialIssue.material_item_id)
            .join(StockLot, StockLot.id == WorkOrderMaterialIssue.source_lot_id)
            .join(WorkOrderMaterial, WorkOrderMaterial.id == WorkOrderMaterialIssue.work_order_material_id)
            .outerjoin(surplus_return_subquery, surplus_return_subquery.c.source_lot_id == WorkOrderMaterialIssue.source_lot_id)
            .where(WorkOrderMaterialIssue.work_order_id == work_order.id)
            .order_by(WorkOrderMaterialIssue.id)
        ).mappings().all()
        material_rows = [
            WorkOrderLedgerMaterialRow(
                material_item_id=int(issue["material_item_id"]),
                material_code=issue["item_code"],
                material_name=issue["item_name"],
                source_lot_id=int(issue["source_lot_id"]),
                source_lot_no=issue["lot_no"],
                issued_weight_kg=_round_decimal(to_decimal(issue["issued_weight_kg"])),
                returned_weight_kg=_round_decimal(to_decimal(issue["returned_weight_kg"])),
                unit_cost=_round_decimal(to_decimal(issue["unit_cost"]), "0.0001"),
                amount=_round_decimal(to_decimal(issue["amount"]), "0.01"),
            )
            for issue in issue_rows
        ]
        operation_rows = [
            WorkOrderLedgerOperationRow(
                work_order_operation_id=int(operation.id),
                seq_no=int(operation.seq_no),
                operation_name=operation.operation_name,
                report_qty=_round_decimal(to_decimal(operation.report_qty)),
                good_qty=_round_decimal(to_decimal(operation.good_qty)),
                scrap_qty=_round_decimal(to_decimal(operation.scrap_qty)),
                status=operation.status,
            )
            for operation in _get_operations(db, work_order.id)
        ]
        material_names = sorted({row.material_name for row in material_rows})
        source_lot_nos = [row.source_lot_no for row in material_rows]
        rows.append(
            WorkOrderLedgerRow(
                work_order_id=int(work_order.id),
                work_order_no=work_order.work_order_no,
                product_item_id=int(work_order.product_item_id),
                product_code=product.item_code if product else "",
                product_name=product.item_name if product else "",
                planned_qty=_round_decimal(to_decimal(work_order.planned_qty)),
                status=work_order.status,
                remark=work_order.remark,
                material_summary="、".join(material_names) if material_names else "-",
                source_lot_summary="、".join(source_lot_nos) if source_lot_nos else "-",
                issued_weight_kg=limits.issued_weight_kg,
                used_material_weight_kg=limits.used_material_weight_kg,
                returned_material_weight_kg=limits.returned_material_weight_kg,
                finished_received_qty=limits.finished_received_qty,
                max_finished_inbound_qty=limits.max_finished_inbound_qty,
                max_scrap_inbound_weight_kg=limits.max_scrap_inbound_weight_kg,
                max_surplus_inbound_weight_kg=limits.max_surplus_inbound_weight_kg,
                limits=limits,
                materials=material_rows,
                operations=operation_rows,
            )
        )

    return rows
