from datetime import date, datetime
from decimal import Decimal, ROUND_HALF_UP
from sqlalchemy.orm import Session
from app.models.inventory import InventoryItem, InventoryTransaction
from app.models.accounting import InventoryLot

MONEY_Q = Decimal("0.01")


def d(value) -> Decimal:
    return Decimal(str(value)).quantize(MONEY_Q, rounding=ROUND_HALF_UP)


class InventoryCostingError(Exception):
    pass


def _refresh_status(item: InventoryItem) -> None:
    if item.quantity <= item.min_quantity:
        item.status = "low_stock"
    else:
        item.status = "in_stock"


def _next_lot_number(db: Session) -> str:
    year = datetime.now().year
    prefix = f"LT-{year}-"
    last = db.query(InventoryLot).filter(
        InventoryLot.lot_number.like(f"{prefix}%")
    ).order_by(InventoryLot.lot_number.desc()).first()
    seq = (int(last.lot_number.split("-")[-1]) + 1) if last else 1
    return f"{prefix}{seq:06d}"


def _available_qty(db: Session, item_id: str, branch_id: str | None = None) -> Decimal:
    q = db.query(InventoryLot).filter(
        InventoryLot.item_id == item_id,
        InventoryLot.is_active == True,  # noqa: E712
        InventoryLot.remaining_qty > 0,
    )
    if branch_id:
        q = q.filter(
            (InventoryLot.branch_id == branch_id) | (InventoryLot.branch_id.is_(None))
        )
    return d(sum(d(lot.remaining_qty) for lot in q.all()))


def inventory_in(db: Session, *, item_id: str, quantity, unit_cost=None,
                 received_date: date | None = None, branch_id: str | None = None,
                 batch_id: str | None = None, reference_type: str | None = None,
                 reference_id: str | None = None, created_by: str | None = None,
                 notes: str | None = None, adjust_quantity: bool = True,
                 journal_entry_id: str | None = None) -> InventoryLot:
    item = db.query(InventoryItem).filter(InventoryItem.id == item_id).first()
    if not item:
        raise InventoryCostingError("Inventory item not found")
    qty = int(quantity)
    if qty <= 0:
        raise InventoryCostingError("Stock-in quantity must be positive")
    uc = d(unit_cost) if unit_cost is not None else d(item.cost_price or 0)
    if adjust_quantity:
        item.quantity += qty
        _refresh_status(item)
    lot = InventoryLot(
        item_id=item.id,
        branch_id=branch_id,
        batch_id=batch_id,
        lot_number=_next_lot_number(db),
        received_date=received_date or date.today(),
        received_qty=qty,
        remaining_qty=qty,
        unit_cost=uc,
        is_active=True,
    )
    db.add(lot)
    db.flush()
    db.add(InventoryTransaction(
        item_id=item.id,
        type="in",
        quantity=qty,
        unit_cost=uc,
        value=d(uc * qty),
        branch_id=branch_id,
        lot_id=lot.id,
        journal_entry_id=journal_entry_id,
        reference_type=reference_type,
        reference_id=reference_id,
        notes=notes,
        created_by=created_by,
    ))
    db.flush()
    return lot


def peek_fifo(db: Session, item_id: str, quantity, branch_id: str | None = None,
              fallback_unit_cost=None) -> list:
    """Plan a FIFO consumption without mutating state. Returns
    [{"lot_id", "quantity", "unit_cost", "value", "legacy": bool}, ...].
    On-hand shortfall from legacy stock (no lots) is planned against the
    inventory item's current cost_price so posting never hard-fails mid-flow."""
    item = db.query(InventoryItem).filter(InventoryItem.id == item_id).first()
    if not item:
        raise InventoryCostingError("Inventory item not found")
    qty = int(quantity)
    if qty <= 0:
        raise InventoryCostingError("Stock-out quantity must be positive")
    planned = []
    remaining = Decimal(qty)
    q = db.query(InventoryLot).filter(
        InventoryLot.item_id == item_id,
        InventoryLot.is_active == True,  # noqa: E712
        InventoryLot.remaining_qty > 0,
    )
    if branch_id:
        q = q.filter(
            (InventoryLot.branch_id == branch_id) | (InventoryLot.branch_id.is_(None))
        )
    for lot in q.order_by(InventoryLot.received_date, InventoryLot.created_at).all():
        if remaining <= 0:
            break
        available = Decimal(lot.remaining_qty)
        take = min(remaining, available)
        planned.append({
            "lot_id": lot.id,
            "quantity": int(take),
            "unit_cost": d(lot.unit_cost),
            "value": d(d(lot.unit_cost) * take),
            "legacy": False,
        })
        remaining -= take
    if remaining > 0:
        uc = d(fallback_unit_cost) if fallback_unit_cost is not None else d(item.cost_price or 0)
        planned.append({
            "lot_id": None,
            "quantity": int(remaining),
            "unit_cost": uc,
            "value": d(uc * remaining),
            "legacy": True,
        })
    return planned


