# ================================================================
# services/procurement/app/services/recipe_explosion_service.py
# ================================================================
"""
Recipe Explosion Service - Multi-level BOM (Bill of Materials) explosion
Converts finished product demand into raw ingredient requirements for locally-produced items
"""

import uuid
import structlog
from typing import Dict, List, Optional, Set, Tuple
from decimal import Decimal
from collections import defaultdict

from shared.clients.recipes_client import RecipesServiceClient
from shared.clients.inventory_client import InventoryServiceClient
from app.core.config import settings

logger = structlog.get_logger()


class CircularDependencyError(Exception):
    """Raised when a circular dependency is detected in recipe tree"""
    pass


class RecipeExplosionService:
    """
    Service for exploding finished product requirements into raw ingredient requirements.
    Supports multi-level BOM explosion (recipes that reference other recipes).
    """

    def __init__(
        self,
        recipes_client: RecipesServiceClient,
        inventory_client: InventoryServiceClient
    ):
        self.recipes_client = recipes_client
        self.inventory_client = inventory_client
        self.max_depth = settings.MAX_BOM_EXPLOSION_DEPTH  # Default: 5 levels

    async def explode_requirements(
        self,
        tenant_id: uuid.UUID,
        requirements: List[Dict]
    ) -> Tuple[List[Dict], Dict]:
        """
        Explode locally-produced finished products into raw ingredient requirements.

        Args:
            tenant_id: Tenant ID
            requirements: List of procurement requirements (can mix locally-produced and purchased items)

        Returns:
            Tuple of (exploded_requirements, explosion_metadata)
            - exploded_requirements: Final list with locally-produced items exploded to ingredients
            - explosion_metadata: Details about the explosion process
        """
        logger.info("Starting recipe explosion",
                   tenant_id=str(tenant_id),
                   total_requirements=len(requirements))

        # Separate locally-produced from purchased items
        locally_produced = []
        purchased_direct = []

        for req in requirements:
            if req.get('is_locally_produced', False) and req.get('recipe_id'):
                locally_produced.append(req)
            else:
                purchased_direct.append(req)

        logger.info("Requirements categorized",
                   locally_produced_count=len(locally_produced),
                   purchased_direct_count=len(purchased_direct))

        # If no locally-produced items, return as-is
        if not locally_produced:
            return requirements, {'explosion_performed': False, 'message': 'No locally-produced items'}

        # Explode locally-produced items
        exploded_ingredients = await self._explode_locally_produced_batch(
            tenant_id=tenant_id,
            locally_produced_requirements=locally_produced
        )

        # Combine purchased items with exploded ingredients
        final_requirements = purchased_direct + exploded_ingredients

        # Create metadata
        metadata = {
            'explosion_performed': True,
            'locally_produced_items_count': len(locally_produced),
            'purchased_direct_count': len(purchased_direct),
            'exploded_ingredients_count': len(exploded_ingredients),
            'total_final_requirements': len(final_requirements)
        }

        logger.info("Recipe explosion completed", **metadata)

        return final_requirements, metadata

    async def _explode_locally_produced_batch(
        self,
        tenant_id: uuid.UUID,
        locally_produced_requirements: List[Dict]
    ) -> List[Dict]:
        """
        Explode a batch of locally-produced requirements into raw ingredients.

        Uses multi-level explosion (recursive) to handle recipes that reference other recipes.
        """
        # Aggregated ingredient requirements
        aggregated_ingredients: Dict[str, Dict] = {}

        for req in locally_produced_requirements:
            product_id = req['product_id']
            recipe_id = req['recipe_id']
            required_quantity = Decimal(str(req['required_quantity']))

            logger.info("Exploding locally-produced item",
                       product_id=str(product_id),
                       recipe_id=str(recipe_id),
                       quantity=float(required_quantity))

            try:
                # Explode this recipe (recursive)
                ingredients = await self._explode_recipe_recursive(
                    tenant_id=tenant_id,
                    recipe_id=recipe_id,
                    required_quantity=required_quantity,
                    current_depth=0,
                    visited_recipes=set(),
                    parent_requirement=req
                )

                # Aggregate ingredients
                for ingredient in ingredients:
                    ingredient_id = ingredient['ingredient_id']
                    quantity = ingredient['quantity']

                    if ingredient_id in aggregated_ingredients:
                        # Add to existing
                        existing_qty = Decimal(str(aggregated_ingredients[ingredient_id]['quantity']))
                        aggregated_ingredients[ingredient_id]['quantity'] = float(existing_qty + quantity)
                    else:
                        # New ingredient
                        aggregated_ingredients[ingredient_id] = ingredient

            except CircularDependencyError as e:
                logger.error("Circular dependency detected",
                           product_id=str(product_id),
                           recipe_id=str(recipe_id),
                           error=str(e))
                # Skip this item or handle gracefully
                continue
            except Exception as e:
                logger.error("Error exploding recipe",
                           product_id=str(product_id),
                           recipe_id=str(recipe_id),
                           error=str(e))
                continue

        # Convert aggregated dict to list
        return list(aggregated_ingredients.values())

    async def _explode_recipe_recursive(
        self,
        tenant_id: uuid.UUID,
        recipe_id: uuid.UUID,
        required_quantity: Decimal,
        current_depth: int,
        visited_recipes: Set[str],
        parent_requirement: Dict
    ) -> List[Dict]:
        """
        Recursively explode a recipe into raw ingredients.

        Args:
            tenant_id: Tenant ID
            recipe_id: Recipe to explode
            required_quantity: How much of the finished product is needed
            current_depth: Current recursion depth (to prevent infinite loops)
            visited_recipes: Set of recipe IDs already visited (circular dependency detection)
            parent_requirement: The parent procurement requirement

        Returns:
            List of ingredient requirements (raw materials only)
        """
        # Check depth limit
        if current_depth >= self.max_depth:
            logger.warning("Max explosion depth reached",
                          recipe_id=str(recipe_id),
                          max_depth=self.max_depth)
            raise RecursionError(f"Max BOM explosion depth ({self.max_depth}) exceeded")

        # Check circular dependency
        recipe_id_str = str(recipe_id)
        if recipe_id_str in visited_recipes:
            logger.error("Circular dependency detected",
                        recipe_id=recipe_id_str,
                        visited_recipes=list(visited_recipes))
            raise CircularDependencyError(
                f"Circular dependency detected: recipe {recipe_id_str} references itself"
            )

        # Add to visited set
        visited_recipes.add(recipe_id_str)

        logger.debug("Exploding recipe",
                    recipe_id=recipe_id_str,
                    required_quantity=float(required_quantity),
                    depth=current_depth)

        # Fetch recipe from Recipes Service
        recipe_data = await self.recipes_client.get_recipe_by_id(
            tenant_id=str(tenant_id),
            recipe_id=recipe_id_str
        )

        if not recipe_data:
            logger.error("Recipe not found", recipe_id=recipe_id_str)
            raise ValueError(f"Recipe {recipe_id_str} not found")

        # Calculate scale factor
        recipe_yield_quantity = Decimal(str(recipe_data.get('yield_quantity', 1)))
        scale_factor = required_quantity / recipe_yield_quantity

        logger.debug("Recipe scale calculation",
                    recipe_yield=float(recipe_yield_quantity),
                    required=float(required_quantity),
                    scale_factor=float(scale_factor))

        # Get recipe ingredients
        ingredients = recipe_data.get('ingredients', [])
        if not ingredients:
            logger.warning("Recipe has no ingredients", recipe_id=recipe_id_str)
            return []

        # Process each ingredient
        exploded_ingredients = []

        for recipe_ingredient in ingredients:
            ingredient_id = uuid.UUID(recipe_ingredient['ingredient_id'])
            ingredient_quantity = Decimal(str(recipe_ingredient['quantity']))
            scaled_quantity = ingredient_quantity * scale_factor

            logger.debug("Processing recipe ingredient",
                        ingredient_id=str(ingredient_id),
                        base_quantity=float(ingredient_quantity),
                        scaled_quantity=float(scaled_quantity))

            # Check if this ingredient is ALSO locally produced (nested recipe)
            ingredient_info = await self._get_ingredient_info(tenant_id, ingredient_id)

            if ingredient_info and ingredient_info.get('produced_locally') and ingredient_info.get('recipe_id'):
                # Recursive case: This ingredient has its own recipe
                logger.info("Ingredient is locally produced, recursing",
                           ingredient_id=str(ingredient_id),
                           nested_recipe_id=ingredient_info['recipe_id'],
                           depth=current_depth + 1)

                nested_ingredients = await self._explode_recipe_recursive(
                    tenant_id=tenant_id,
                    recipe_id=uuid.UUID(ingredient_info['recipe_id']),
                    required_quantity=scaled_quantity,
                    current_depth=current_depth + 1,
                    visited_recipes=visited_recipes.copy(),  # Pass a copy to allow sibling branches
                    parent_requirement=parent_requirement
                )

                exploded_ingredients.extend(nested_ingredients)

            else:
                # Base case: This is a raw ingredient (not produced locally)
                exploded_ingredients.append({
                    'ingredient_id': str(ingredient_id),
                    'product_id': str(ingredient_id),
                    'quantity': float(scaled_quantity),
                    'unit': recipe_ingredient.get('unit'),
                    'is_locally_produced': False,
                    'recipe_id': None,
                    'parent_requirement_id': parent_requirement.get('id'),
                    'bom_explosion_level': current_depth + 1,
                    'source_recipe_id': recipe_id_str
                })

        return exploded_ingredients

    async def _get_ingredient_info(
        self,
        tenant_id: uuid.UUID,
        ingredient_id: uuid.UUID
    ) -> Optional[Dict]:
        """
        Get ingredient info from Inventory Service to check if it's locally produced.

        Args:
            tenant_id: Tenant ID
            ingredient_id: Ingredient/Product ID

        Returns:
            Dict with ingredient info including produced_locally and recipe_id flags
        """
        try:
            ingredient = await self.inventory_client.get_ingredient_by_id(
                tenant_id=str(tenant_id),
                ingredient_id=str(ingredient_id)
            )

            if not ingredient:
                return None

            return {
                'id': ingredient.get('id'),
                'name': ingredient.get('name'),
                'produced_locally': ingredient.get('produced_locally', False),
                'recipe_id': ingredient.get('recipe_id')
            }

        except Exception as e:
            logger.error("Error fetching ingredient info",
                        ingredient_id=str(ingredient_id),
                        error=str(e))
            return None

    async def validate_recipe_explosion(
        self,
        tenant_id: uuid.UUID,
        recipe_id: uuid.UUID
    ) -> Dict:
        """
        Validate if a recipe can be safely exploded (check for circular dependencies).

        Args:
            tenant_id: Tenant ID
            recipe_id: Recipe to validate

        Returns:
            Dict with validation results
        """
        try:
            await self._explode_recipe_recursive(
                tenant_id=tenant_id,
                recipe_id=recipe_id,
                required_quantity=Decimal("1"),  # Test with 1 unit
                current_depth=0,
                visited_recipes=set(),
                parent_requirement={}
            )

            return {
                'valid': True,
                'message': 'Recipe can be safely exploded'
            }

        except CircularDependencyError as e:
            return {
                'valid': False,
                'error': 'circular_dependency',
                'message': str(e)
            }

        except RecursionError as e:
            return {
                'valid': False,
                'error': 'max_depth_exceeded',
                'message': str(e)
            }

        except Exception as e:
            return {
                'valid': False,
                'error': 'unknown',
                'message': str(e)
            }
