"""
Inventory Service - Manages character inventory, equipment, and consumable usage.

This service provides an orchestration layer on top of the Character model's
inventory methods, adding:
- Input validation and error handling
- Equipment slot validation (weapon vs armor slots)
- Level and class requirement checks
- Consumable effect application
- Integration with CharacterService for persistence

Usage:
    from app.services.inventory_service import get_inventory_service

    inventory_service = get_inventory_service()
    inventory_service.equip_item(character, item, "weapon", user_id)
    inventory_service.use_consumable(character, "health_potion_small", user_id)
"""

from typing import List, Optional, Dict, Any, Tuple
from dataclasses import dataclass

from app.models.character import Character
from app.models.items import Item
from app.models.effects import Effect
from app.models.enums import ItemType, EffectType
from app.services.character_service import get_character_service, CharacterService
from app.utils.logging import get_logger

logger = get_logger(__file__)


# =============================================================================
# Custom Exceptions
# =============================================================================

class InventoryError(Exception):
    """Base exception for inventory operations."""
    pass


class ItemNotFoundError(InventoryError):
    """Raised when an item is not found in the character's inventory."""
    pass


class CannotEquipError(InventoryError):
    """Raised when an item cannot be equipped (wrong slot, level requirement, etc.)."""
    pass


class InvalidSlotError(InventoryError):
    """Raised when an invalid equipment slot is specified."""
    pass


class CannotUseItemError(InventoryError):
    """Raised when an item cannot be used (not consumable, etc.)."""
    pass


class InventoryFullError(InventoryError):
    """Raised when inventory capacity is exceeded."""
    pass


# =============================================================================
# Equipment Slot Configuration
# =============================================================================

# Valid equipment slots in the game
VALID_SLOTS = {
    "weapon",       # Primary weapon
    "off_hand",     # Shield or secondary weapon
    "helmet",       # Head armor
    "chest",        # Chest armor
    "gloves",       # Hand armor
    "boots",        # Foot armor
    "accessory_1",  # Ring, amulet, etc.
    "accessory_2",  # Secondary accessory
}

# Map item types to allowed slots
ITEM_TYPE_SLOTS = {
    ItemType.WEAPON: {"weapon", "off_hand"},
    ItemType.ARMOR: {"helmet", "chest", "gloves", "boots"},
}

# Maximum inventory size (0 = unlimited)
MAX_INVENTORY_SIZE = 100


# =============================================================================
# Consumable Effect Result
# =============================================================================

@dataclass
class ConsumableResult:
    """Result of using a consumable item."""

    item_name: str
    effects_applied: List[Dict[str, Any]]
    hp_restored: int = 0
    mp_restored: int = 0
    message: str = ""

    def to_dict(self) -> Dict[str, Any]:
        """Serialize to dictionary for API response."""
        return {
            "item_name": self.item_name,
            "effects_applied": self.effects_applied,
            "hp_restored": self.hp_restored,
            "mp_restored": self.mp_restored,
            "message": self.message,
        }


# =============================================================================
# Inventory Service
# =============================================================================

