from django.conf import settings
import requests
from typing import Dict, List, Optional
from functools import lru_cache
from typing import List, Dict
import re
from fractions import Fraction


import logging

logger = logging.getLogger(__name__)


class USDAFoodAPI:
    BASE_URL = "https://api.nal.usda.gov/fdc/v1"

    def __init__(self, api_key: Optional[str] = None):
        self.api_key = api_key or getattr(settings, "USDA_API_KEY", None)
        if not self.api_key:
            raise ValueError(
                "USDA API key not found. Set USDA_API_KEY in your Django settings."
            )
        self.session = requests.Session()

    def _get_base_params(self) -> Dict:
        """Return base parameters including the API key."""
        return {"api_key": self.api_key}

    @lru_cache(maxsize=100)
    def search_foods(
        self, query: str, page_size: int = 50, page_number: int = 1
    ) -> List[Dict]:
        """Search for food items by keyword."""
        url = f"{self.BASE_URL}/foods/search"
        params = {
            **self._get_base_params(),
            "query": query,
            "pageSize": page_size,
            "pageNumber": page_number,
            "includeNutrientCodes": "203,204,205,208,269",
        }
        response = self.session.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        return data.get("foods", [])

    def get_food_details(self, fdc_id: int) -> Dict:
        """Retrieve detailed nutrient data for a specific food item."""
        url = f"{self.BASE_URL}/food/{fdc_id}"
        params = {
            **self._get_base_params(),
            "nutrients": [
                203,
                204,
                205,
                208,
                269,
            ],  # Protein, Fat, Carbs, Calories, Fiber
        }
        response = self.session.get(url, params=params)
        response.raise_for_status()
        return response.json()

    def get_multiple_foods(self, fdc_ids: List[int]) -> List[Dict]:
        """Retrieve details for multiple food items."""
        url = f"{self.BASE_URL}/foods"
        params = {
            **self._get_base_params(),
            "fdcIds": fdc_ids,
            "nutrients": ["203", "204", "205", "208", "269"],
        }
        response = self.session.get(url, params=params)
        response.raise_for_status()
        return response.json().get("foods", [])

    def extract_nutrients(self, food_data: Dict) -> Dict:
        """Extract key nutrients into a clean dictionary."""
        nutrients = {
            "protein": 0,
            "fat": 0,
            "carbohydrates": 0,
            "calories": 0,
            "fiber": 0,
            "sugar": 0,
            "sodium": 0,
        }
        nutrient_map = {
            "203": "protein",
            "204": "fat",
            "205": "carbohydrates",
            "208": "calories",
            "269": "fiber",
            "2000": "sodium",
            "269": "sugar",
        }
        food_nutrients = food_data.get("foodNutrients", [])

        if not food_nutrients:
            logger.warning(
                f"No nutrients found for food: {food_data.get('description', 'unknown')}"
            )
            return nutrients

        for nutrient in food_nutrients:
            nutrient_id = None
            value = None

            if isinstance(nutrient, dict):
                if "nutrientNumber" in nutrient:
                    nutrient_id = str(nutrient.get("nutrientNumber", ""))
                    value = nutrient.get("value")
                elif "nutrient" in nutrient and isinstance(nutrient["nutrient"], dict):
                    nutrient_id = str(nutrient.get("nutrient", {}).get("id", ""))
                    value = nutrient.get("value")
                elif "number" in nutrient:
                    nutrient_id = str(nutrient.get("number", ""))
                    value = nutrient.get("value")

            if nutrient_id and nutrient_id in nutrient_map and value is not None:
                key = nutrient_map[nutrient_id]
                try:
                    nutrients[key] = float(value)
                except (TypeError, ValueError):
                    pass

        return nutrients

    def get_food_summary(self, fdc_id: int) -> Dict:
        """Return a simplified summary of a food item."""
        details = self.get_food_details(fdc_id)
        return {
            "fdcId": details.get("fdcId"),
            "description": details.get("description"),
            "dataType": details.get("dataType"),
            "brandOwner": details.get("brandOwner"),
            "servingSize": details.get("servingSize"),
            "servingSizeUnit": details.get("servingSizeUnit"),
            "nutrients": self.extract_nutrients(details),
        }


