from __future__ import annotations

from datetime import datetime
from typing import List, Optional

from passlib.context import CryptContext

try:  # pragma: no cover - defensive compatibility shim
    import importlib.metadata as importlib_metadata
    import argon2  # type: ignore

    setattr(argon2, "__version__", importlib_metadata.version("argon2-cffi"))
except Exception:
    pass
from sqlalchemy import (
    Boolean,
    DateTime,
    ForeignKey,
    Integer,
    String,
    Text,
    UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func

from config.database import Base

# Configure password hashing strategy. Argon2 provides strong resistance against
# GPU-based cracking attempts, aligning with the security plan.
password_context = CryptContext(schemes=["argon2"], deprecated="auto")


class User(Base):
    """Authenticated platform user with optional elevated privileges."""

    __tablename__ = "users"
    __table_args__ = (
        UniqueConstraint("email", name="uq_users_email"),
        UniqueConstraint("username", name="uq_users_username"),
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    email: Mapped[str] = mapped_column(String(255), nullable=False)
    username: Mapped[str] = mapped_column(String(128), nullable=False)
    password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
    is_active: Mapped[bool] = mapped_column(
        Boolean, nullable=False, default=True)
    is_superuser: Mapped[bool] = mapped_column(
        Boolean, nullable=False, default=False)
    last_login_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now()
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
    )

    role_assignments: Mapped[List["UserRole"]] = relationship(
        "UserRole",
        back_populates="user",
        cascade="all, delete-orphan",
        foreign_keys="UserRole.user_id",
    )
    roles: Mapped[List["Role"]] = relationship(
        "Role",
        secondary="user_roles",
        primaryjoin="User.id == UserRole.user_id",
        secondaryjoin="Role.id == UserRole.role_id",
        viewonly=True,
        back_populates="users",
    )

    def set_password(self, raw_password: str) -> None:
        """Hash and store a password for the user."""

        self.password_hash = self.hash_password(raw_password)

    @staticmethod
    def hash_password(raw_password: str) -> str:
        """Return the Argon2 hash for a clear-text password."""

        return password_context.hash(raw_password)

    def verify_password(self, candidate_password: str) -> bool:
        """Validate a password against the stored hash."""

        if not self.password_hash:
            return False
        return password_context.verify(candidate_password, self.password_hash)

    def __repr__(self) -> str:  # pragma: no cover - helpful for debugging
        return f"User(id={self.id!r}, email={self.email!r})"


class Role(Base):
    """Role encapsulating a set of permissions."""

    __tablename__ = "roles"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
    display_name: Mapped[str] = mapped_column(String(128), nullable=False)
    description: Mapped[str | None] = mapped_column(Text, nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now()
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
    )

    assignments: Mapped[List["UserRole"]] = relationship(
        "UserRole",
        back_populates="role",
        cascade="all, delete-orphan",
        foreign_keys="UserRole.role_id",
    )
    users: Mapped[List["User"]] = relationship(
        "User",
        secondary="user_roles",
        primaryjoin="Role.id == UserRole.role_id",
        secondaryjoin="User.id == UserRole.user_id",
        viewonly=True,
        back_populates="roles",
    )

    def __repr__(self) -> str:  # pragma: no cover - helpful for debugging
        return f"Role(id={self.id!r}, name={self.name!r})"


class UserRole(Base):
    """Association between users and roles with assignment metadata."""

    __tablename__ = "user_roles"
    __table_args__ = (
        UniqueConstraint("user_id", "role_id", name="uq_user_roles_user_role"),
    )

    user_id: Mapped[int] = mapped_column(
        Integer,
        ForeignKey("users.id", ondelete="CASCADE"),
        primary_key=True,
    )
    role_id: Mapped[int] = mapped_column(
        Integer,
        ForeignKey("roles.id", ondelete="CASCADE"),
        primary_key=True,
    )
    granted_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now()
    )
    granted_by: Mapped[Optional[int]] = mapped_column(
        Integer,
        ForeignKey("users.id", ondelete="SET NULL"),
        nullable=True,
    )

    user: Mapped["User"] = relationship(
        "User",
        foreign_keys=[user_id],
        back_populates="role_assignments",
    )
    role: Mapped["Role"] = relationship(
        "Role",
        foreign_keys=[role_id],
        back_populates="assignments",
    )
    granted_by_user: Mapped[Optional["User"]] = relationship(
        "User",
        foreign_keys=[granted_by],
    )

    def __repr__(self) -> str:  # pragma: no cover - debugging helper
        return f"UserRole(user_id={self.user_id!r}, role_id={self.role_id!r})"
