"""End-to-end planner orchestration: load → filter → score → select → persist."""
from __future__ import annotations

from datetime import date
from decimal import Decimal
from typing import Dict, List, Optional, Set
from uuid import UUID

from sqlalchemy.orm import Session

from app.models import (
    FamilyProfile,
    GroceryItem,
    HomePantry,
    Ingredient,
    IngredientGroceryMatch,
    MealPlan,
    MealPlanItem,
    MealPlanItemStatus,
    MealPlanStatus,
    MealType,
    NeverSuggest,
    Recipe,
)
from app.services.planner.config import DEFAULT, PlannerConfig
from app.services.planner.cost import compute_recipe_cost
from app.services.planner.filter import filter_recipes
from app.services.planner.score import score_recipes
from app.services.planner.select import select_set
from app.services.planner.types import GenerationResult


def _load_match_index(db: Session) -> Dict[UUID, List[dict]]:
    """ingredient_id → list of match dicts ordered by confidence DESC."""
    rows = (
        db.query(IngredientGroceryMatch, GroceryItem, Ingredient)
        .join(GroceryItem, GroceryItem.id == IngredientGroceryMatch.grocery_item_id)
        .join(Ingredient, Ingredient.id == IngredientGroceryMatch.ingredient_id)
        .order_by(IngredientGroceryMatch.confidence.desc())
        .all()
    )
    index: Dict[UUID, List[dict]] = {}
    for match, grocery, ingredient in rows:
        index.setdefault(match.ingredient_id, []).append(
            {
                "grocery_item_id": grocery.id,
                "grocery_item_name": grocery.name,
                "current_price": grocery.current_price,
                "regular_price": grocery.regular_price,
                "is_on_sale": bool(grocery.is_on_sale),
                "confidence": match.confidence,
                "ingredient_name": ingredient.name.lower(),
                "grocery_unit": grocery.unit,
            }
        )
    return index


def _load_blocklists(
    db: Session, family_id: UUID
) -> tuple[Set[UUID], Set[UUID]]:
    blocked_ingredients: Set[UUID] = set()
    blocked_recipes: Set[UUID] = set()
    for row in db.query(NeverSuggest).filter(NeverSuggest.family_profile_id == family_id).all():
        if row.ingredient_id is not None:
            blocked_ingredients.add(row.ingredient_id)
        if row.recipe_id is not None:
            blocked_recipes.add(row.recipe_id)
    return blocked_ingredients, blocked_recipes


def _load_pantry(db: Session, family_id: UUID) -> Set[UUID]:
    return {
        row.ingredient_id
        for row in db.query(HomePantry).filter(HomePantry.family_profile_id == family_id).all()
        if row.ingredient_id is not None
    }


def _load_last_cooked(db: Session, family_id: UUID) -> Dict[UUID, date]:
    rows = (
        db.query(MealPlanItem, MealPlan)
        .join(MealPlan, MealPlan.id == MealPlanItem.meal_plan_id)
        .filter(MealPlan.family_profile_id == family_id)
        .all()
    )
    last: Dict[UUID, date] = {}
    for item, plan in rows:
        if item.recipe_id is None:
            continue
        if item.recipe_id not in last or plan.week_start_date > last[item.recipe_id]:
            last[item.recipe_id] = plan.week_start_date
    return last


