from dataclasses import dataclass
from typing import List

from sqlalchemy import ForeignKey
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import base as sa_base
from sqlalchemy.orm import mapped_column, relationship

from ttfrog.db.base import BaseObject
from ttfrog.db.schema import prototypes
from ttfrog.db.schema.constants import InventoryType
from ttfrog.db.schema.modifiers import ModifierMixin

inventory_type_map = {
    InventoryType.EQUIPMENT: [
        prototypes.ItemType.WEAPON,
        prototypes.ItemType.ARMOR,
        prototypes.ItemType.SHIELD,
        prototypes.ItemType.ITEM,
        prototypes.ItemType.SCROLL,
        prototypes.ItemType.CONTAINER,
    ],
    InventoryType.SPELL: [prototypes.ItemType.SPELL],
}


def inventory_map_creator(fields):
    # if isinstance(fields, Item):
    #     return fields
    # return Item(**fields)
    return Item(**fields)


class Inventory(BaseObject):
    """
    Creates a many-to-many between Items or Spells and any model inheriting from the InventoryMixin.
    """

    __tablename__ = "inventory"

    id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
    inventory_type: Mapped[InventoryType] = mapped_column(nullable=False)

    primary_table_name: Mapped[str] = mapped_column(nullable=False)
    primary_table_id: Mapped[int] = mapped_column(nullable=False)

    _item_contents: Mapped[List["Item"]] = relationship(
        uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: []
    )

    _spell_contents: Mapped[List["Spell"]] = relationship(
        uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: []
    )

    character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), nullable=True, default=None)
    character = relationship("Character", uselist=False, default=None)

    @property
    def contents(self):
        if self.inventory_type == InventoryType.SPELL:
            return self._spell_contents
        return self._item_contents

    @property
    def all_contents(self):
        def nested(obj):
            if hasattr(obj, "contents"):
                for mapping in obj.contents:
                    yield mapping
                    yield from nested(mapping)
            elif hasattr(obj, "inventory"):
                yield from nested(obj.inventory)

        yield from nested(self)

    def get(self, prototype):
        return self.get_all(prototype)[0]

    def get_all(self, prototype):
        return [mapping for mapping in self.all_contents if mapping.prototype == prototype]

    def add(self, prototype):
        if prototype.item_type not in inventory_type_map[self.inventory_type]:
            return False

        mapping = globals()[prototype.__inventory_item_class__](prototype_id=prototype.id)
        mapping.prototype = prototype

        if prototype.consumable:
            mapping.count = prototype.count
        if prototype.charges:
            mapping.charges = [Charge(item_id=mapping.id) for i in range(prototype.charges)]

        self.contents.append(mapping)
        return mapping

    def remove(self, mapping):
        if mapping in self.contents:
            self.contents.remove(mapping)
            return mapping
        return False

    def __contains__(self, obj):
        if isinstance(obj, prototypes.BaseItem):
            return obj in [mapping.prototype for mapping in self.all_contents]
        elif isinstance(obj, Item):
            return obj in self.all_contents

    def __iter__(self):
        yield from self.all_contents


@dataclass
class InventoryItemMixin:
    @declared_attr
    def container(cls) -> Mapped["Inventory"]:
        return relationship(uselist=False, viewonly=True, init=False)

    @declared_attr
    def _inventory_id(cls) -> Mapped[int]:
        return mapped_column(ForeignKey("inventory.id"), init=False)


@dataclass
class InventoryMixin:
    """
    Add to a class to make it an inventory.
    """

    @declared_attr
    def inventory(cls):
        """
        Create the join between the current model and the ModifierMap table.
        """
        return relationship(
            "Inventory",
            primaryjoin=(
                "and_("
                f"foreign(Inventory.primary_table_name)=='{cls.__tablename__}', "
                f"foreign(Inventory.primary_table_id)=={cls.__name__}.id"
                ")"
            ),
            cascade="all,delete,delete-orphan",
            overlaps="inventory,inventory",
            single_parent=True,
            uselist=False,
            lazy="immediate",
        )

    def __after_insert__(self, session):
        if self.inventory_type:
            self.inventory = Inventory(
                inventory_type=self.inventory_type,
                primary_table_name=self.__tablename__,
                primary_table_id=self.id,
                character_id=getattr(self, "character_id", None),
            )
            session.add(self)

    def __contains__(self, obj):
        return obj in self.inventory


class Spell(BaseObject, InventoryItemMixin):
    __tablename__ = "spell"

    id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
    prototype_id: Mapped[int] = mapped_column(ForeignKey("spell_prototype.id"))
    always_prepared: Mapped[bool] = mapped_column(default=False)
    _prepared: Mapped[bool] = mapped_column(init=False, default=False)

    prototype: Mapped["prototypes.BaseSpell"] = relationship(uselist=False, lazy="immediate", init=False)

    @property
    def prepared(self):
        return self._prepared or self.always_prepared


class Charge(BaseObject):
    __tablename__ = "charge"
    id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
    item_id: Mapped[int] = mapped_column(ForeignKey("item.id"))
    expended: Mapped[bool] = mapped_column(nullable=False, default=False)


class Item(BaseObject, InventoryMixin, InventoryItemMixin, ModifierMixin):
    __tablename__ = "item"

    id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)

    prototype_id: Mapped[int] = mapped_column(ForeignKey("item_prototype.id"))
    equipped: Mapped[bool] = mapped_column(default=False)
    attuned: Mapped[bool] = mapped_column(default=False)
    count: Mapped[int] = mapped_column(nullable=False, default=1)
    charges: Mapped[List["Charge"]] = relationship(
        uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: []
    )

    _inventory_type: Mapped[InventoryType] = mapped_column(nullable=True, default=None)

    @property
    def inventory_type(self):
        if self._inventory_type:
            return self._inventory_type
        elif self.prototype:
            return self.prototype.inventory_type

    prototype: Mapped["prototypes.BaseItem"] = relationship(uselist=False, lazy="immediate", init=False)

    @property
    def modifiers(self):
        return self.prototype.modifiers

    @property
    def charges_available(self):
        return [charge for charge in self.charges if not charge.expended]

    def equip(self):
        if self.equipped:
            return False
        self.equipped = True
        return True

    def unequip(self):
        if not self.equipped:
            return False
        self.equipped = False
        return True

    def use(self, item_property: prototypes.ItemProperty, charges=None):
        if item_property.charge_cost is None:
            return True
        avail = self.charges_available
        if charges is None:
            charges = item_property.charge_cost
        if len(avail) < charges:
            return False
        for charge in avail[:charges]:
            charge.expended = True
        return True

    def consume(self, count=1):
        if count < 0:
            return False
        if not self.prototype.consumable:
            return False
        if self.count < count:
            return False
        self.count -= count
        if self.count == 0:
            self.container.remove(self)
            return 0
        return self.count

    def attune(self):
        if self.attuned:
            return False
        if not self.requires_attunement:
            return False
        if len(self.container.character.attuned_items) >= 3:
            return False
        self.attuned = True
        return True

    def unattune(self):
        if not self.attuned:
            return False
        self.attuned = False
        return True

    def move_to(self, target):
        target_inventory = getattr(target, "inventory", target)
        if self.container == target_inventory:
            return False
        self.container.contents.remove(self)
        self.container = target_inventory
        self.container.id = target_inventory.id
        target_inventory.contents.append(self)
        return True

    def __getattr__(self, name: str):
        if name == sa_base.DEFAULT_STATE_ATTR:
            raise AttributeError()
        return getattr(self.prototype, name)
