from __future__ import annotations

from datetime import date, datetime
from decimal import Decimal

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

from app.models.master_data import Item, Warehouse
from app.models.operations import InventoryTxn, SpecialWarehouseAdjustment, SpecialWarehouseAdjustmentLine, StockLot
from app.schemas.operations import SpecialWarehouseAdjustmentCreate
from app.services.operations import (
    build_inventory_lot_no,
    clamp_non_negative,
    create_inventory_txn,
    get_location_or_default,
    to_decimal,
    upsert_stock_balance,
)
from app.services.sales_planning import next_doc_no
from app.services.stocktake import ensure_warehouses_unlocked


SOURCE_DOC_TYPE = "SPECIAL_ADJUSTMENT"
LOT_ROLE = "SPECIAL_ADJUSTMENT"


def _strip_text(value: str | None) -> str:
    return str(value or "").strip()


def _raise_value_from_http(exc: HTTPException) -> None:
    detail = getattr(exc, "detail", None)
    raise ValueError(str(detail or exc)) from exc


def _format_decimal(value: Decimal) -> str:
    normalized = value.quantize(Decimal("0.000001")).normalize()
    if normalized == normalized.to_integral():
        return str(normalized.quantize(Decimal("1")))
    return format(normalized, "f")


def _trim_255(value: str) -> str:
    return value[:255]


def _trim_to(value: str, limit: int) -> str:
    if limit <= 0:
        return ""
    return value[:limit]


def _warehouse_type_label(warehouse_type: str) -> str:
    return {
        "AUX": "辅料",
        "FINISHED": "成品",
        "RETURN": "退货",
        "SEMI": "半成品",
        "SCRAP": "废品",
        "RAW": "原材料",
    }.get(warehouse_type, "库存")


def _build_adjustment_no(db: Session, *, direction: str, adjusted_at: datetime) -> str:
    direction_label = "入库" if direction == "IN" else "出库"
    return next_doc_no(
        db,
        SpecialWarehouseAdjustment,
        "adjustment_no",
        f"特殊调整{adjusted_at.strftime('%Y%m%d')}{direction_label}",
    )


def _build_special_lot_no(
    db: Session,
    *,
    adjustment_id: int,
    item_id: int,
    warehouse_type: str,
    now: datetime,
) -> str:
    if warehouse_type == "RAW":
        return build_inventory_lot_no(db, item_id)
    prefix = f"{_warehouse_type_label(warehouse_type)}特殊{now.strftime('%Y%m%d')}-{adjustment_id}"
    candidate = next_doc_no(db, StockLot, "lot_no", prefix)
    sequence = 1
    while db.scalar(select(StockLot.id).where(StockLot.lot_no == candidate).limit(1)):
        sequence += 1
        candidate = f"{prefix}-{sequence:03d}"
    return candidate


def _normalize_change(
    *,
    warehouse_type: str,
    raw_qty: Decimal,
    raw_weight: Decimal,
) -> tuple[Decimal, Decimal]:
    qty = to_decimal(raw_qty)
    weight = to_decimal(raw_weight)
    if qty < 0 or weight < 0:
        raise ValueError("特殊调整数量和重量不能小于0")

    if warehouse_type in {"RAW", "SCRAP"}:
        qty = Decimal("0")
        if weight <= 0:
            raise ValueError("特殊调整重量必须大于0")
    elif warehouse_type == "AUX":
        weight = Decimal("0")
        if qty <= 0:
            raise ValueError("特殊调整数量必须大于0")
    elif warehouse_type == "SEMI":
        if qty > 0 and weight > 0:
            raise ValueError("半成品特殊调整只允许选择数量或重量一种口径")
        if qty <= 0 and weight <= 0:
            raise ValueError("特殊调整数量或重量必须大于0")
    else:
        if qty <= 0:
            raise ValueError("特殊调整数量必须大于0")
        weight = max(weight, Decimal("0"))
    return to_decimal(qty), to_decimal(weight)