def generate_meal_plan(
    db: Session,
    *,
    family_id: UUID,
    week_start_date: date,
    config: PlannerConfig = DEFAULT,
    today: Optional[date] = None,
    exclude_recipe_ids: Optional[Set[UUID]] = None,
) -> GenerationResult:
    today = today or date.today()
    family = db.query(FamilyProfile).filter(FamilyProfile.id == family_id).first()
    if family is None:
        raise ValueError(f"family_profile {family_id} not found")

    # Merge family-level planner overrides if present
    effective_config = config
    if family.planner_config:
        from app.services.planner.config import PlannerConfig
        try:
            effective_config = config.merge(family.planner_config)
        except (ValueError, TypeError) as exc:
            logger.warning("Invalid planner_config for family %s: %s", family_id, exc)

    recipes = db.query(Recipe).all()
    exclude_set = exclude_recipe_ids or set()
    recipe_dicts = [
        {
            "id": r.id,
            "name": r.name,
            "prep_time_minutes": r.prep_time_minutes,
            "cook_time_minutes": r.cook_time_minutes,
            "calories_per_serving": r.calories_per_serving,
            "protein_type": r.protein_type,
            "cuisine_tags": list(r.cuisine_tags or []),
            "ingredients": list(r.ingredients or []),
            "servings": r.servings or 4,
        }
        for r in recipes
        if r.id not in exclude_set
    ]

    recipe_ingredient_ids: Dict[UUID, Set[UUID]] = {}
    for r in recipe_dicts:
        ids: Set[UUID] = set()
        for line in r["ingredients"]:
            ing_id = line.get("ingredient_id")
            if isinstance(ing_id, str):
                ing_id = UUID(ing_id)
            if ing_id is not None:
                ids.add(ing_id)
        recipe_ingredient_ids[r["id"]] = ids

    match_index = _load_match_index(db)
    pantry_ids = _load_pantry(db, family_id)
    blocked_ings, blocked_recipes = _load_blocklists(db, family_id)
    last_cooked = _load_last_cooked(db, family_id)

    recipe_costs = {
        r["id"]: compute_recipe_cost(
            recipe_id=r["id"],
            ingredients=r["ingredients"],
            match_index=match_index,
            pantry_ingredient_ids=pantry_ids,
            servings=r["servings"],
        )
        for r in recipe_dicts
    }

    filtered = filter_recipes(
        recipes=recipe_dicts,
        recipe_ingredient_ids=recipe_ingredient_ids,
        recipe_costs=recipe_costs,
        blocked_ingredient_ids=blocked_ings,
        blocked_recipe_ids=blocked_recipes,
        last_cooked_at=last_cooked,
        family_calorie_target=family.calorie_target,
        config=effective_config,
        today=today,
    )

    feasible_recipes = [r for r in recipe_dicts if r["id"] in filtered.feasible_recipe_ids]
    scored = score_recipes(
        recipes=feasible_recipes,
        recipe_costs=recipe_costs,
        last_cooked_at=last_cooked,
        config=effective_config,
        today=today,
    )
    chosen, set_score = select_set(scored, effective_config)

    plan = MealPlan(
        family_profile_id=family_id,
        week_start_date=week_start_date,
        status=MealPlanStatus.DRAFT,
        total_estimated_cost=sum(
            (s.cost.total_cost for s in chosen), Decimal("0.00")
        ),
    )
    db.add(plan)
    db.flush()

    _dinner_days = [1, 3, 5]  # Mon, Wed, Fri — spread across the week
    for index, scored_recipe in enumerate(chosen):
        day = _dinner_days[index] if index < len(_dinner_days) else index + 1
        meal_type = MealType.DINNER
        item = MealPlanItem(
            meal_plan_id=plan.id,
            recipe_id=scored_recipe.recipe_id,
            day_of_week=day,
            meal_type=meal_type,
            approval_status=MealPlanItemStatus.pending,
            estimated_cost=scored_recipe.cost.total_cost,
            score=scored_recipe.score,
            components=scored_recipe.components,
        )
        db.add(item)

    db.commit()
    db.refresh(plan)

    rejected_summary: Dict[str, int] = {}
    for reason in filtered.rejected.values():
        rejected_summary[reason] = rejected_summary.get(reason, 0) + 1

    return GenerationResult(
        meal_plan_id=plan.id,
        selected=chosen,
        feasible_count=len(filtered.feasible_recipe_ids),
        rejected_summary=rejected_summary,
        set_score=set_score,
    )