def record_out(db: Session, *, item_id: str, planned: list,
               branch_id: str | None = None, reference_type: str | None = None,
               reference_id: str | None = None, created_by: str | None = None,
               notes: str | None = None, adjust_quantity: bool = True,
               journal_entry_id: str | None = None, out_type: str = "out") -> None:
    item = db.query(InventoryItem).filter(InventoryItem.id == item_id).first()
    if not item:
        raise InventoryCostingError("Inventory item not found")
    total = sum(int(p["quantity"]) for p in planned)
    for p in planned:
        qty = int(p["quantity"])
        if qty <= 0:
            continue
        if p["lot_id"]:
            lot = db.query(InventoryLot).filter(InventoryLot.id == p["lot_id"]).first()
            if lot:
                lot.remaining_qty = d(Decimal(str(lot.remaining_qty)) - qty)
                if lot.remaining_qty <= 0:
                    lot.remaining_qty = Decimal("0")
                    lot.is_active = False
        db.add(InventoryTransaction(
            item_id=item.id,
            type=out_type,
            quantity=qty,
            unit_cost=p["unit_cost"],
            value=p["value"],
            branch_id=branch_id,
            lot_id=p["lot_id"],
            journal_entry_id=journal_entry_id,
            reference_type=reference_type,
            reference_id=reference_id,
            notes=notes,
            created_by=created_by,
        ))
    if adjust_quantity:
        item.quantity -= total
        _refresh_status(item)
    db.flush()


def inventory_out(db: Session, *, item_id: str, quantity, branch_id: str | None = None,
                  reference_type: str | None = None, reference_id: str | None = None,
                  created_by: str | None = None, notes: str | None = None,
                  adjust_quantity: bool = True, journal_entry_id: str | None = None,
                  cost_price: float | None = None) -> list:
    planned = peek_fifo(db, item_id, quantity, branch_id, cost_price)
    record_out(db, item_id=item_id, planned=planned, branch_id=branch_id,
               reference_type=reference_type, reference_id=reference_id,
               created_by=created_by, notes=notes, adjust_quantity=adjust_quantity,
               journal_entry_id=journal_entry_id)
    return planned


def inventory_valuation(db: Session, item_id: str | None = None,
                        branch_id: str | None = None) -> list:
    q = db.query(InventoryLot).filter(
        InventoryLot.is_active == True,  # noqa: E712
        InventoryLot.remaining_qty > 0,
    )
    if item_id:
        q = q.filter(InventoryLot.item_id == item_id)
    if branch_id:
        q = q.filter(InventoryLot.branch_id == branch_id)
    totals = {}
    for lot in q.all():
        item = totals.setdefault(lot.item_id, {"item_id": lot.item_id, "quantity": 0, "value": Decimal("0")})
        item["quantity"] += int(lot.remaining_qty)
        item["value"] += d(d(lot.unit_cost) * lot.remaining_qty)
    out = []
    for key, row in sorted(totals.items()):
        item = db.query(InventoryItem).filter(InventoryItem.id == key).first()
        out.append({
            "item_id": key,
            "item_name": item.name if item else None,
            "item_code": item.code if item else None,
            "quantity": row["quantity"],
            "value": float(d(row["value"])),
        })
    return out