"""
FreeExile Inventory, Personal Stash & Premium Merchant Stash Service.
Authoritative management of character bags, multi-tab storage, and MegaShop-gated merchant stalls.
Follows Engineering Standards 2026: Soft cap <= 350 lines, Zero-trust server authority.
"""

from __future__ import annotations
import time
from typing import Dict, List, Optional, Tuple, Any

from .inventory_types import (
    ItemType,
    EquipmentSlot,
    InventoryItem,
    CharacterInventory,
    PersonalStash,
    MerchantStashTab,
    PlayerStorageContainer,
)
from .inventory_helpers import (
    try_merge_stack,
    find_first_empty_slot,
    is_equipment_compatible,
    create_split_item,
    create_default_storage_container,
)
from .spatial_grid_types import get_default_item_dimensions
from shop.shop_service import MegaShopService


class InventoryService:
    """Authoritative service governing Character Inventory, Multi-Tab Stashes, and Premium Merchant Stash."""

    PREMIUM_MERCHANT_STASH_PRODUCT_ID = "item_premium_merchant_stash_tab"

    def __init__(
        self,
        shop_service: Optional[MegaShopService] = None,
        consignment_vault: Optional[Any] = None
    ) -> None:
        self.shop_service = shop_service or MegaShopService()
        self.consignment_vault = consignment_vault
        self._containers: Dict[Tuple[str, str], PlayerStorageContainer] = {}

    def get_or_create_character_storage(
        self, account_id: str, character_id: str
    ) -> PlayerStorageContainer:
        key = (account_id, character_id)
        if key not in self._containers:
            self._containers[key] = create_default_storage_container(account_id, character_id)

        container = self._containers[key]
        if not container.merchant_stash.is_unlocked and self.shop_service:
            player = self.shop_service.get_player(account_id)
            if player.product_purchase_counts.get(self.PREMIUM_MERCHANT_STASH_PRODUCT_ID, 0) > 0:
                container.merchant_stash.is_unlocked = True
                container.merchant_stash.unlocked_at = time.time()

        return container

    def get_character_inventory(self, account_id: str, character_id: str) -> CharacterInventory:
        return self.get_or_create_character_storage(account_id, character_id).inventory

    def get_personal_stash(self, account_id: str, character_id: str) -> PersonalStash:
        return self.get_or_create_character_storage(account_id, character_id).stash

    def get_merchant_stash(self, account_id: str, character_id: str) -> MerchantStashTab:
        return self.get_or_create_character_storage(account_id, character_id).merchant_stash

    # --- INVENTORY OPERATIONS ---

    def add_item_to_inventory(
        self,
        account_id: str,
        character_id: str,
        item: InventoryItem,
        preferred_slot: Optional[int] = None
    ) -> Tuple[bool, str, Optional[int]]:
        inv = self.get_character_inventory(account_id, character_id)

        if item.width <= 1 and item.height <= 1 and item.item_type in (ItemType.WEAPON, ItemType.ARMOR):
            dim = get_default_item_dimensions(item.item_id, item.item_type)
            item.width, item.height = dim.width, dim.height

        merged, s_idx = try_merge_stack(inv.slots, item)
        if merged and s_idx is not None:
            return True, f"Stacked {item.stack_count} into slot {s_idx}", s_idx

        if preferred_slot is not None and 0 <= preferred_slot < inv.capacity and preferred_slot not in inv.slots:
            inv.slots[preferred_slot] = item
            return True, f"Item placed into slot {preferred_slot}", preferred_slot

        empty_slot = find_first_empty_slot(inv.slots, inv.capacity)
        if empty_slot is not None:
            inv.slots[empty_slot] = item
            return True, f"Item placed into slot {empty_slot}", empty_slot

        return False, "Inventory bag is full", None

    def remove_item_from_inventory(
        self, account_id: str, character_id: str, slot: int, count: int = 1
    ) -> Tuple[bool, str, Optional[InventoryItem]]:
        inv = self.get_character_inventory(account_id, character_id)
        if slot not in inv.slots:
            return False, f"Slot {slot} is empty", None
        item = inv.slots[slot]
        if item.stack_count > count:
            item.stack_count -= count
            return True, f"Extracted {count} items from slot {slot}", create_split_item(item, count)
        del inv.slots[slot]
        return True, f"Removed item from slot {slot}", item

    def move_inventory_item(
        self, account_id: str, character_id: str, from_slot: int, to_slot: int
    ) -> Tuple[bool, str]:
        inv = self.get_character_inventory(account_id, character_id)
        if from_slot not in inv.slots:
            return False, f"Source slot {from_slot} is empty"
        if to_slot < 0 or to_slot >= inv.capacity:
            return False, f"Target slot {to_slot} out of range"
        if from_slot == to_slot:
            return True, "No-op"

        src_item = inv.slots.pop(from_slot)
        if to_slot in inv.slots:
            inv.slots[from_slot] = inv.slots[to_slot]
            inv.slots[to_slot] = src_item
            return True, f"Swapped slot {from_slot} with {to_slot}"

        inv.slots[to_slot] = src_item
        return True, f"Moved item to slot {to_slot}"

    def equip_item(
        self, account_id: str, character_id: str, inv_slot: int, equip_slot: EquipmentSlot
    ) -> Tuple[bool, str]:
        inv = self.get_character_inventory(account_id, character_id)
        if inv_slot not in inv.slots:
            return False, f"Inventory slot {inv_slot} is empty"

        item = inv.slots[inv_slot]
        if not is_equipment_compatible(item.item_type, equip_slot):
            return False, f"Cannot equip {item.item_type.value} into {equip_slot.value}"

        del inv.slots[inv_slot]
        if equip_slot in inv.equipment:
            inv.slots[inv_slot] = inv.equipment[equip_slot]

        inv.equipment[equip_slot] = item
        return True, f"Equipped {item.name} to {equip_slot.value}"

    def unequip_item(
        self,
        account_id: str,
        character_id: str,
        equip_slot: EquipmentSlot,
        target_inv_slot: Optional[int] = None
    ) -> Tuple[bool, str]:
        inv = self.get_character_inventory(account_id, character_id)
        if equip_slot not in inv.equipment:
            return False, f"No gear equipped in {equip_slot.value}"

        item = inv.equipment[equip_slot]
        dest_slot = target_inv_slot
        if dest_slot is None or dest_slot in inv.slots or dest_slot >= inv.capacity:
            dest_slot = find_first_empty_slot(inv.slots, inv.capacity)

        if dest_slot is None:
            return False, "Cannot unequip: inventory is full"

        del inv.equipment[equip_slot]
        inv.slots[dest_slot] = item
        return True, f"Unequipped {item.name} into inventory slot {dest_slot}"

    # --- PERSONAL STASH OPERATIONS ---

    def deposit_to_stash(
        self, account_id: str, character_id: str, inv_slot: int, tab_id: str, stash_slot: Optional[int] = None
    ) -> Tuple[bool, str]:
        inv = self.get_character_inventory(account_id, character_id)
        stash = self.get_personal_stash(account_id, character_id)

        if inv_slot not in inv.slots:
            return False, f"Inventory slot {inv_slot} is empty"
        if tab_id not in stash.tabs:
            return False, f"Stash tab {tab_id} not found"

        tab = stash.tabs[tab_id]
        item = inv.slots[inv_slot]
        dest_slot = stash_slot if (stash_slot is not None and stash_slot not in tab.slots and stash_slot < tab.capacity) else find_first_empty_slot(tab.slots, tab.capacity)

        if dest_slot is None:
            return False, f"Stash tab {tab.name} is full"

        del inv.slots[inv_slot]
        tab.slots[dest_slot] = item
        return True, f"Deposited {item.name} to {tab.name} slot {dest_slot}"

    def withdraw_from_stash(
        self, account_id: str, character_id: str, tab_id: str, stash_slot: int, target_inv_slot: Optional[int] = None
    ) -> Tuple[bool, str]:
        stash = self.get_personal_stash(account_id, character_id)
        inv = self.get_character_inventory(account_id, character_id)

        if tab_id not in stash.tabs:
            return False, f"Stash tab {tab_id} not found"
        tab = stash.tabs[tab_id]
        if stash_slot not in tab.slots:
            return False, f"Stash slot {stash_slot} is empty"

        item = tab.slots[stash_slot]
        dest_slot = target_inv_slot if (target_inv_slot is not None and target_inv_slot not in inv.slots and target_inv_slot < inv.capacity) else find_first_empty_slot(inv.slots, inv.capacity)

        if dest_slot is None:
            return False, "Cannot withdraw: inventory is full"

        del tab.slots[stash_slot]
        inv.slots[dest_slot] = item
        return True, f"Withdrew {item.name} to inventory slot {dest_slot}"

    # --- PREMIUM MERCHANT STASH (MEGASHOP COIN UNLOCK) ---

    def is_merchant_stash_unlocked(self, account_id: str, character_id: str) -> bool:
        container = self.get_or_create_character_storage(account_id, character_id)
        if container.merchant_stash.is_unlocked:
            return True
        if self.shop_service:
            player = self.shop_service.get_player(account_id)
            if player.product_purchase_counts.get(self.PREMIUM_MERCHANT_STASH_PRODUCT_ID, 0) > 0:
                container.merchant_stash.is_unlocked = True
                container.merchant_stash.unlocked_at = time.time()
                return True
        return False

    def unlock_merchant_stash_with_coins(
        self, account_id: str, character_id: str
    ) -> Tuple[bool, str]:
        if self.is_merchant_stash_unlocked(account_id, character_id):
            return True, "Merchant stash tab is already unlocked"

        if not self.shop_service:
            return False, "MegaShop service unavailable"

        settle_result = self.shop_service.purchase_product_with_coins(
            account_id=account_id,
            product_id=self.PREMIUM_MERCHANT_STASH_PRODUCT_ID
        )
        if not settle_result.success:
            return False, f"Failed to unlock: {settle_result.error_message}"

        container = self.get_or_create_character_storage(account_id, character_id)
        container.merchant_stash.is_unlocked = True
        container.merchant_stash.unlocked_at = time.time()
        return True, "Successfully unlocked Premium Merchant Stash Tab with Gold Cores!"

    def place_item_in_merchant_stash(
        self,
        account_id: str,
        character_id: str,
        inv_slot: int,
        merchant_slot: int,
        price_currency: str,
        price_amount: int
    ) -> Tuple[bool, str]:
        if not self.is_merchant_stash_unlocked(account_id, character_id):
            return False, "Merchant Stash Tab is locked. Purchase the Premium Merchant Stash Tab in MegaShop with Gold Cores to unlock."

        if price_amount <= 0:
            return False, "Price amount must be strictly greater than 0"

        inv = self.get_character_inventory(account_id, character_id)
        merchant_tab = self.get_merchant_stash(account_id, character_id)

        if inv_slot not in inv.slots:
            return False, f"Inventory slot {inv_slot} is empty"
        if merchant_slot < 0 or merchant_slot >= merchant_tab.capacity:
            return False, f"Merchant slot {merchant_slot} out of range"
        if merchant_slot in merchant_tab.slots:
            return False, f"Merchant slot {merchant_slot} is already occupied"

        item = inv.slots.pop(inv_slot)
        item.asking_price_currency = price_currency
        item.asking_price_amount = price_amount

        if self.consignment_vault and hasattr(self.consignment_vault, "list_item_for_sale"):
            try:
                item.listing_id = self.consignment_vault.list_item_for_sale(
                    seller_id=account_id, item=item, price_currency=price_currency, price_amount=price_amount
                )
            except Exception:
                pass

        merchant_tab.slots[merchant_slot] = item
        return True, f"Listed {item.name} in Merchant Stash for {price_amount} {price_currency}"

    def update_merchant_item_price(
        self, account_id: str, character_id: str, merchant_slot: int, new_currency: str, new_amount: int
    ) -> Tuple[bool, str]:
        merchant_tab = self.get_merchant_stash(account_id, character_id)
        if merchant_slot not in merchant_tab.slots:
            return False, f"Merchant slot {merchant_slot} is empty"
        if new_amount <= 0:
            return False, "New price amount must be greater than 0"

        item = merchant_tab.slots[merchant_slot]
        if item.is_locked:
            return False, "Item is currently locked in an active trade swap"

        item.asking_price_currency = new_currency
        item.asking_price_amount = new_amount
        return True, f"Updated price to {new_amount} {new_currency}"

    def remove_item_from_merchant_stash(
        self,
        account_id: str,
        character_id: str,
        merchant_slot: int,
        target_inv_slot: Optional[int] = None
    ) -> Tuple[bool, str]:
        merchant_tab = self.get_merchant_stash(account_id, character_id)
        inv = self.get_character_inventory(account_id, character_id)

        if merchant_slot not in merchant_tab.slots:
            return False, f"Merchant slot {merchant_slot} is empty"

        item = merchant_tab.slots[merchant_slot]
        if item.is_locked:
            return False, "Cannot withdraw: item is locked in 2PC checkout"

        dest_slot = target_inv_slot
        if dest_slot is None or dest_slot in inv.slots or dest_slot >= inv.capacity:
            dest_slot = find_first_empty_slot(inv.slots, inv.capacity)

        if dest_slot is None:
            return False, "Cannot withdraw: inventory bag is full"

        del merchant_tab.slots[merchant_slot]
        item.asking_price_currency = None
        item.asking_price_amount = 0
        item.listing_id = None
        inv.slots[dest_slot] = item
        return True, f"Withdrew {item.name} from Merchant Stash back to bag"
