"""Hard-constraint filter (constraints #1-#6 from the spec)."""
from __future__ import annotations

from datetime import date, timedelta
from typing import Dict, Iterable, List, Optional, Set
from uuid import UUID

from app.services.planner.config import PlannerConfig
from app.services.planner.types import FilterResult, RecipeCost


def _calorie_band(target: int, pct: int) -> tuple[int, int]:
    delta = target * pct / 100.0
    return (int(target - delta), int(target + delta))


def filter_recipes(
    *,
    recipes: Iterable[dict],
    recipe_ingredient_ids: Dict[UUID, Set[UUID]],
    recipe_costs: Dict[UUID, RecipeCost],
    blocked_ingredient_ids: Set[UUID],
    blocked_recipe_ids: Set[UUID],
    last_cooked_at: Dict[UUID, date],
    family_calorie_target: Optional[int],
    config: PlannerConfig,
    today: date,
) -> FilterResult:
    feasible: List[UUID] = []
    rejected: Dict[UUID, str] = {}

    cal_band: Optional[tuple[int, int]] = None
    if family_calorie_target is not None:
        cal_band = _calorie_band(family_calorie_target, config.calorie_tolerance_pct)

    recency_cutoff = today - timedelta(weeks=config.recency_weeks)

    for r in recipes:
        rid = r["id"]
        if isinstance(rid, str):
            rid = UUID(rid)

        # #2: per-recipe blocklist
        if rid in blocked_recipe_ids:
            rejected[rid] = "blocked_recipe"
            continue

        # #1: ingredient blocklist intersection
        if blocked_ingredient_ids & recipe_ingredient_ids.get(rid, set()):
            rejected[rid] = "blocked_ingredient"
            continue

        # #3: recency
        last = last_cooked_at.get(rid)
        if last and last >= recency_cutoff:
            rejected[rid] = "recency"
            continue

        # #4: calories
        if cal_band is not None:
            cps = r.get("calories_per_serving")
            if cps is None:
                rejected[rid] = "calories_missing"
                continue
            lo, hi = cal_band
            if cps < lo or cps > hi:
                rejected[rid] = "calories"
                continue

        # #5: time
        prep = int(r.get("prep_time_minutes") or 0)
        cook = int(r.get("cook_time_minutes") or 0)
        if prep + cook > config.max_total_minutes:
            rejected[rid] = "time"
            continue

        # #6: cost
        cost = recipe_costs.get(rid)
        if cost is None:
            rejected[rid] = "no_cost"
            continue
        if float(cost.cost_per_serving) > config.max_meal_cost:
            rejected[rid] = "cost"
            continue

        feasible.append(rid)

    return FilterResult(feasible_recipe_ids=feasible, rejected=rejected)