def _assert_out_available(lot: StockLot, *, qty: Decimal, weight: Decimal, warehouse_type: str) -> None:
    available_qty = clamp_non_negative(to_decimal(lot.remaining_qty) - to_decimal(lot.locked_qty))
    available_weight = clamp_non_negative(to_decimal(lot.remaining_weight_kg) - to_decimal(lot.locked_weight_kg))
    if warehouse_type == "AUX" and qty > available_qty:
        raise ValueError("特殊出库数量不能超过当前可用数量")
    if warehouse_type in {"RAW", "SCRAP"} and weight > available_weight:
        raise ValueError("特殊出库重量不能超过当前可用重量")
    if warehouse_type not in {"AUX", "RAW", "SCRAP"}:
        if qty > available_qty:
            raise ValueError("特殊出库数量不能超过当前可用数量")
        if weight > available_weight:
            raise ValueError("特殊出库重量不能超过当前可用重量")


def _build_remark(
    *,
    direction: str,
    reason: str,
    line_reason: str | None,
    before_qty: Decimal,
    after_qty: Decimal,
    before_weight: Decimal,
    after_weight: Decimal,
) -> str:
    direction_label = "特殊入库" if direction == "IN" else "特殊出库"
    metrics = [
        f"调整前数量：{_format_decimal(before_qty)}",
        f"调整后数量：{_format_decimal(after_qty)}",
        f"调整前重量：{_format_decimal(before_weight)}",
        f"调整后重量：{_format_decimal(after_weight)}",
    ]
    metric_text = "；".join(metrics)
    reason_prefix = f"{direction_label}说明："
    reason_text = _strip_text(reason)
    detail = _strip_text(line_reason)

    reserved_without_detail = f"{reason_prefix}；{metric_text}"
    reason_limit = max(0, 255 - len(reserved_without_detail))
    parts = [f"{reason_prefix}{_trim_to(reason_text, reason_limit)}", metric_text]

    if detail:
        detail_prefix = "明细说明："
        detail_reserved = "；".join(parts) + f"；{detail_prefix}"
        detail_limit = max(0, 255 - len(detail_reserved))
        trimmed_detail = _trim_to(detail, detail_limit)
        if trimmed_detail:
            parts.append(f"{detail_prefix}{trimmed_detail}")
    return "；".join(parts)


def _create_new_special_lot(
    db: Session,
    *,
    adjustment_id: int,
    item_id: int,
    warehouse_id: int,
    location_id: int | None,
    warehouse_type: str,
    qty: Decimal,
    weight: Decimal,
    unit_cost: Decimal,
    reason: str,
    now: datetime,
) -> StockLot:
    summary = f"特殊入库说明：{reason}"
    lot = StockLot(
        lot_no=_build_special_lot_no(
            db,
            adjustment_id=adjustment_id,
            item_id=item_id,
            warehouse_type=warehouse_type,
            now=now,
        ),
        parent_lot_id=None,
        lot_role=LOT_ROLE,
        material_sub_batch_no=None,
        item_id=item_id,
        warehouse_id=warehouse_id,
        location_id=location_id,
        source_doc_type=SOURCE_DOC_TYPE,
        source_doc_id=adjustment_id,
        source_line_id=None,
        source_material_lot_id=None,
        source_material_sub_batch_no=None,
        source_material_summary=summary,
        inbound_qty=qty,
        inbound_weight_kg=weight,
        remaining_qty=qty,
        remaining_weight_kg=weight,
        locked_qty=to_decimal(0),
        locked_weight_kg=to_decimal(0),
        unit_cost=unit_cost,
        production_date=now.date() if isinstance(now.date(), date) else None,
        expire_date=None,
        quality_status="PASS",
        status="AVAILABLE",
        remark=_trim_255(summary),
        created_at=now,
        updated_at=now,
    )
    db.add(lot)
    db.flush()
    return lot