class InventoryService:
    """
    Service for managing character inventory and equipment.

    This service wraps the Character model's inventory methods with additional
    validation, error handling, and persistence integration.

    All methods that modify state will persist changes via CharacterService.
    """

    def __init__(self, character_service: Optional[CharacterService] = None):
        """
        Initialize inventory service.

        Args:
            character_service: Optional CharacterService instance (uses global if not provided)
        """
        self._character_service = character_service
        logger.info("InventoryService initialized")

    @property
    def character_service(self) -> CharacterService:
        """Get CharacterService instance (lazy-loaded)."""
        if self._character_service is None:
            self._character_service = get_character_service()
        return self._character_service

    # =========================================================================
    # Read Operations
    # =========================================================================

    def get_inventory(self, character: Character) -> List[Item]:
        """
        Get all items in character's inventory.

        Args:
            character: Character instance

        Returns:
            List of Item objects in inventory
        """
        return list(character.inventory)

    def get_equipped_items(self, character: Character) -> Dict[str, Item]:
        """
        Get all equipped items.

        Args:
            character: Character instance

        Returns:
            Dictionary mapping slot names to equipped Item objects
        """
        return dict(character.equipped)

    def get_item_by_id(self, character: Character, item_id: str) -> Optional[Item]:
        """
        Find an item in inventory by ID.

        Args:
            character: Character instance
            item_id: Item ID to find

        Returns:
            Item if found, None otherwise
        """
        for item in character.inventory:
            if item.item_id == item_id:
                return item
        return None

    def get_equipped_item(self, character: Character, slot: str) -> Optional[Item]:
        """
        Get the item equipped in a specific slot.

        Args:
            character: Character instance
            slot: Equipment slot name

        Returns:
            Item if slot is occupied, None otherwise
        """
        return character.equipped.get(slot)

    def get_inventory_count(self, character: Character) -> int:
        """
        Get the number of items in inventory.

        Args:
            character: Character instance

        Returns:
            Number of items in inventory
        """
        return len(character.inventory)

    # =========================================================================
    # Add/Remove Operations
    # =========================================================================

    def add_item(
        self,
        character: Character,
        item: Item,
        user_id: str,
        save: bool = True
    ) -> None:
        """
        Add an item to character's inventory.

        Args:
            character: Character instance
            item: Item to add
            user_id: User ID for persistence authorization
            save: Whether to persist changes (default True)

        Raises:
            InventoryFullError: If inventory is at maximum capacity
        """
        # Check inventory capacity
        if MAX_INVENTORY_SIZE > 0 and len(character.inventory) >= MAX_INVENTORY_SIZE:
            raise InventoryFullError(
                f"Inventory is full ({MAX_INVENTORY_SIZE} items max)"
            )

        # Add to inventory
        character.add_item(item)

        logger.info("Item added to inventory",
                   character_id=character.character_id,
                   item_id=item.item_id,
                   item_name=item.get_display_name())

        # Persist changes
        if save:
            self.character_service.update_character(character, user_id)

    def remove_item(
        self,
        character: Character,
        item_id: str,
        user_id: str,
        save: bool = True
    ) -> Item:
        """
        Remove an item from character's inventory.

        Args:
            character: Character instance
            item_id: ID of item to remove
            user_id: User ID for persistence authorization
            save: Whether to persist changes (default True)

        Returns:
            The removed Item

        Raises:
            ItemNotFoundError: If item is not in inventory
        """
        # Find item first (for better error message)
        item = self.get_item_by_id(character, item_id)
        if item is None:
            raise ItemNotFoundError(f"Item not found in inventory: {item_id}")

        # Remove from inventory
        removed_item = character.remove_item(item_id)

        logger.info("Item removed from inventory",
                   character_id=character.character_id,
                   item_id=item_id,
                   item_name=item.get_display_name())

        # Persist changes
        if save:
            self.character_service.update_character(character, user_id)

        return removed_item

    def drop_item(
        self,
        character: Character,
        item_id: str,
        user_id: str
    ) -> Item:
        """
        Drop an item (remove permanently with no return).

        This is an alias for remove_item, but semantically indicates
        the item is being discarded rather than transferred.

        Args:
            character: Character instance
            item_id: ID of item to drop
            user_id: User ID for persistence authorization

        Returns:
            The dropped Item (for logging/notification purposes)

        Raises:
            ItemNotFoundError: If item is not in inventory
        """
        return self.remove_item(character, item_id, user_id, save=True)

    # =========================================================================
    # Equipment Operations
    # =========================================================================

    def equip_item(
        self,
        character: Character,
        item_id: str,
        slot: str,
        user_id: str
    ) -> Optional[Item]:
        """
        Equip an item from inventory to a specific slot.

        Args:
            character: Character instance
            item_id: ID of item to equip (must be in inventory)
            slot: Equipment slot to use
            user_id: User ID for persistence authorization

        Returns:
            Previously equipped item in that slot (or None)

        Raises:
            ItemNotFoundError: If item is not in inventory
            InvalidSlotError: If slot name is invalid
            CannotEquipError: If item cannot be equipped (wrong type, level, etc.)
        """
        # Validate slot
        if slot not in VALID_SLOTS:
            raise InvalidSlotError(
                f"Invalid equipment slot: '{slot}'. "
                f"Valid slots: {', '.join(sorted(VALID_SLOTS))}"
            )

        # Find item in inventory
        item = self.get_item_by_id(character, item_id)
        if item is None:
            raise ItemNotFoundError(f"Item not found in inventory: {item_id}")

        # Validate item can be equipped
        self._validate_equip(character, item, slot)

        # Perform equip (Character.equip_item handles inventory management)
        previous_item = character.equip_item(item, slot)

        logger.info("Item equipped",
                   character_id=character.character_id,
                   item_id=item_id,
                   slot=slot,
                   previous_item=previous_item.item_id if previous_item else None)

        # Persist changes
        self.character_service.update_character(character, user_id)

        return previous_item

    def unequip_item(
        self,
        character: Character,
        slot: str,
        user_id: str
    ) -> Optional[Item]:
        """
        Unequip an item from a specific slot (returns to inventory).

        Args:
            character: Character instance
            slot: Equipment slot to unequip from
            user_id: User ID for persistence authorization

        Returns:
            The unequipped Item (or None if slot was empty)

        Raises:
            InvalidSlotError: If slot name is invalid
            InventoryFullError: If inventory is full and cannot receive the item
        """
        # Validate slot
        if slot not in VALID_SLOTS:
            raise InvalidSlotError(
                f"Invalid equipment slot: '{slot}'. "
                f"Valid slots: {', '.join(sorted(VALID_SLOTS))}"
            )

        # Check if slot has an item
        equipped_item = character.equipped.get(slot)
        if equipped_item is None:
            logger.debug("Unequip from empty slot",
                        character_id=character.character_id,
                        slot=slot)
            return None

        # Check inventory capacity (item will return to inventory)
        if MAX_INVENTORY_SIZE > 0 and len(character.inventory) >= MAX_INVENTORY_SIZE:
            raise InventoryFullError(
                "Cannot unequip: inventory is full"
            )

        # Perform unequip (Character.unequip_item handles inventory management)
        unequipped_item = character.unequip_item(slot)

        logger.info("Item unequipped",
                   character_id=character.character_id,
                   item_id=unequipped_item.item_id if unequipped_item else None,
                   slot=slot)

        # Persist changes
        self.character_service.update_character(character, user_id)

        return unequipped_item

    def swap_equipment(
        self,
        character: Character,
        item_id: str,
        slot: str,
        user_id: str
    ) -> Optional[Item]:
        """
        Swap equipment: equip item and return the previous item.

        This is semantically the same as equip_item but makes the swap
        intention explicit.

        Args:
            character: Character instance
            item_id: ID of item to equip
            slot: Equipment slot to use
            user_id: User ID for persistence authorization

        Returns:
            Previously equipped item (or None)
        """
        return self.equip_item(character, item_id, slot, user_id)

    def _validate_equip(self, character: Character, item: Item, slot: str) -> None:
        """
        Validate that an item can be equipped to a slot.

        Args:
            character: Character instance
            item: Item to validate
            slot: Target equipment slot

        Raises:
            CannotEquipError: If item cannot be equipped
        """
        # Check item type is equippable
        if item.item_type not in ITEM_TYPE_SLOTS:
            raise CannotEquipError(
                f"Cannot equip {item.item_type.value} items. "
                f"Only weapons and armor can be equipped."
            )

        # Check slot matches item type
        allowed_slots = ITEM_TYPE_SLOTS[item.item_type]
        if slot not in allowed_slots:
            raise CannotEquipError(
                f"Cannot equip {item.item_type.value} to '{slot}' slot. "
                f"Allowed slots: {', '.join(sorted(allowed_slots))}"
            )

        # Check level requirement
        if not item.can_equip(character.level, character.class_id):
            if character.level < item.required_level:
                raise CannotEquipError(
                    f"Cannot equip '{item.get_display_name()}': "
                    f"requires level {item.required_level} (you are level {character.level})"
                )
            if item.required_class and item.required_class != character.class_id:
                raise CannotEquipError(
                    f"Cannot equip '{item.get_display_name()}': "
                    f"requires class '{item.required_class}'"
                )

    # =========================================================================
    # Consumable Operations
    # =========================================================================

    def use_consumable(
        self,
        character: Character,
        item_id: str,
        user_id: str,
        current_hp: Optional[int] = None,
        max_hp: Optional[int] = None,
        current_mp: Optional[int] = None,
        max_mp: Optional[int] = None
    ) -> ConsumableResult:
        """
        Use a consumable item and apply its effects.

        For HP/MP restoration effects, provide current/max values to calculate
        actual restoration (clamped to max). If not provided, uses character's
        computed max_hp from stats.

        Note: Outside of combat, characters are always at full HP. During combat,
        HP tracking is handled by the combat system and current_hp should be passed.

        Args:
            character: Character instance
            item_id: ID of consumable to use
            user_id: User ID for persistence authorization
            current_hp: Current HP (for healing calculations)
            max_hp: Maximum HP (for healing cap)
            current_mp: Current MP (for mana restore calculations)
            max_mp: Maximum MP (for mana restore cap)

        Returns:
            ConsumableResult with details of effects applied

        Raises:
            ItemNotFoundError: If item is not in inventory
            CannotUseItemError: If item is not a consumable
        """
        # Find item in inventory
        item = self.get_item_by_id(character, item_id)
        if item is None:
            raise ItemNotFoundError(f"Item not found in inventory: {item_id}")

        # Validate item is consumable
        if not item.is_consumable():
            raise CannotUseItemError(
                f"Cannot use '{item.get_display_name()}': not a consumable item"
            )

        # Use character computed values if not provided
        if max_hp is None:
            max_hp = character.max_hp
        if current_hp is None:
            current_hp = max_hp  # Outside combat, assume full HP

        # MP handling (if character has MP system)
        effective_stats = character.get_effective_stats()
        if max_mp is None:
            max_mp = getattr(effective_stats, 'magic_points', 100)
        if current_mp is None:
            current_mp = max_mp  # Outside combat, assume full MP

        # Apply effects
        result = self._apply_consumable_effects(
            item, current_hp, max_hp, current_mp, max_mp
        )

        # Remove consumable from inventory (it's used up)
        character.remove_item(item_id)

        logger.info("Consumable used",
                   character_id=character.character_id,
                   item_id=item_id,
                   item_name=item.get_display_name(),
                   hp_restored=result.hp_restored,
                   mp_restored=result.mp_restored)

        # Persist changes
        self.character_service.update_character(character, user_id)

        return result

    def _apply_consumable_effects(
        self,
        item: Item,
        current_hp: int,
        max_hp: int,
        current_mp: int,
        max_mp: int
    ) -> ConsumableResult:
        """
        Apply consumable effects and calculate results.

        Args:
            item: Consumable item
            current_hp: Current HP
            max_hp: Maximum HP
            current_mp: Current MP
            max_mp: Maximum MP

        Returns:
            ConsumableResult with effect details
        """
        effects_applied = []
        total_hp_restored = 0
        total_mp_restored = 0
        messages = []

        for effect in item.effects_on_use:
            effect_result = {
                "effect_name": effect.name,
                "effect_type": effect.effect_type.value,
            }

            if effect.effect_type == EffectType.HOT:
                # Instant heal (for potions, treat HOT as instant outside combat)
                heal_amount = effect.power * effect.stacks
                actual_heal = min(heal_amount, max_hp - current_hp)
                current_hp += actual_heal
                total_hp_restored += actual_heal

                effect_result["value"] = actual_heal
                effect_result["message"] = f"Restored {actual_heal} HP"
                messages.append(f"Restored {actual_heal} HP")

            elif effect.effect_type == EffectType.BUFF:
                # Stat buff - would be applied in combat context
                stat_name = effect.stat_affected.value if effect.stat_affected else "unknown"
                effect_result["stat_affected"] = stat_name
                effect_result["modifier"] = effect.power
                effect_result["duration"] = effect.duration
                effect_result["message"] = f"+{effect.power} {stat_name} for {effect.duration} turns"
                messages.append(f"+{effect.power} {stat_name}")

            elif effect.effect_type == EffectType.SHIELD:
                # Apply shield effect
                shield_power = effect.power * effect.stacks
                effect_result["shield_power"] = shield_power
                effect_result["duration"] = effect.duration
                effect_result["message"] = f"Shield for {shield_power} damage"
                messages.append(f"Shield: {shield_power}")

            else:
                # Other effect types (DOT, DEBUFF, STUN - unusual for consumables)
                effect_result["power"] = effect.power
                effect_result["duration"] = effect.duration
                effect_result["message"] = f"{effect.name} applied"

            effects_applied.append(effect_result)

        # Build summary message
        summary = f"Used {item.get_display_name()}"
        if messages:
            summary += f": {', '.join(messages)}"

        return ConsumableResult(
            item_name=item.get_display_name(),
            effects_applied=effects_applied,
            hp_restored=total_hp_restored,
            mp_restored=total_mp_restored,
            message=summary,
        )

    def use_consumable_in_combat(
        self,
        character: Character,
        item_id: str,
        user_id: str,
        current_hp: int,
        max_hp: int,
        current_mp: int = 0,
        max_mp: int = 0
    ) -> Tuple[ConsumableResult, List[Effect]]:
        """
        Use a consumable during combat.

        Returns both the result summary and a list of Effect objects that
        should be applied to the combatant for duration-based effects.

        Args:
            character: Character instance
            item_id: ID of consumable to use
            user_id: User ID for persistence authorization
            current_hp: Current combat HP
            max_hp: Maximum combat HP
            current_mp: Current combat MP
            max_mp: Maximum combat MP

        Returns:
            Tuple of (ConsumableResult, List[Effect]) for combat system

        Raises:
            ItemNotFoundError: If item not in inventory
            CannotUseItemError: If item is not consumable
        """
        # Find item
        item = self.get_item_by_id(character, item_id)
        if item is None:
            raise ItemNotFoundError(f"Item not found in inventory: {item_id}")

        if not item.is_consumable():
            raise CannotUseItemError(
                f"Cannot use '{item.get_display_name()}': not a consumable"
            )

        # Separate instant effects from duration effects
        instant_effects = []
        duration_effects = []

        for effect in item.effects_on_use:
            # HOT effects in combat should tick, not instant heal
            if effect.duration > 1 or effect.effect_type in [
                EffectType.BUFF, EffectType.DEBUFF, EffectType.DOT,
                EffectType.HOT, EffectType.SHIELD, EffectType.STUN
            ]:
                # Copy effect for combat tracking
                combat_effect = Effect(
                    effect_id=f"{item.item_id}_{effect.effect_id}",
                    name=effect.name,
                    effect_type=effect.effect_type,
                    duration=effect.duration,
                    power=effect.power,
                    stat_affected=effect.stat_affected,
                    stacks=effect.stacks,
                    max_stacks=effect.max_stacks,
                    source=item.item_id,
                )
                duration_effects.append(combat_effect)
            else:
                instant_effects.append(effect)

        # Calculate instant effect results
        result = self._apply_consumable_effects(
            item, current_hp, max_hp, current_mp, max_mp
        )

        # Remove from inventory
        character.remove_item(item_id)

        logger.info("Consumable used in combat",
                   character_id=character.character_id,
                   item_id=item_id,
                   duration_effects=len(duration_effects))

        # Persist
        self.character_service.update_character(character, user_id)

        return result, duration_effects

    # =========================================================================
    # Bulk Operations
    # =========================================================================

    def add_items(
        self,
        character: Character,
        items: List[Item],
        user_id: str
    ) -> int:
        """
        Add multiple items to inventory (e.g., loot drop).

        Args:
            character: Character instance
            items: List of items to add
            user_id: User ID for persistence

        Returns:
            Number of items actually added

        Note:
            Stops adding if inventory becomes full. Does not raise error
            for partial success.
        """
        added_count = 0
        for item in items:
            try:
                self.add_item(character, item, user_id, save=False)
                added_count += 1
            except InventoryFullError:
                logger.warning("Inventory full, dropping remaining loot",
                             character_id=character.character_id,
                             items_dropped=len(items) - added_count)
                break

        # Save once after all items added
        if added_count > 0:
            self.character_service.update_character(character, user_id)

        return added_count

    def get_items_by_type(
        self,
        character: Character,
        item_type: ItemType
    ) -> List[Item]:
        """
        Get all inventory items of a specific type.

        Args:
            character: Character instance
            item_type: Type to filter by

        Returns:
            List of matching items
        """
        return [
            item for item in character.inventory
            if item.item_type == item_type
        ]

    def get_equippable_items(
        self,
        character: Character,
        slot: Optional[str] = None
    ) -> List[Item]:
        """
        Get all items that can be equipped.

        Args:
            character: Character instance
            slot: Optional slot to filter by

        Returns:
            List of equippable items (optionally filtered by slot)
        """
        equippable = []
        for item in character.inventory:
            # Skip non-equippable types
            if item.item_type not in ITEM_TYPE_SLOTS:
                continue

            # Skip items that don't meet requirements
            if not item.can_equip(character.level, character.class_id):
                continue

            # Filter by slot if specified
            if slot:
                allowed_slots = ITEM_TYPE_SLOTS[item.item_type]
                if slot not in allowed_slots:
                    continue

            equippable.append(item)

        return equippable


# =============================================================================
# Global Instance
# =============================================================================

_service_instance: Optional[InventoryService] = None


def get_inventory_service() -> InventoryService:
    """
    Get the global InventoryService instance.

    Returns:
        Singleton InventoryService instance
    """
    global _service_instance
    if _service_instance is None:
        _service_instance = InventoryService()
    return _service_instance
