"""Opérations transactionnelles et FEFO du stock d'entretien."""
from datetime import date
from decimal import Decimal, ROUND_HALF_UP

from sqlalchemy import or_, case

from ...extensions import db
from ...core.models import (
    ProductGeneric, CommercialProduct, ProductPackaging, StockLocation,
    StockLot, StockLotBalance, StockMovement, ReusableContainer, ContainerFill,
)

Q = Decimal("0.000001")


class StockError(ValueError):
    pass


def dec(value):
    try:
        return Decimal(str(value)).quantize(Q, rounding=ROUND_HALF_UP)
    except Exception as exc:
        raise StockError("Quantité invalide") from exc


def _balance(lot_id, location_id, create=False):
    query = StockLotBalance.query.filter_by(lot_id=lot_id, location_id=location_id)
    balance = query.with_for_update().first()
    if not balance and create:
        balance = StockLotBalance(lot_id=lot_id, location_id=location_id, quantity=0)
        db.session.add(balance)
        db.session.flush()
    return balance


def _movement(**kwargs):
    movement = StockMovement(**kwargs)
    db.session.add(movement)
    return movement


def receive_stock(*, generic_product_id, commercial_product_id, packaging_id, packages,
                  lot_number, received_at, location_id, user_id=None, expiry_date=None,
                  manufactured_at=None, comment=None):
    """Réceptionne un nombre de conditionnements et crée le lot + mouvement."""
    packages = dec(packages)
    if packages <= 0:
        raise StockError("La quantité reçue doit être positive")
    packaging = ProductPackaging.query.get(packaging_id)
    if not packaging or packaging.generic_product_id != generic_product_id:
        raise StockError("Conditionnement inconnu pour ce produit")
    if packaging.commercial_product_id and packaging.commercial_product_id != commercial_product_id:
        raise StockError("Conditionnement associé à une autre référence")
    quantity = (packages * dec(packaging.reference_quantity_per_package)).quantize(Q)
    lot = StockLot(generic_product_id=generic_product_id, commercial_product_id=commercial_product_id,
                   lot_number=(lot_number or "").strip() or "Sans numéro", received_at=received_at,
                   manufactured_at=manufactured_at, expiry_date=expiry_date,
                   quantity_received=quantity)
    db.session.add(lot)
    db.session.flush()
    balance = _balance(lot.id, location_id, create=True)
    balance.quantity = quantity
    _movement(generic_product_id=generic_product_id, commercial_product_id=commercial_product_id,
              lot_id=lot.id, movement_type="reception_fournisseur", quantity_reference=quantity,
              destination_location_id=location_id, user_id=user_id, comment=comment)
    return lot


def _usable_lots(generic_product_id, location_id, today=None):
    today = today or date.today()
    return (StockLot.query.join(StockLotBalance)
            .filter(StockLot.generic_product_id == generic_product_id,
                    StockLotBalance.location_id == location_id,
                    StockLotBalance.quantity > 0,
                    StockLot.status.notin_(("bloqué", "detruit", "expire", "epuise")),
                    or_(StockLot.expiry_date.is_(None), StockLot.expiry_date >= today))
            .order_by(case((StockLot.expiry_date.is_(None), 1), else_=0), StockLot.expiry_date, StockLot.received_at, StockLot.id)
            .with_for_update().all())


def issue_stock(*, generic_product_id, quantity, source_location_id, user_id=None,
                staff_id=None, movement_type="sortie_agent", comment=None, today=None):
    """Sortie FEFO atomique. Retourne les mouvements créés."""
    requested = dec(quantity)
    if requested <= 0:
        raise StockError("La quantité doit être positive")
    remaining = requested
    movements = []
    for lot in _usable_lots(generic_product_id, source_location_id, today=today):
        balance = _balance(lot.id, source_location_id)
        available = dec(balance.quantity)
        taken = min(available, remaining)
        if taken <= 0:
            continue
        balance.quantity = available - taken
        if balance.quantity == 0 and all(dec(b.quantity) == 0 for b in lot.balances):
            lot.status = "epuise"
        movements.append(_movement(generic_product_id=generic_product_id,
            commercial_product_id=lot.commercial_product_id, lot_id=lot.id,
            movement_type=movement_type, quantity_reference=-taken,
            source_location_id=source_location_id, staff_id=staff_id, user_id=user_id, comment=comment))
        remaining -= taken
        if remaining <= 0:
            break
    if remaining > 0:
        raise StockError(f"Stock disponible insuffisant (manque {remaining} unité(s) de référence)")
    return movements


def transfer_stock(*, generic_product_id, quantity, source_location_id, destination_location_id,
                   user_id=None, comment=None):
    if source_location_id == destination_location_id:
        raise StockError("Les emplacements source et destination doivent être différents")
    requested = dec(quantity)
    if requested <= 0:
        raise StockError("La quantité doit être positive")
    remaining = requested
    total = Decimal("0")
    for lot in _usable_lots(generic_product_id, source_location_id):
        source = _balance(lot.id, source_location_id)
        taken = min(dec(source.quantity), remaining)
        if taken <= 0:
            continue
        source.quantity = dec(source.quantity) - taken
        destination = _balance(lot.id, destination_location_id, create=True)
        destination.quantity = dec(destination.quantity) + taken
        _movement(generic_product_id=generic_product_id,
                  commercial_product_id=lot.commercial_product_id, lot_id=lot.id,
                  movement_type="transfert", quantity_reference=-taken,
                  source_location_id=source_location_id, destination_location_id=destination_location_id,
                  user_id=user_id, comment=comment)
        total += taken
        remaining -= taken
        if remaining <= 0:
            break
    if remaining > 0:
        raise StockError(f"Stock disponible insuffisant (manque {remaining} unité(s) de référence)")
    return total


def transvasement(*, generic_product_id, quantity_liters, source_location_id,
                  container_id=None, user_id=None, staff_id=None, comment=None):
    movements = issue_stock(generic_product_id=generic_product_id, quantity=quantity_liters,
                            source_location_id=source_location_id, user_id=user_id,
                            staff_id=staff_id, movement_type="transvasement", comment=comment)
    total = sum((-dec(m.quantity_reference) for m in movements), Decimal("0"))
    if container_id:
        container = ReusableContainer.query.get(container_id)
        if not container:
            raise StockError("Flacon inconnu")
        if container.dedicated_product_id and container.dedicated_product_id != generic_product_id:
            raise StockError("Ce flacon est dédié à un autre produit")
        if total > dec(container.volume_liters):
            raise StockError("Le volume dépasse la capacité du flacon")
        container.current_product_id = generic_product_id
        container.last_filled_at = db.func.now()
        db.session.flush()
        db.session.add(ContainerFill(container_id=container.id, movement_id=movements[0].id,
                                      volume_liters=total))
    return movements


def dilute(*, generic_product_id, total_liters, ratio_percent, source_location_id,
           user_id=None, staff_id=None, container_id=None, comment=None):
    total = dec(total_liters)
    ratio = dec(ratio_percent)
    if total <= 0 or ratio <= 0 or ratio >= 100:
        raise StockError("Volume et taux de dilution invalides")
    concentrate = (total * ratio / Decimal("100")).quantize(Q)
    return transvasement(generic_product_id=generic_product_id, quantity_liters=concentrate,
                         source_location_id=source_location_id, container_id=container_id,
                         user_id=user_id, staff_id=staff_id, comment=f"Dilution {ratio}%{(' — ' + comment) if comment else ''}")
