"""Unit conversion between recipe quantities and grocery store prices.

Handles:
1. Unit normalization (synonyms → canonical)
2. Within-family linear conversion (lb ↔ oz, cup ↔ tbsp, etc.)
3. Cross-family conversion via ingredient density lookups
4. Fallback to dimensionless qty if conversion is impossible
"""
from __future__ import annotations

from decimal import Decimal
from typing import Dict, Optional

# ---------------------------------------------------------------------------
# Normalization
# ---------------------------------------------------------------------------
_NORMALIZE: Dict[str, str] = {
    # Weight
    "pound": "lb",
    "pounds": "lb",
    "lbs": "lb",
    "ounce": "oz",
    "ounces": "oz",
    "gram": "g",
    "grams": "g",
    "kg": "g",
    "kilogram": "g",
    "kilograms": "g",
    # Volume
    "cups": "cup",
    "tablespoon": "tbsp",
    "tablespoons": "tbsp",
    "tbs": "tbsp",
    "teaspoon": "tsp",
    "teaspoons": "tsp",
    "gallon": "gal",
    "gallons": "gal",
    "pint": "pt",
    "pints": "pt",
    "quart": "qt",
    "quarts": "qt",
    "fluid ounce": "fl oz",
    "fluid ounces": "fl oz",
    "fl oz": "fl oz",
    "milliliter": "ml",
    "milliliters": "ml",
    "liter": "l",
    "liters": "l",
    # Count / discrete
    "each": "ea",
    "piece": "ea",
    "pieces": "ea",
    "item": "ea",
    "items": "ea",
    "doz": "dozen",
    "dozen": "dozen",
    # Container / pack
    "jar": "jar",
    "pack": "pack",
    "container": "container",
    "carton": "carton",
    "bag": "bag",
    "loaf": "loaf",
    "head": "head",
    "bunch": "bunch",
    "stalk": "stalk",
    "clove": "clove",
    "can": "can",
    # Leave these alone
    "lb": "lb",
    "oz": "oz",
    "g": "g",
    "cup": "cup",
    "tbsp": "tbsp",
    "tsp": "tsp",
    "ea": "ea",
    "dozen": "dozen",
    "gal": "gal",
    "pt": "pt",
    "qt": "qt",
    "ml": "ml",
    "l": "l",
    "fl oz": "fl oz",
}

# ---------------------------------------------------------------------------
# Linear conversion tables (canonical unit → factor to base unit in family)
# ---------------------------------------------------------------------------
_WEIGHT = {"lb": Decimal("1"), "oz": Decimal("0.0625"), "g": Decimal("0.00220462")}
_VOLUME = {
    "cup": Decimal("1"),
    "tbsp": Decimal("0.0625"),
    "tsp": Decimal("0.0208333"),
    "gal": Decimal("16"),
    "qt": Decimal("4"),
    "pt": Decimal("2"),
    "fl oz": Decimal("0.125"),
    "ml": Decimal("0.00422675"),
    "l": Decimal("4.22675"),
}
_COUNT = {"ea": Decimal("1"), "dozen": Decimal("12")}

# ---------------------------------------------------------------------------
# Ingredient density table  (grams per canonical unit used in recipes)
# ---------------------------------------------------------------------------
_DENSITY_G_PER_UNIT: Dict[str, Dict[str, Decimal]] = {
    # ingredient_name_lower → {canonical_unit: grams_per_unit}
    "olive oil": {"tbsp": Decimal("13.5"), "cup": Decimal("216")},
    "butter, unsalted": {"tbsp": Decimal("14.2"), "cup": Decimal("227")},
    "greek yogurt, plain": {"cup": Decimal("227")},
    "milk, whole": {"cup": Decimal("244")},
    "sour cream": {"cup": Decimal("230")},
    "chicken broth": {"cup": Decimal("240")},
    "rice, long-grain white": {"cup": Decimal("185")},
    "pasta, penne": {"lb": Decimal("454"), "cup": Decimal("100")},  # dry
    "pasta, spaghetti": {"lb": Decimal("454"), "cup": Decimal("100")},  # dry
    "salt, kosher": {"tsp": Decimal("6"), "tbsp": Decimal("18")},
    "black pepper": {"tsp": Decimal("2.3")},
    "cumin, ground": {"tsp": Decimal("2.1")},
    "italian seasoning": {"tsp": Decimal("1.5")},
    "curry powder": {"tsp": Decimal("2.4")},
    "paprika, smoked": {"tsp": Decimal("2.3")},
    "soy sauce": {"tbsp": Decimal("16"), "cup": Decimal("256")},
    "salsa, jarred": {"tbsp": Decimal("16")},
    "cheddar cheese, sharp": {"oz": Decimal("28.35")},
    "mozzarella, shredded": {"oz": Decimal("28.35")},
    "parmesan, grated": {"tbsp": Decimal("5")},
    "coconut milk, canned": {"can": Decimal("400"), "cup": Decimal("240")},
    "black beans, canned": {"can": Decimal("439"), "cup": Decimal("240")},
    "chickpeas, canned": {"can": Decimal("439"), "cup": Decimal("240")},
    "diced tomatoes, canned": {"can": Decimal("411"), "cup": Decimal("240")},
}

# Fallbacks: when an ingredient isn't in density table, but we know typical
# pack sizes from seed data (makes approximate cross-unit possible)
_SEED_SIZE_G: Dict[str, Decimal] = {
    "eggs, large": Decimal("50"),
    "avocado": Decimal("170"),
    "lemon": Decimal("58"),
    "lime": Decimal("44"),
    "tomato, roma": Decimal("62"),
    "yellow onion": Decimal("110"),
    "red bell pepper": Decimal("119"),
    "green bell pepper": Decimal("109"),
    "sweet potato": Decimal("114"),
    "zucchini": Decimal("196"),
    "carrot": Decimal("61"),
    "broccoli": Decimal("454"),
    "spinach, fresh": Decimal("28.35"),  # per oz
    "cilantro": Decimal("15"),
    "parsley, italian": Decimal("15"),
}


class UnitConverter:
    """Convert recipe qty+unit to a store price unit (dimensionless fallback)."""

    def __init__(self) -> None:
        # Build fast lookup: alias → density row
        self._density: Dict[str, Dict[str, Decimal]] = {}
        for name, dmap in _DENSITY_G_PER_UNIT.items():
            entry: Dict[str, Decimal] = dict(dmap)
            # Standard weight units are always grams-per-weight-unit
            entry.setdefault("lb", Decimal("453.592"))
            entry.setdefault("oz", Decimal("28.3495"))
            entry.setdefault("g", Decimal("1"))
            self._density[name] = entry
            for alias in name.split(", "):
                self._density.setdefault(alias, entry)
        for name, grams in _SEED_SIZE_G.items():
            entry = self._density.setdefault(name, {})
            entry.setdefault("ea", grams)
            entry.setdefault("lb", Decimal("453.592"))
            entry.setdefault("oz", Decimal("28.3495"))
            entry.setdefault("g", Decimal("1"))
            for alias in name.split(", "):
                self._density.setdefault(alias, entry)

    # -- public API ---------------------------------------------------------

    def normalize(self, unit: Optional[str]) -> str:
        """Return canonical unit string or 'unknown'."""
        if not unit:
            return "unknown"
        s = unit.strip().lower()
        # Exact match first
        canon = _NORMALIZE.get(s)
        if canon:
            return canon
        # De-pluralize fallback
        return _NORMALIZE.get(s.rstrip("s"), s)

    def convert(
        self,
        qty: float,
        from_unit: Optional[str],
        to_unit: Optional[str],
        ingredient_name_lower: Optional[str] = None,
    ) -> Decimal:
        """Return a dimensionless scalar to multiply store price by.

        If conversion is impossible, returns the original qty unchanged
        (preserves the old monotonic ranking signal).
        """
        if not from_unit or not to_unit:
            return Decimal(str(qty))

        fu = self.normalize(from_unit)
        tu = self.normalize(to_unit)

        if fu == tu:
            return Decimal(str(qty))

        # 1. Same family → linear ratio
        ratio = self._same_family_ratio(fu, tu)
        if ratio is not None:
            return (Decimal(str(qty)) * ratio).quantize(Decimal("0.0001"))

        # 2. Cross-family with density
        if ingredient_name_lower:
            ratio = self._density_ratio(fu, tu, ingredient_name_lower)
            if ratio is not None:
                return (Decimal(str(qty)) * ratio).quantize(Decimal("0.0001"))

        # 3. Fallback: treat as dimensionless
        return Decimal(str(qty))

    # -- helpers -----------------------------------------------------------

    def _same_family_ratio(self, fu: str, tu: str) -> Optional[Decimal]:
        for family in (_WEIGHT, _VOLUME, _COUNT):
            if fu in family and tu in family:
                base_fu = family[fu]
                base_tu = family[tu]
                # fu →* base, tu →* base  →  qty_fu × (base_fu / base_tu) = qty_tu
                return base_fu / base_tu
        return None

    def _density_ratio(
        self,
        fu: str,
        tu: str,
        ing_name: str,
    ) -> Optional[Decimal]:
        dmap = self._density.get(ing_name.lower())
        if not dmap:
            return None
        g_fu = dmap.get(fu)
        g_tu = dmap.get(tu)
        if g_fu is None or g_tu is None:
            return None
        # qty_fu × g_fu grams = qty_tu × g_tu grams
        # → qty_tu = qty_fu × g_fu / g_tu
        return g_fu / g_tu


# Module-level singleton for convenience
_DEFAULT_CONVERTER = UnitConverter()


# Convenience free functions so callers don't need an instance

def normalize_unit(unit: Optional[str]) -> str:
    return _DEFAULT_CONVERTER.normalize(unit)


def convert_qty(
    qty: float,
    from_unit: Optional[str],
    to_unit: Optional[str],
    ingredient_name_lower: Optional[str] = None,
) -> Decimal:
    return _DEFAULT_CONVERTER.convert(qty, from_unit, to_unit, ingredient_name_lower)
