"""
Caprail ORM Relationships.

Mix in these descriptors on your Model subclasses::

    class Post(Model):
        @property
        def author(self):
            return self.belongs_to(User, foreign_key="user_id")

        @property
        def comments(self):
            return self.has_many(Comment, foreign_key="post_id")
"""
from __future__ import annotations

from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from caprail.orm.model import Model


class HasOne:
    """One-to-one: this model owns one of another model."""

    def __init__(self, owner: "Model", related: type["Model"], foreign_key: str, local_key: str = "id") -> None:
        self._owner = owner
        self._related = related
        self._foreign_key = foreign_key
        self._local_key = local_key

    def __await__(self):
        return self._resolve().__await__()

    async def _resolve(self) -> "Model | None":
        local_val = self._owner._attributes.get(self._local_key)
        return await self._related.where(self._foreign_key, local_val).first()


class HasMany:
    """One-to-many: this model owns many of another model."""

    def __init__(self, owner: "Model", related: type["Model"], foreign_key: str, local_key: str = "id") -> None:
        self._owner = owner
        self._related = related
        self._foreign_key = foreign_key
        self._local_key = local_key

    def __await__(self):
        return self._resolve().__await__()

    async def _resolve(self) -> list["Model"]:
        local_val = self._owner._attributes.get(self._local_key)
        return await self._related.where(self._foreign_key, local_val).get()

    def where(self, column: str, value: Any):
        from caprail.orm.query_builder import QueryBuilder
        local_val = self._owner._attributes.get(self._local_key)
        return self._related.where(self._foreign_key, local_val).where(column, value)


class BelongsTo:
    """Inverse of HasOne/HasMany — this model holds the foreign key."""

    def __init__(self, owner: "Model", related: type["Model"], foreign_key: str, owner_key: str = "id") -> None:
        self._owner = owner
        self._related = related
        self._foreign_key = foreign_key
        self._owner_key = owner_key

    def __await__(self):
        return self._resolve().__await__()

    async def _resolve(self) -> "Model | None":
        fk_val = self._owner._attributes.get(self._foreign_key)
        if fk_val is None:
            return None
        return await self._related.where(self._owner_key, fk_val).first()


class BelongsToMany:
    """
    Many-to-many via a pivot table.

    Example::

        class User(Model):
            @property
            def roles(self):
                return self.belongs_to_many(Role, pivot_table="role_user",
                                            foreign_pivot_key="user_id",
                                            related_pivot_key="role_id")
    """

    def __init__(
        self,
        owner: "Model",
        related: type["Model"],
        pivot_table: str,
        foreign_pivot_key: str,
        related_pivot_key: str,
        parent_key: str = "id",
        related_key: str = "id",
    ) -> None:
        self._owner = owner
        self._related = related
        self._pivot_table = pivot_table
        self._foreign_pivot_key = foreign_pivot_key
        self._related_pivot_key = related_pivot_key
        self._parent_key = parent_key
        self._related_key = related_key

    def __await__(self):
        return self._resolve().__await__()

    async def _resolve(self) -> list["Model"]:
        from caprail.orm.model import _get_session
        import sqlalchemy as sa

        parent_val = self._owner._attributes.get(self._parent_key)
        pivot = sa.Table(self._pivot_table, sa.MetaData())

        async with _get_session() as session:
            async with session.bind.connect() as conn:
                await conn.run_sync(pivot.metadata.reflect, only=[self._pivot_table])
            pivot = pivot.metadata.tables[self._pivot_table]
            stmt = sa.select(pivot.c[self._related_pivot_key]).where(
                pivot.c[self._foreign_pivot_key] == parent_val
            )
            result = await session.execute(stmt)
            related_ids = [row[0] for row in result.fetchall()]

        if not related_ids:
            return []
        return await self._related.where_in(self._related_key, related_ids).get()

    async def attach(self, related_id: Any, pivot_data: dict | None = None) -> None:
        from caprail.orm.model import _get_session
        import sqlalchemy as sa

        parent_val = self._owner._attributes.get(self._parent_key)
        pivot = sa.Table(self._pivot_table, sa.MetaData())
        values = {self._foreign_pivot_key: parent_val, self._related_pivot_key: related_id}
        if pivot_data:
            values.update(pivot_data)
        async with _get_session() as session:
            async with session.bind.connect() as conn:
                await conn.run_sync(pivot.metadata.reflect, only=[self._pivot_table])
            pivot = pivot.metadata.tables[self._pivot_table]
            await session.execute(sa.insert(pivot).values(**values))
            await session.commit()

    async def detach(self, related_id: Any) -> None:
        from caprail.orm.model import _get_session
        import sqlalchemy as sa

        parent_val = self._owner._attributes.get(self._parent_key)
        pivot = sa.Table(self._pivot_table, sa.MetaData())
        async with _get_session() as session:
            async with session.bind.connect() as conn:
                await conn.run_sync(pivot.metadata.reflect, only=[self._pivot_table])
            pivot = pivot.metadata.tables[self._pivot_table]
            await session.execute(
                sa.delete(pivot).where(
                    (pivot.c[self._foreign_pivot_key] == parent_val)
                    & (pivot.c[self._related_pivot_key] == related_id)
                )
            )
            await session.commit()


def has_one(owner: "Model", related: type["Model"], foreign_key: str, local_key: str = "id") -> HasOne:
    return HasOne(owner, related, foreign_key, local_key)


def has_many(owner: "Model", related: type["Model"], foreign_key: str, local_key: str = "id") -> HasMany:
    return HasMany(owner, related, foreign_key, local_key)


def belongs_to(owner: "Model", related: type["Model"], foreign_key: str, owner_key: str = "id") -> BelongsTo:
    return BelongsTo(owner, related, foreign_key, owner_key)


def belongs_to_many(
    owner: "Model",
    related: type["Model"],
    pivot_table: str,
    foreign_pivot_key: str,
    related_pivot_key: str,
    parent_key: str = "id",
    related_key: str = "id",
) -> BelongsToMany:
    return BelongsToMany(owner, related, pivot_table, foreign_pivot_key, related_pivot_key, parent_key, related_key)