class NutritionCalculator:
    """Calculate recipe nutrition from ingredients using USDA API."""

    # Common units to strip from ingredient names
    UNITS = [
        "cup",
        "cups",
        "c",
        "tablespoon",
        "tablespoons",
        "tbsp",
        "tb",
        "t",
        "teaspoon",
        "teaspoons",
        "tsp",
        "ts",
        "ounce",
        "ounces",
        "oz",
        "fluid ounce",
        "fluid ounces",
        "fl oz",
        "pound",
        "pounds",
        "lb",
        "lbs",
        "gram",
        "grams",
        "g",
        "kilogram",
        "kilograms",
        "kg",
        "milliliter",
        "milliliters",
        "ml",
        "liter",
        "liters",
        "l",
        "piece",
        "pieces",
        "whole",
        "large",
        "medium",
        "small",
        "can",
        "cans",
        "bottle",
        "bottles",
        "jar",
        "jars",
        "package",
        "packages",
        "pkg",
        "box",
        "boxes",
        "slice",
        "slices",
        "clove",
        "cloves",
        "stick",
        "sticks",
        "pinch",
        "dash",
        "handful",
        "bunch",
        "sprig",
        "sprigs",
    ]

    # Common modifiers to strip (don't affect nutrition much)
    MODIFIERS = [
        "all-purpose",
        "all purpose",
        "ap",
        "bread",
        "cake",
        "self-rising",
        "self rising",
        "granulated",
        "powdered",
        "confectioners",
        "brown",
        "white",
        "raw",
        "extra virgin",
        "virgin",
        "light",
        "dark",
        "heavy",
        "whipping",
        "fresh",
        "dried",
        "frozen",
        "canned",
        "jarred",
        "bottled",
        "chopped",
        "diced",
        "minced",
        "sliced",
        "grated",
        "shredded",
        "crushed",
        "melted",
        "softened",
        "room temperature",
        "cold",
        "warm",
        "hot",
        "packed",
        "sifted",
        "sieved",
        "ground",
        "whole",
        "halved",
        "quartered",
        "peeled",
        "unpeeled",
        "seeded",
        "pitted",
        "cored",
        "trimmed",
        "optional",
        "to taste",
        "as needed",
        "for garnish",
        "for serving",
    ]

    def __init__(self):
        self.usda = USDAFoodAPI()
        self._unit_pattern = self._build_unit_pattern()

    def _build_unit_pattern(self) -> re.Pattern:
        """Build regex pattern for matching units."""
        # Sort by length (longest first) to match "tablespoon" before "tbsp"
        sorted_units = sorted(self.UNITS, key=len, reverse=True)
        unit_str = "|".join(re.escape(u) for u in sorted_units)
        return re.compile(rf"\b({unit_str})\b", re.IGNORECASE)

    def parse_ingredient(self, ingredient_text: str) -> Dict:
        """
        Parse ingredient string into quantity, unit, and clean ingredient name.

        Examples:
            "2 cups all-purpose flour" → {qty: 2, unit: 'cups', name: 'flour'}
            "1 1/2 tsp salt" → {qty: 1.5, unit: 'tsp', name: 'salt'}
            "1/4 cup olive oil" → {qty: 0.25, unit: 'cup', name: 'olive oil'}
            "3 large eggs" → {qty: 3, unit: 'large', name: 'eggs'}
        """
        text = ingredient_text.strip()

        # Parse quantity (handles whole numbers, fractions, and mixed numbers)
        quantity, remaining = self._parse_quantity(text)

        # Parse unit from remaining text
        unit, remaining = self._parse_unit(remaining)

        # Clean the ingredient name
        ingredient_name = self._clean_ingredient_name(remaining)

        return {
            "quantity": quantity,
            "unit": unit,
            "ingredient": ingredient_name,
            "original": text,
        }

    def _parse_quantity(self, text: str) -> tuple:
        """
        Extract quantity from start of ingredient text.
        Handles: "2", "1/2", "1 1/2", "0.5", etc.
        """
        text = text.strip()

        # Pattern for mixed numbers: "1 1/2", "2 3/4"
        mixed_pattern = r"^(\d+)\s+(\d+)/(\d+)"
        mixed_match = re.match(mixed_pattern, text)
        if mixed_match:
            whole = int(mixed_match.group(1))
            numerator = int(mixed_match.group(2))
            denominator = int(mixed_match.group(3))
            quantity = whole + (numerator / denominator)
            remaining = text[mixed_match.end() :].strip()
            return quantity, remaining

        # Pattern for simple fractions: "1/2", "3/4"
        fraction_pattern = r"^(\d+)/(\d+)"
        fraction_match = re.match(fraction_pattern, text)
        if fraction_match:
            numerator = int(fraction_match.group(1))
            denominator = int(fraction_match.group(2))
            quantity = numerator / denominator
            remaining = text[fraction_match.end() :].strip()
            return quantity, remaining

        # Pattern for decimals: "0.5", "1.5"
        decimal_pattern = r"^(\d+\.?\d*)"
        decimal_match = re.match(decimal_pattern, text)
        if decimal_match:
            quantity = float(decimal_match.group(1))
            remaining = text[decimal_match.end() :].strip()
            return quantity, remaining

        # No quantity found
        return 1, text

    def _parse_unit(self, text: str) -> tuple:
        """Extract unit from text."""
        text = text.strip()

        match = self._unit_pattern.match(text)
        if match:
            unit = match.group(1).lower()
            remaining = text[match.end() :].strip()
            return unit, remaining

        return None, text

    def _clean_ingredient_name(self, text: str) -> str:
        """
        Clean ingredient name for better USDA matching.
        Removes modifiers, extra whitespace, and normalizes text.
        """
        text = text.strip()

        # Remove parenthetical notes: "flour (sifted)" → "flour"
        text = re.sub(r"\s*\([^)]*\)", "", text)

        # Remove common modifiers
        for modifier in sorted(self.MODIFIERS, key=len, reverse=True):
            # Use word boundaries to avoid partial matches
            pattern = rf"\b{re.escape(modifier)}\b"
            text = re.sub(pattern, "", text, flags=re.IGNORECASE)

        # Remove extra whitespace
        text = re.sub(r"\s+", " ", text)

        # Remove trailing punctuation
        text = text.strip(" ,;:.")

        # Remove leading "of" (e.g., "cup of flour" → "flour")
        text = re.sub(r"^of\s+", "", text, flags=re.IGNORECASE)

        return text.strip()

    def _find_usda_match(self, ingredient_name: str) -> Optional[Dict]:
        """
        Find best USDA match for an ingredient name.
        Tries multiple search strategies for better matching.
        """
        strategies = [
            # Strategy 1: Direct search
            ingredient_name,
            # Strategy 2: Singular form (remove trailing 's')
            (ingredient_name.rstrip("s") if ingredient_name.endswith("s") else None),
            # Strategy 3: Remove common suffixes
            re.sub(
                r"\s+(fresh|dried|ground|chopped|sliced)$",
                "",
                ingredient_name,
                flags=re.IGNORECASE,
            ),
            # Strategy 4: Just the last word (sometimes works for simple ingredients)
            (ingredient_name.split()[-1] if len(ingredient_name.split()) > 1 else None),
        ]

        for query in strategies:
            if not query or len(query) < 2:
                continue

            try:
                results = self.usda.search_foods(query, page_size=5)
                if results:
                    logger.debug(f"Found {query}: {results[0].get('description')}")
                    return results[0]
            except Exception as e:
                logger.warning(f"USDA search failed for '{query}': {e}")
                continue

        return None

    def _convert_to_grams(
        self, quantity: float, unit: str, ingredient_name: str
    ) -> float:
        """
        Convert quantity to grams for more accurate nutrition calculation.
        This is simplified - production code should use a proper conversion table.
        """
        # Base conversions (approximate)
        volume_to_grams = {
            "cup": 240,
            "cups": 240,
            "tablespoon": 15,
            "tablespoons": 15,
            "tbsp": 15,
            "teaspoon": 5,
            "teaspoons": 5,
            "tsp": 5,
            "ounce": 28,
            "ounces": 28,
            "oz": 28,
            "pound": 454,
            "pounds": 454,
            "lb": 454,
            "lbs": 454,
            "gram": 1,
            "grams": 1,
            "g": 1,
        }

        # Get base weight in grams
        base_grams = volume_to_grams.get(unit, 100)  # Default to 100g if unknown

        # Adjust for ingredient type (very simplified)
        if any(
            word in ingredient_name.lower()
            for word in ["flour", "sugar", "rice", "grain"]
        ):
            base_grams = volume_to_grams.get(unit, 125)  # Dry ingredients
        elif any(word in ingredient_name.lower() for word in ["oil", "butter", "fat"]):
            base_grams = volume_to_grams.get(unit, 220)  # Dense liquids
        elif any(
            word in ingredient_name.lower() for word in ["milk", "water", "broth"]
        ):
            base_grams = volume_to_grams.get(unit, 240)  # Liquids

        return quantity * base_grams

    def calculate_nutrition(self, ingredients: List[str], servings: int = 1) -> Dict:
        """
        Calculate total nutrition for a recipe from ingredient list.
        Returns nutrition per serving.
        """
        totals = {
            "calories": 0,
            "protein": 0,
            "fat": 0,
            "carbohydrates": 0,
            "fiber": 0,
            "sugar": 0,
            "sodium": 0,
        }

        matched_count = 0
        unmatched = []

        for ingredient in ingredients:
            parsed = self.parse_ingredient(ingredient)

            # Find USDA match
            match = self._find_usda_match(
                " ".join(
                    [
                        str(parsed["quantity"]),
                        str(parsed["unit"]),
                        str(parsed["ingredient"]),
                    ]
                )
            )
            if not match:
                match = self._find_usda_match(parsed["ingredient"])

            if match:
                try:
                    nutrients = self.usda.extract_nutrients(match)
                    # Convert to grams for more accurate calculation
                    grams = self._convert_to_grams(
                        parsed["quantity"],
                        parsed["unit"],
                        parsed["ingredient"],
                    )

                    # USDA data is typically per 100g, so scale accordingly
                    multiplier = grams / 100

                    totals["calories"] += nutrients.get("calories", 0) * multiplier
                    totals["protein"] += nutrients.get("protein", 0) * multiplier
                    totals["fat"] += nutrients.get("fat", 0) * multiplier
                    totals["carbohydrates"] += (
                        nutrients.get("carbohydrates", 0) * multiplier
                    )
                    totals["fiber"] += nutrients.get("fiber", 0) * multiplier
                    totals["sugar"] += nutrients.get("sugar", 0) * multiplier
                    totals["sodium"] += nutrients.get("sodium", 0) * multiplier

                    matched_count += 1

                except Exception:
                    unmatched.append(parsed["original"])
            else:
                unmatched.append(parsed["original"])

        # Divide by servings for per-serving nutrition
        if servings > 0:
            for key in totals:
                totals[key] = round(totals[key] / servings, 2)

        # Add metadata
        totals["_matched"] = matched_count
        totals["_total"] = len(ingredients)
        totals["_unmatched"] = unmatched

        return totals
