"""Prévision déterministe tenant compte du calendrier réel du collège."""
from datetime import date, timedelta
from decimal import Decimal

from ...core.models import ProductGeneric, ProductPackaging, StockLot, StockLotBalance, CollegeClosure, ClosureWorkDay, CleaningForecastConfig


def _day_kind(day, closures, permanence_dates):
    if day in permanence_dates:
        return "permanence"
    for closure in closures:
        if closure.start_date <= day <= closure.end_date:
            return "ferme"
    if day.weekday() >= 5:
        return "ferme"
    return "scolaire"


def forecast_product(product, target_date, *, start_date=None, config=None):
    """Retourne un dictionnaire explicable de besoin et de conditionnement."""
    start_date = start_date or date.today()
    if target_date < start_date:
        raise ValueError("La date cible doit être postérieure au début")
    config = config or CleaningForecastConfig.query.filter_by(is_active=True).first() or CleaningForecastConfig()
    # Les valeurs de colonne SQLAlchemy ne sont appliquées qu'à l'insertion ;
    # conserver les mêmes valeurs par défaut pour un calcul sans configuration.
    normal_coefficient = config.normal_coefficient if config.normal_coefficient is not None else Decimal("1")
    permanence_coefficient = config.permanence_coefficient if config.permanence_coefficient is not None else Decimal("0.4")
    closed_coefficient = config.closed_coefficient if config.closed_coefficient is not None else Decimal("0")
    closures = CollegeClosure.query.filter(CollegeClosure.end_date >= start_date,
                                           CollegeClosure.start_date <= target_date).all()
    closure_ids = [c.id for c in closures]
    permanence_dates = {d.work_date for d in ClosureWorkDay.query.filter(
        ClosureWorkDay.work_date >= start_date, ClosureWorkDay.work_date <= target_date,
        ClosureWorkDay.closure_id.in_(closure_ids)).all()} if closure_ids else set()
    normal = Decimal(str(product.forecast_daily_quantity or 0)) * Decimal(str(normal_coefficient))
    permanence = Decimal(str(product.forecast_daily_quantity or 0)) * Decimal(str(permanence_coefficient))
    closed = Decimal(str(product.forecast_daily_quantity or 0)) * Decimal(str(closed_coefficient))
    quantities = {"scolaire": Decimal("0"), "permanence": Decimal("0"), "ferme": Decimal("0")}
    day = start_date
    while day <= target_date:
        kind = _day_kind(day, closures, permanence_dates)
        quantities[kind] += {"scolaire": normal, "permanence": permanence, "ferme": closed}[kind]
        day += timedelta(days=1)
    total_need = sum(quantities.values(), Decimal("0"))
    # Stock réellement utile selon la chronologie FEFO : un lot expiré avant
    # la fin de période n'est retenu qu'à concurrence de la consommation avant DLU.
    usable = Decimal("0")
    expiring = Decimal("0")
    for lot in (StockLot.query.filter_by(generic_product_id=product.id)
                .filter(StockLot.status.notin_(("bloqué", "detruit", "expire", "epuise")))
                .order_by(StockLot.expiry_date.is_(None), StockLot.expiry_date, StockLot.received_at).all()):
        quantity = sum((Decimal(str(b.quantity or 0)) for b in lot.balances), Decimal("0"))
        if not quantity:
            continue
        if lot.expiry_date and lot.expiry_date < target_date:
            # consommation estimée jusqu'à la DLU (approximation jour par jour)
            before = forecast_product(product, lot.expiry_date, start_date=start_date, config=config)["consumption_total"] if lot.expiry_date >= start_date else Decimal("0")
            usable += min(quantity, before)
            expiring += max(quantity - before, Decimal("0"))
        else:
            usable += quantity
    net = max(total_need + Decimal(str(product.stock_security or 0)) - usable, Decimal("0"))
    packaging = ProductPackaging.query.filter_by(generic_product_id=product.id, is_active=True, is_current=True).order_by(ProductPackaging.reference_quantity_per_package).first()
    packages = (net / Decimal(str(packaging.reference_quantity_per_package))).to_integral_value(rounding="ROUND_CEILING") if packaging and net else Decimal("0")
    proposed = packages * Decimal(str(packaging.reference_quantity_per_package)) if packaging else net
    return {"start_date": start_date, "target_date": target_date, "school_days_consumption": quantities["scolaire"],
            "permanence_consumption": quantities["permanence"], "closed_consumption": quantities["ferme"],
            "consumption_total": total_need, "usable_stock": usable, "expiring_stock": expiring,
            "safety_stock": Decimal(str(product.stock_security or 0)), "net_need": net,
            "packaging": packaging, "packages_to_order": packages, "proposed_quantity": proposed}