def create_special_inventory_adjustment(
    db: Session,
    payload: SpecialWarehouseAdjustmentCreate,
    user_id: int | None = None,
) -> SpecialWarehouseAdjustment:
    now = datetime.now()
    direction = str(payload.direction or "").upper()
    reason = _strip_text(payload.reason)
    if direction not in {"IN", "OUT"}:
        raise ValueError("特殊库存调整方向只能是特殊入库或特殊出库")
    direction_label = "特殊入库" if direction == "IN" else "特殊出库"
    if len(reason) < 4:
        raise ValueError(f"{direction_label}必须填写说明")
    if len(reason) > 1000:
        raise ValueError(f"{direction_label}说明不能超过1000个字")

    try:
        warehouse = db.get(Warehouse, payload.warehouse_id)
        if not warehouse:
            raise ValueError("仓库不存在")
        warehouse_type = str(warehouse.warehouse_type or "").upper()
        ensure_warehouses_unlocked(db, [payload.warehouse_id], direction_label)

        adjustment = SpecialWarehouseAdjustment(
            adjustment_no=_build_adjustment_no(db, direction=direction, adjusted_at=now),
            warehouse_id=payload.warehouse_id,
            warehouse_type=warehouse_type,
            direction=direction,
            reason=reason,
            status="已确认",
            operator_user_id=user_id,
            adjusted_at=now,
            created_at=now,
            updated_at=now,
        )
        db.add(adjustment)
        db.flush()

        for index, line_payload in enumerate(payload.lines, start=1):
            item = db.get(Item, line_payload.item_id)
            if not item:
                raise ValueError(f"物料不存在: {line_payload.item_id}")
            location = get_location_or_default(db, payload.warehouse_id, line_payload.location_id)
            if location and int(location.warehouse_id) != int(payload.warehouse_id):
                raise ValueError("选择的库位不属于当前仓库")

            change_qty, change_weight = _normalize_change(
                warehouse_type=warehouse_type,
                raw_qty=to_decimal(line_payload.adjust_qty),
                raw_weight=to_decimal(line_payload.adjust_weight_kg),
            )
            unit_cost = to_decimal(line_payload.unit_cost, "0.0001")
            lot: StockLot | None

            if direction == "OUT":
                if not line_payload.lot_id:
                    raise ValueError("特殊出库必须选择库存批次")
                lot = db.get(StockLot, line_payload.lot_id)
                if not lot or int(lot.item_id) != int(line_payload.item_id) or int(lot.warehouse_id) != int(payload.warehouse_id):
                    raise ValueError("库存批次不存在或不属于当前仓库物料")
                if location and lot.location_id and int(lot.location_id) != int(location.id):
                    raise ValueError("库存批次库位与选择库位不一致")
                _assert_out_available(lot, qty=change_qty, weight=change_weight, warehouse_type=warehouse_type)
                unit_cost = to_decimal(lot.unit_cost, "0.0001") if unit_cost <= 0 else unit_cost
                before_qty = to_decimal(lot.remaining_qty)
                before_weight = to_decimal(lot.remaining_weight_kg)
                after_qty = clamp_non_negative(before_qty - change_qty)
                after_weight = clamp_non_negative(before_weight - change_weight)
                qty_delta = -change_qty
                weight_delta = -change_weight
            else:
                if line_payload.lot_id:
                    lot = db.get(StockLot, line_payload.lot_id)
                    if not lot or int(lot.item_id) != int(line_payload.item_id) or int(lot.warehouse_id) != int(payload.warehouse_id):
                        raise ValueError("库存批次不存在或不属于当前仓库物料")
                    if location and lot.location_id and int(lot.location_id) != int(location.id):
                        raise ValueError("库存批次库位与选择库位不一致")
                    unit_cost = to_decimal(lot.unit_cost, "0.0001") if unit_cost <= 0 else unit_cost
                    before_qty = to_decimal(lot.remaining_qty)
                    before_weight = to_decimal(lot.remaining_weight_kg)
                    lot.inbound_qty = to_decimal(lot.inbound_qty) + change_qty
                    lot.inbound_weight_kg = to_decimal(lot.inbound_weight_kg) + change_weight
                    lot.remaining_qty = before_qty + change_qty
                    lot.remaining_weight_kg = before_weight + change_weight
                    lot.status = "AVAILABLE"
                    lot.updated_at = now
                    db.add(lot)
                else:
                    before_qty = to_decimal(0)
                    before_weight = to_decimal(0)
                    lot = _create_new_special_lot(
                        db,
                        adjustment_id=adjustment.id,
                        item_id=line_payload.item_id,
                        warehouse_id=payload.warehouse_id,
                        location_id=location.id if location else None,
                        warehouse_type=warehouse_type,
                        qty=change_qty,
                        weight=change_weight,
                        unit_cost=unit_cost,
                        reason=reason,
                        now=now,
                    )
                after_qty = before_qty + change_qty
                after_weight = before_weight + change_weight
                qty_delta = change_qty
                weight_delta = change_weight

            remark = _build_remark(
                direction=direction,
                reason=reason,
                line_reason=line_payload.line_reason,
                before_qty=before_qty,
                after_qty=after_qty,
                before_weight=before_weight,
                after_weight=after_weight,
            )
            if direction == "OUT":
                lot.remaining_qty = after_qty
                lot.remaining_weight_kg = after_weight
                if after_qty <= 0 and after_weight <= 0:
                    lot.status = "DEPLETED"
                lot.updated_at = now
                db.add(lot)

            line = SpecialWarehouseAdjustmentLine(
                adjustment_id=adjustment.id,
                line_no=index,
                item_id=line_payload.item_id,
                warehouse_id=payload.warehouse_id,
                location_id=lot.location_id if lot else (location.id if location else None),
                lot_id=lot.id,
                inventory_txn_id=None,
                before_qty=before_qty,
                change_qty=qty_delta,
                after_qty=after_qty,
                before_weight_kg=before_weight,
                change_weight_kg=weight_delta,
                after_weight_kg=after_weight,
                unit_cost=unit_cost,
                line_reason=_strip_text(line_payload.line_reason) or reason,
                created_at=now,
                updated_at=now,
            )
            db.add(line)
            db.flush()
            if direction == "IN" and lot.source_doc_type == SOURCE_DOC_TYPE and lot.source_line_id is None:
                lot.source_line_id = line.id
                db.add(lot)

            upsert_stock_balance(
                db,
                item_id=line_payload.item_id,
                warehouse_id=payload.warehouse_id,
                location_id=line.location_id,
                qty_delta=qty_delta,
                weight_delta=weight_delta,
                available_qty_delta=qty_delta,
                available_weight_delta=weight_delta,
                unit_cost=unit_cost,
            )
            txn: InventoryTxn = create_inventory_txn(
                db,
                txn_type="SPECIAL_IN" if direction == "IN" else "SPECIAL_OUT",
                item_id=line_payload.item_id,
                warehouse_id=payload.warehouse_id,
                location_id=line.location_id,
                lot_id=lot.id,
                qty_change=qty_delta,
                weight_change=weight_delta,
                unit_cost=unit_cost,
                source_doc_type=SOURCE_DOC_TYPE,
                source_doc_id=adjustment.id,
                source_line_id=line.id,
                biz_time=now,
                operator_user_id=user_id,
                remark=remark,
                amount_basis="WEIGHT" if abs(weight_delta) > 0 else "QTY",
            )
            line.inventory_txn_id = txn.id
            line.updated_at = now
            db.add(line)

        db.commit()
        db.refresh(adjustment)
        return adjustment
    except HTTPException as exc:
        db.rollback()
        _raise_value_from_http(exc)
    except ValueError:
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise
