"""Recipe Ingestion — normalize and persist discovered recipes.

Takes ExternalRecipe objects, deduplicates, maps ingredients, and inserts into DB.
"""
from __future__ import annotations

import html
import logging
import uuid as _uuid_mod
from typing import List, Optional

from rapidfuzz import fuzz
from sqlalchemy.dialects.postgresql import insert as _pg_insert
from sqlalchemy.orm import Session

from app.services.recipe_enrichment import RecipeEnrichmentService
from app.models import Ingredient, Recipe

logger = logging.getLogger(__name__)

_INGREDIENT_FUZZY_THRESHOLD = 70
_MAX_INGREDIENTS = 20


class RecipeIngestionService:
    """Ingest external recipes into our database."""

    def __init__(self) -> None:
        self.enrichment = RecipeEnrichmentService()

    def ingest(
        self,
        db: Session,
        family_profile_id: _uuid_mod.UUID,
        candidates: List,
        analysis_dict: dict,
    ) -> int:
        """Insert candidates, skipping duplicates. Returns count added."""
        added = 0
        seen_external: set[str] = set()

        for ext in candidates:
            # Skip if already in DB by external_source+external_id
            ext_key = f"{ext.external_source}:{ext.external_id}"
            if ext_key in seen_external:
                continue

            existing = (
                db.query(Recipe)
                .filter(
                    Recipe.external_source == ext.external_source,
                    Recipe.external_id == ext.external_id,
                )
                .first()
            )
            if existing:
                logger.info("RecipeIngestion: duplicate external recipe %s", ext_key)
                continue

            # Skip if name is very similar to an existing recipe (basic fuzzy dedup)
            dup = (
                db.query(Recipe)
                .filter(Recipe.name.ilike(f"%{ext.name[:30]}%"))
                .first()
            )
            if dup and fuzz.ratio(dup.name.lower(), ext.name.lower()) > 85:
                logger.info("RecipeIngestion: fuzzy duplicate with %s", dup.name)
                continue

            # Normalize instructions
            instructions = []
            for step in (ext.instructions or []):
                # Strip Spoonacular HTML tags
                plain = html.unescape(step).replace("\r", "")
                instructions.append(plain)

            # Map ingredients to canonical names/IDs
            mapped_ingredients = self._map_ingredients(db, ext.ingredients)

            # Build discovery_reason from analysis
            queries = analysis_dict.get("discovery_queries", [])
            top_signals = [s["value"] for s in analysis_dict.get("positive_signals", [])]
            discovery_reason = f"Matched queries: {', '.join(queries[:2])}. Signals: {', '.join(top_signals[:2])}."

            # --- Enrich: detailed instructions + side dishes ---
            enriched = self.enrichment.enrich(ext.name, mapped_ingredients, instructions)
            final_instructions = enriched.get("instructions") or instructions
            side_dishes = enriched.get("side_dishes") or []

            # Append side-dish ingredients to the main ingredient list so shopping lists pick them up
            for side in side_dishes:
                for sd_ing in side.get("ingredients", []):
                    mapped_ingredients.append({
                        "ingredient_id": None,
                        "name": sd_ing.get("name", ""),
                        "qty": sd_ing.get("qty"),
                        "unit": sd_ing.get("unit", ""),
                    })

            recipe = Recipe(
                id=_uuid_mod.uuid4(),
                family_profile_id=family_profile_id,
                name=ext.name,
                description=ext.description,
                image_url=ext.image_url,
                image_source=ext.external_source,
                prep_time_minutes=ext.prep_time_minutes,
                cook_time_minutes=ext.cook_time_minutes,
                servings=ext.servings or 4,
                cuisine_tags=ext.cuisine_tags,
                dietary_tags=ext.dietary_tags,
                protein_type=ext.protein_type,
                calories_per_serving=ext.calories_per_serving,
                ingredients=mapped_ingredients,
                side_dishes=side_dishes,
                instructions=final_instructions,
                source_url=ext.source_url,
                external_source=ext.external_source,
                external_id=ext.external_id,
                discovery_reason=discovery_reason,
                is_manually_added=False,
            )
            db.add(recipe)
            added += 1
            seen_external.add(ext_key)
            logger.info("RecipeIngestion: added %r", ext.name)

        # Flush so the recipe is visible to queries within the same session
        db.flush()
        return added

    def _map_ingredients(
        self,
        db: Session,
        external_ingredients: List[dict],
    ) -> List[dict]:
        """Match external ingredient names to canonical Ingredient rows.

        Strategy:
        1. Exact name match (case-insensitive)
        2. Fuzzy match above threshold
        3. If no match, create a new Ingredient row with is_system=False
        """
        out = []
        db_ingredients = {i.name.lower(): i for i in db.query(Ingredient).all()}

        for ing in external_ingredients[:_MAX_INGREDIENTS]:
            name = ing.get("name", "").strip().lower()
            if not name:
                continue

            # Exact match
            canon = db_ingredients.get(name)
            if not canon:
                # Fuzzy fallback
                best = None
                best_score = 0
                for other_name, other in db_ingredients.items():
                    score = fuzz.ratio(name, other_name)
                    if score > best_score:
                        best_score = score
                        best = other
                if best and best_score >= _INGREDIENT_FUZZY_THRESHOLD:
                    canon = best

            if canon:
                out.append({
                    "ingredient_id": str(canon.id),
                    "name": canon.name,
                    "qty": ing.get("qty"),
                    "unit": ing.get("unit", ""),
                })
            else:
                # Create Ingredient from external data (unverified)
                name_title = ing["name"].strip().title()
                new_ing = Ingredient(
                    id=_uuid_mod.uuid4(),
                    name=name_title,
                    name_lower=name_title.lower(),
                    aliases=[],
                )
                db.add(new_ing)
                # Flush to get ID
                db.flush()
                db_ingredients[name] = new_ing
                out.append({
                    "ingredient_id": str(new_ing.id),
                    "name": new_ing.name,
                    "qty": ing.get("qty"),
                    "unit": ing.get("unit", ""),
                })
                logger.debug("RecipeIngestion: created unmapped ingredient %r", new_ing.name)

        return out
