# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Spoonacular Recipe Skill

Recipe parsing, scaling, substitutions, meal planning, and nutrition analysis.
Powers the Chef job class with professional kitchen intelligence.

Setup:
    SPOONACULAR_API_KEY=your_key   (get free at https://spoonacular.com/food-api)
    Free tier: 150 points/day (most endpoints cost 1 point)

API: https://api.spoonacular.com/
"""

import logging

from familiar.core.api_cache import cached
from familiar.skills._api_utils import (
    RATE_LIMITERS,
    APIError,
    api_get,
    get_env,
)

logger = logging.getLogger(__name__)

BASE_URL = "https://api.spoonacular.com"

_limiter = RATE_LIMITERS["spoonacular"]


def _api_key() -> str:
    return get_env("SPOONACULAR_API_KEY") or ""


def _params(**extra) -> dict:
    p = {"apiKey": _api_key()}
    p.update(extra)
    return p


def _check_key() -> str:
    """Return error message if API key not configured, empty string if OK."""
    if not _api_key():
        return (
            "Spoonacular API key not configured.\n"
            "Set SPOONACULAR_API_KEY in your environment.\n"
            "Get a free key at: https://spoonacular.com/food-api"
        )
    return ""


def _check_rate_limit() -> str:
    """Return error message if rate limited, empty string if OK."""
    if not _limiter.acquire():
        return f"Spoonacular rate limit reached. Try again in {_limiter.wait_time():.0f} seconds."
    return ""


# ---------------------------------------------------------------------------
# Tool handlers
# ---------------------------------------------------------------------------


def recipe_search(data: dict) -> str:
    """Search for recipes by query, cuisine, diet, or ingredients."""
    err = _check_key()
    if err:
        return err

    query = data.get("query", "").strip()
    cuisine = data.get("cuisine", "")
    diet = data.get("diet", "")
    ingredients = data.get("include_ingredients", "")
    max_results = min(int(data.get("limit", 10)), 20)

    if not query and not cuisine and not diet and not ingredients:
        return "Please provide a search query, cuisine, diet, or ingredients."

    err = _check_rate_limit()
    if err:
        return err

    try:
        result = _recipe_search_cached(query, cuisine, diet, ingredients, max_results)

        recipes = result.get("results", [])
        if not recipes:
            return f"No recipes found for '{query or cuisine or diet or ingredients}'."

        total = result.get("totalResults", len(recipes))
        lines = [f"**Found {total} recipes** (showing {len(recipes)}):", ""]

        for i, r in enumerate(recipes, 1):
            title = r.get("title", "Untitled")
            recipe_id = r.get("id", "")
            ready_min = r.get("readyInMinutes", "")
            servings = r.get("servings", "")
            image = r.get("image", "")

            lines.append(f"{i}. **{title}** (ID: {recipe_id})")
            details = []
            if ready_min:
                details.append(f"{ready_min} min")
            if servings:
                details.append(f"{servings} servings")
            if details:
                lines.append(f"   {' | '.join(details)}")
            if image:
                lines.append(f"   Image: {image}")
            lines.append("")

        lines.append("Use `recipe_nutrition` with a recipe ID for full nutrition info.")
        return "\n".join(lines)

    except APIError as e:
        return str(e)
    except Exception as e:
        logger.error("Spoonacular search error: %s", e)
        return f"Error searching recipes: {e}"


@cached(prefix="spoonacular_search", ttl=86400)
def _recipe_search_cached(
    query: str, cuisine: str, diet: str, ingredients: str, max_results: int
) -> dict:
    params = _params(number=max_results)
    if query:
        params["query"] = query
    if cuisine:
        params["cuisine"] = cuisine
    if diet:
        params["diet"] = diet
    if ingredients:
        params["includeIngredients"] = ingredients
    params["addRecipeInformation"] = True
    return api_get(
        f"{BASE_URL}/recipes/complexSearch",
        params=params,
        service_name="Spoonacular",
    )


def parse_recipe(data: dict) -> str:
    """Extract a recipe from a URL."""
    url = data.get("url", "").strip()
    if not url:
        return "Please provide a recipe URL to parse."

    err = _check_key()
    if err:
        return err
    err = _check_rate_limit()
    if err:
        return err

    try:
        result = _parse_recipe_cached(url)

        title = result.get("title", "Untitled Recipe")
        servings = result.get("servings", "")
        ready_min = result.get("readyInMinutes", "")
        instructions = result.get("instructions", "")

        lines = [f"**{title}**", ""]
        if servings:
            lines.append(f"Servings: {servings}")
        if ready_min:
            lines.append(f"Ready in: {ready_min} minutes")
        lines.append("")

        # Ingredients
        ingredients = result.get("extendedIngredients", [])
        if ingredients:
            lines.append("**Ingredients:**")
            for ing in ingredients:
                original = ing.get("original", ing.get("name", ""))
                lines.append(f"  - {original}")
            lines.append("")

        # Instructions
        if instructions:
            lines.append("**Instructions:**")
            lines.append(instructions[:2000])
            lines.append("")

        # Nutrition summary
        nutrition = result.get("nutrition", {})
        nutrients = nutrition.get("nutrients", [])
        if nutrients:
            lines.append("**Nutrition (per serving):**")
            key_names = ["Calories", "Protein", "Fat", "Carbohydrates", "Fiber", "Sugar"]
            for n in nutrients:
                name = n.get("name", "")
                if name in key_names:
                    amount = n.get("amount", 0)
                    unit = n.get("unit", "")
                    lines.append(f"  {name}: {amount} {unit}")
            lines.append("")

        return "\n".join(lines)

    except APIError as e:
        return str(e)
    except Exception as e:
        logger.error("Spoonacular parse error: %s", e)
        return f"Error parsing recipe: {e}"


@cached(prefix="spoonacular_parse", ttl=86400)
def _parse_recipe_cached(url: str) -> dict:
    return api_get(
        f"{BASE_URL}/recipes/extract",
        params=_params(url=url, analyze=True, forceExtraction=True),
        service_name="Spoonacular",
    )


def recipe_nutrition(data: dict) -> str:
    """Get detailed nutrition for a recipe by ID."""
    recipe_id = data.get("recipe_id", "")
    if not recipe_id:
        return "Please provide a recipe ID. Use `recipe_search` to find one."

    err = _check_key()
    if err:
        return err
    err = _check_rate_limit()
    if err:
        return err

    try:
        result = _nutrition_cached(str(recipe_id))

        lines = []

        # Per-serving nutrition
        nutrients = result.get("nutrients", [])
        if nutrients:
            lines.append("**Nutrition per serving:**")
            for n in nutrients:
                name = n.get("name", "")
                amount = n.get("amount", 0)
                unit = n.get("unit", "")
                pct = n.get("percentOfDailyNeeds", "")
                line = f"  {name}: {amount} {unit}"
                if pct:
                    line += f" ({pct:.0f}% DV)"
                lines.append(line)
            lines.append("")

        # Per-ingredient breakdown
        ingredients = result.get("ingredients", [])
        if ingredients:
            lines.append("**Per-ingredient breakdown:**")
            for ing in ingredients[:10]:
                name = ing.get("name", "")
                amount = ing.get("amount", "")
                unit = ing.get("unit", "")
                ing_nutrients = ing.get("nutrients", [])
                cals = ""
                for n in ing_nutrients:
                    if n.get("name") == "Calories":
                        cals = f" ({n.get('amount', 0)} cal)"
                        break
                lines.append(f"  - {amount} {unit} {name}{cals}")
            lines.append("")

        if not lines:
            return f"No nutrition data found for recipe ID {recipe_id}."

        # Cost info
        cost = result.get("costPerServing")
        if cost:
            lines.append(f"**Estimated cost per serving:** ${cost / 100:.2f}")

        return "\n".join(lines)

    except APIError as e:
        return str(e)
    except Exception as e:
        logger.error("Spoonacular nutrition error: %s", e)
        return f"Error fetching nutrition: {e}"


@cached(prefix="spoonacular_nutrition", ttl=86400)
def _nutrition_cached(recipe_id: str) -> dict:
    return api_get(
        f"{BASE_URL}/recipes/{recipe_id}/nutritionWidget.json",
        params=_params(),
        service_name="Spoonacular",
    )


def find_substitutes(data: dict) -> str:
    """Find substitutes for an ingredient."""
    ingredient = data.get("ingredient", "").strip()
    if not ingredient:
        return "Please provide an ingredient to find substitutes for."

    err = _check_key()
    if err:
        return err
    err = _check_rate_limit()
    if err:
        return err

    try:
        result = _substitutes_cached(ingredient)

        substitutes = result.get("substitutes", [])
        if not substitutes:
            message = result.get("message", f"No substitutes found for '{ingredient}'.")
            return message

        lines = [f"**Substitutes for {ingredient}:**", ""]
        for i, sub in enumerate(substitutes, 1):
            lines.append(f"  {i}. {sub}")

        message = result.get("message", "")
        if message:
            lines.append("")
            lines.append(f"_{message}_")

        return "\n".join(lines)

    except APIError as e:
        return str(e)
    except Exception as e:
        logger.error("Spoonacular substitutes error: %s", e)
        return f"Error finding substitutes: {e}"


@cached(prefix="spoonacular_subs", ttl=86400)
def _substitutes_cached(ingredient: str) -> dict:
    return api_get(
        f"{BASE_URL}/food/ingredients/substitutes",
        params=_params(ingredientName=ingredient),
        service_name="Spoonacular",
    )


def meal_plan(data: dict) -> str:
    """Generate a meal plan for a day or week."""
    timeframe = data.get("timeframe", "day").lower()
    if timeframe not in ("day", "week"):
        return "Timeframe must be 'day' or 'week'."

    target_calories = int(data.get("target_calories", 2000))
    diet = data.get("diet", "")
    exclude = data.get("exclude", "")

    err = _check_key()
    if err:
        return err
    err = _check_rate_limit()
    if err:
        return err

    try:
        result = _meal_plan_cached(timeframe, target_calories, diet, exclude)

        if timeframe == "day":
            meals = result.get("meals", [])
            nutrients = result.get("nutrients", {})

            lines = [
                f"**Meal Plan ({target_calories} cal target):**",
                "",
            ]

            meal_names = ["Breakfast", "Lunch", "Dinner"]
            for i, meal in enumerate(meals[:3]):
                label = meal_names[i] if i < len(meal_names) else f"Meal {i + 1}"
                title = meal.get("title", "")
                ready = meal.get("readyInMinutes", "")
                servings = meal.get("servings", "")
                meal_id = meal.get("id", "")

                lines.append(f"**{label}: {title}** (ID: {meal_id})")
                details = []
                if ready:
                    details.append(f"{ready} min")
                if servings:
                    details.append(f"{servings} servings")
                if details:
                    lines.append(f"  {' | '.join(details)}")
                lines.append("")

            if nutrients:
                lines.append("**Daily totals:**")
                for key in ["calories", "protein", "fat", "carbohydrates"]:
                    val = nutrients.get(key, "")
                    if val:
                        lines.append(f"  {key.title()}: {val:.0f}")

        else:
            # Week plan
            week = result.get("week", {})
            lines = [f"**Weekly Meal Plan ({target_calories} cal/day):**", ""]

            for day_name in [
                "monday",
                "tuesday",
                "wednesday",
                "thursday",
                "friday",
                "saturday",
                "sunday",
            ]:
                day_data = week.get(day_name, {})
                meals = day_data.get("meals", [])
                nutrients = day_data.get("nutrients", {})

                lines.append(f"**{day_name.title()}:**")
                for meal in meals[:3]:
                    title = meal.get("title", "")
                    lines.append(f"  - {title}")

                cals = nutrients.get("calories", "")
                if cals:
                    lines.append(f"  Total: {cals:.0f} cal")
                lines.append("")

        return "\n".join(lines)

    except APIError as e:
        return str(e)
    except Exception as e:
        logger.error("Spoonacular meal plan error: %s", e)
        return f"Error generating meal plan: {e}"


@cached(prefix="spoonacular_meal", ttl=3600)
def _meal_plan_cached(timeframe: str, target_calories: int, diet: str, exclude: str) -> dict:
    params = _params(timeFrame=timeframe, targetCalories=target_calories)
    if diet:
        params["diet"] = diet
    if exclude:
        params["exclude"] = exclude
    return api_get(
        f"{BASE_URL}/mealplanner/generate",
        params=params,
        service_name="Spoonacular",
    )


def scale_recipe(data: dict) -> str:
    """Scale a recipe to a different number of servings."""
    recipe_id = data.get("recipe_id", "")
    if not recipe_id:
        return "Please provide a recipe ID to scale."

    new_servings = int(data.get("new_servings", 4))
    if new_servings < 1 or new_servings > 100:
        return "Servings must be between 1 and 100."

    err = _check_key()
    if err:
        return err
    err = _check_rate_limit()
    if err:
        return err

    try:
        # Get recipe info first, then calculate scaled amounts
        result = _recipe_info_cached(str(recipe_id))

        title = result.get("title", "Untitled")
        original_servings = result.get("servings", 1) or 1
        scale_factor = new_servings / original_servings
        ingredients = result.get("extendedIngredients", [])

        lines = [
            f"**{title}** — scaled to {new_servings} servings",
            f"(original: {original_servings} servings, scale: {scale_factor:.2f}x)",
            "",
            "**Scaled Ingredients:**",
        ]

        for ing in ingredients:
            amount = ing.get("amount", 0)
            unit = ing.get("unit", "")
            name = ing.get("name", ing.get("original", ""))

            scaled = amount * scale_factor
            # Format nicely
            if scaled == int(scaled):
                scaled_str = str(int(scaled))
            else:
                scaled_str = f"{scaled:.2f}"

            lines.append(f"  - {scaled_str} {unit} {name}")

        return "\n".join(lines)

    except APIError as e:
        return str(e)
    except Exception as e:
        logger.error("Spoonacular scale error: %s", e)
        return f"Error scaling recipe: {e}"


@cached(prefix="spoonacular_info", ttl=86400)
def _recipe_info_cached(recipe_id: str) -> dict:
    return api_get(
        f"{BASE_URL}/recipes/{recipe_id}/information",
        params=_params(),
        service_name="Spoonacular",
    )


# ---------------------------------------------------------------------------
# Tool definitions
# ---------------------------------------------------------------------------

TOOLS = [
    {
        "name": "recipe_search",
        "description": (
            "Search for recipes by query, cuisine, diet, or ingredients."
            " Returns recipe IDs for use with recipe_nutrition and scale_recipe."
            " Covers thousands of recipes with detailed information."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Recipe search query (e.g., 'pasta carbonara', 'chicken soup')",
                },
                "cuisine": {
                    "type": "string",
                    "description": (
                        "Filter by cuisine: Italian, Mexican, Chinese, Indian,"
                        " Thai, Japanese, Mediterranean, etc."
                    ),
                },
                "diet": {
                    "type": "string",
                    "description": (
                        "Dietary filter: vegetarian, vegan, gluten free, ketogenic, paleo, whole30"
                    ),
                },
                "include_ingredients": {
                    "type": "string",
                    "description": "Comma-separated ingredients to include",
                },
                "limit": {
                    "type": "integer",
                    "description": "Max results (1-20, default 10)",
                    "default": 10,
                },
            },
        },
        "handler": recipe_search,
        "category": "spoonacular",
    },
    {
        "name": "parse_recipe",
        "description": (
            "Extract a recipe from a URL. Parses ingredients, instructions,"
            " nutrition, and serving info from any recipe webpage."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "URL of the recipe page to parse",
                },
            },
            "required": ["url"],
        },
        "handler": parse_recipe,
        "category": "spoonacular",
    },
    {
        "name": "recipe_nutrition",
        "description": (
            "Get detailed nutrition breakdown for a recipe by ID."
            " Shows per-serving nutrients with daily value percentages"
            " and per-ingredient calorie breakdown."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "recipe_id": {
                    "type": "string",
                    "description": "Recipe ID from recipe_search results",
                },
            },
            "required": ["recipe_id"],
        },
        "handler": recipe_nutrition,
        "category": "spoonacular",
    },
    {
        "name": "find_substitutes",
        "description": (
            "Find cooking substitutes for an ingredient."
            " Useful when an ingredient is unavailable or for dietary restrictions."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "ingredient": {
                    "type": "string",
                    "description": "Ingredient to find substitutes for (e.g., 'butter', 'eggs')",
                },
            },
            "required": ["ingredient"],
        },
        "handler": find_substitutes,
        "category": "spoonacular",
    },
    {
        "name": "meal_plan",
        "description": (
            "Generate a meal plan for a day or week with calorie targets."
            " Supports dietary restrictions and ingredient exclusions."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "timeframe": {
                    "type": "string",
                    "description": "Plan length: 'day' or 'week'",
                    "default": "day",
                },
                "target_calories": {
                    "type": "integer",
                    "description": "Daily calorie target (default 2000)",
                    "default": 2000,
                },
                "diet": {
                    "type": "string",
                    "description": "Diet type: vegetarian, vegan, gluten free, ketogenic, paleo",
                },
                "exclude": {
                    "type": "string",
                    "description": "Comma-separated ingredients to exclude (allergies, preferences)",
                },
            },
        },
        "handler": meal_plan,
        "category": "spoonacular",
    },
    {
        "name": "scale_recipe",
        "description": (
            "Scale a recipe to a different number of servings."
            " Recalculates all ingredient amounts proportionally."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "recipe_id": {
                    "type": "string",
                    "description": "Recipe ID to scale",
                },
                "new_servings": {
                    "type": "integer",
                    "description": "Target number of servings",
                    "default": 4,
                },
            },
            "required": ["recipe_id"],
        },
        "handler": scale_recipe,
        "category": "spoonacular",
    },
]
