"""
User and access control models.

Tables in this file:
  permission              — permission definitions (global, no lab scoping)
  role_permission         — maps roles → permissions with default grant flag
  user                    — staff members with role-based access
  user_permission_override — per-user permission grant/deny overrides

Access Control Design
─────────────────────
Each User has a Role (PostgreSQL ENUM). Roles have a default permission set
defined in RolePermission. Individual users can override any permission via
UserPermissionOverride.

Resolution order (highest priority wins):
  1. UserPermissionOverride.is_granted  — explicit per-user override
  2. RolePermission.default_granted     — role default
  3. Deny                               — if no record exists for the combo

Superadmin bypasses all permission checks.

Soft Delete Decisions
─────────────────────
  User                   → YES: staff must be deactivatable without losing
                                 audit history (orders, reports, payments).
  Permission             → NO:  global config — hard delete.
  RolePermission         → NO:  global config — hard delete.
  UserPermissionOverride → NO:  revoke = delete the row.

Lab Scoping
───────────
  User                   → YES (LabScopedMixin) — belongs to a branch.
  Permission             → NO  — system-wide definitions.
  RolePermission         → NO  — system-wide role config.
  UserPermissionOverride → NO  — scoped indirectly via user_id → lab_id.
"""

import enum
import uuid
from decimal import Decimal

import sqlalchemy as sa
from sqlalchemy import Boolean, ForeignKey, Numeric, String, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.engine import Dialect
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.models.base import (
    Base,
    LabScopedMixin,
    SoftDeleteMixin,
    TimestampMixin,
    UUIDPrimaryKeyMixin,
)

# ── Enums ──────────────────────────────────────────────────────────────────────


class UserRole(enum.StrEnum):
    """
    Built-in staff roles (kept list, 2026-09-28).

    Custom roles created by the superadmin live only in the `role` table and
    are stored on users as plain strings — no enum change or migration needed.
    """

    SUPERADMIN = "superadmin"
    DOCTOR = "doctor"
    PATHOLOGIST = "pathologist"
    RADIOLOGIST = "radiologist"
    RECEPTIONIST = "receptionist"
    PHLEBOTOMIST = "phlebotomist"
    ACCOUNTANT = "accountant"
    # Legacy code retained so existing tokens/fixtures deserialize during the
    # role merge. New accounts cannot be assigned this code.
    GENERAL_USER = "general_user"

    @classmethod
    def codes(cls) -> set[str]:
        """Assignable built-in codes; legacy general_user is intentionally excluded."""
        return {r.value for r in cls if r is not cls.GENERAL_USER}


# Named PostgreSQL ENUM type (retained for backward compatibility)
UserRoleType = sa.Enum(
    UserRole,
    name="user_role",
    native_enum=True,
    create_constraint=True,
    values_callable=lambda obj: [e.value for e in obj],
)


class RoleStr(str):
    """
    String subclass representing a user role.
    Provides `.value` for seamless backward compatibility with UserRole enum usages.
    """

    @property
    def value(self) -> str:
        return str(self)


class RoleStringType(sa.types.TypeDecorator[RoleStr]):
    impl = sa.String(50)
    cache_ok = True

    def process_bind_param(self, value: object | None, dialect: Dialect) -> str | None:
        if value is None:
            return None
        if hasattr(value, "value"):
            return str(value.value)
        return str(value)

    def process_result_value(self, value: str | None, dialect: Dialect) -> RoleStr | None:
        if value is None:
            return None
        return RoleStr(value)


# ── Models ─────────────────────────────────────────────────────────────────────


class Role(UUIDPrimaryKeyMixin, TimestampMixin, Base):
    """
    Staff and system roles.
    Supports dynamic role creation and custom permission configurations.
    """

    __tablename__ = "role"
    __table_args__ = (UniqueConstraint("code", name="uq_role_code"),)

    code: Mapped[str] = mapped_column(
        String(50),
        nullable=False,
        index=True,
        comment="Unique role code identifier (e.g. 'doctor', 'receptionist')",
    )
    name: Mapped[str] = mapped_column(
        String(100),
        nullable=False,
        comment="Human-readable role name (e.g. 'Consultant Doctor')",
    )
    description: Mapped[str | None] = mapped_column(
        sa.Text,
        nullable=True,
        comment="Description of the role and responsibilities",
    )
    is_system: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        default=False,
        server_default="false",
        comment="True if built-in system role that cannot be deleted",
    )

    def __repr__(self) -> str:
        return f"<Role code={self.code!r} name={self.name!r} system={self.is_system}>"


class Permission(UUIDPrimaryKeyMixin, TimestampMixin, Base):
    """
    A discrete, named capability in the system.

    Permissions are seeded at deploy time — they are not created by users.
    Example codes: 'manage_catalog', 'verify_reports', 'manage_billing'.

    The `code` column is globally unique — it is the stable identifier
    used in application code to check access.
    """

    __tablename__ = "permission"

    code: Mapped[str] = mapped_column(
        String(100),
        nullable=False,
        unique=True,
        index=True,
        comment="Machine-readable permission key (e.g. 'manage_catalog')",
    )
    description: Mapped[str] = mapped_column(
        String(500),
        nullable=False,
        comment="Human-readable description shown in the permission management UI",
    )
    is_active: Mapped[bool] = mapped_column(
        Boolean,
        default=True,
        nullable=False,
        server_default="true",
        comment="Whether this capability/feature is globally active",
    )

    # Relationships
    role_permissions: Mapped[list["RolePermission"]] = relationship(
        back_populates="permission",
        lazy="noload",
        cascade="all, delete-orphan",
    )
    user_overrides: Mapped[list["UserPermissionOverride"]] = relationship(
        back_populates="permission",
        lazy="noload",
        cascade="all, delete-orphan",
    )

    def __repr__(self) -> str:
        return f"<Permission code={self.code!r}>"


class RolePermission(UUIDPrimaryKeyMixin, TimestampMixin, Base):
    """
    Maps a role to a permission with a default grant flag.

    Each (role, permission) pair should appear at most once — enforced by
    the unique constraint. To change a role's default access to a permission,
    update the existing row rather than inserting a duplicate.

    Superadmin can UPDATE default_granted for any row via the admin UI,
    giving them full role permission management without code changes.
    """

    __tablename__ = "role_permission"
    __table_args__ = (
        UniqueConstraint(
            "role",
            "permission_id",
            name="uq_role_permission_role_permission_id",
        ),
    )

    role: Mapped[RoleStr] = mapped_column(
        RoleStringType,
        nullable=False,
        index=True,
        comment="The staff role code this assignment applies to",
    )

    @sa.orm.validates("role")
    def _validate_role(self, key: str, value: object) -> RoleStr:
        if hasattr(value, "value"):
            return RoleStr(value.value)
        return RoleStr(str(value))

    permission_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("permission.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
        comment="FK → permission.id",
    )
    default_granted: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        default=True,
        server_default="true",
        comment="True = granted to this role by default. False = denied by default.",
    )

    # Relationships
    permission: Mapped["Permission"] = relationship(
        back_populates="role_permissions",
        lazy="selectin",
    )

    def __repr__(self) -> str:
        status = "granted" if self.default_granted else "denied"
        return f"<RolePermission role={self.role!r} permission_id={self.permission_id!s:.8} {status}>"


class User(UUIDPrimaryKeyMixin, TimestampMixin, SoftDeleteMixin, LabScopedMixin, Base):
    """
    A staff member who logs into the admin panel.

    Authentication: phone number + bcrypt password.
    Authorization: determined by role defaults + per-user overrides.

    Uniqueness within a branch:
      (lab_id, phone) — a phone number is the login identifier per branch.
      (lab_id, email) — email is optional but unique when provided.

    Deactivation: set is_active=False to block login without deleting.
    Termination:  set deleted_at to soft-delete, preserving audit history.
    """

    __tablename__ = "user"
    __table_args__ = (
        UniqueConstraint("lab_id", "phone", name="uq_user_lab_id_phone"),
        UniqueConstraint("lab_id", "email", name="uq_user_lab_id_email"),
    )

    name: Mapped[str] = mapped_column(
        String(200),
        nullable=False,
        comment="Full name of the staff member",
    )
    phone: Mapped[str] = mapped_column(
        String(20),
        nullable=False,
        index=True,
        comment="Phone number — primary login identifier",
    )
    email: Mapped[str | None] = mapped_column(
        String(255),
        nullable=True,
        comment="Email address — optional, used for notifications",
    )
    role: Mapped[RoleStr] = mapped_column(
        RoleStringType,
        nullable=False,
        index=True,
        comment="Staff role — determines default permissions via role_permission",
    )

    @sa.orm.validates("role")
    def _validate_role(self, key: str, value: object) -> RoleStr:
        if hasattr(value, "value"):
            return RoleStr(value.value)
        return RoleStr(str(value))

    hashed_password: Mapped[str] = mapped_column(
        String(255),
        nullable=False,
        comment="bcrypt hash of the user's password — NEVER store plain text",
    )
    is_active: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        default=True,
        server_default="true",
        comment="False = account suspended (login blocked, records retained)",
    )
    signature_image_path: Mapped[str | None] = mapped_column(
        String(500),
        nullable=True,
        comment="Relative path of the transparent staff signature PNG under storage/signatures/{user_id}/",
    )
    commission_rate: Mapped[Decimal | None] = mapped_column(
        Numeric(5, 2),
        nullable=True,
        comment="Per-report commission % for radiologists (null = no commission). Set by superadmin.",
    )
    license_no: Mapped[str | None] = mapped_column(
        String(50),
        nullable=True,
        comment=(
            "Professional council registration number (e.g. NMC No.)."
            " Mandatory for doctor/pathologist/radiologist; printed under the report signature."
        ),
    )

    # Relationships
    lab: Mapped["Lab"] = relationship(  # type: ignore[name-defined]
        "Lab",
        foreign_keys="[User.lab_id]",
        lazy="selectin",
    )
    permission_overrides: Mapped[list["UserPermissionOverride"]] = relationship(
        back_populates="user",
        lazy="noload",
        foreign_keys="[UserPermissionOverride.user_id]",
        cascade="all, delete-orphan",
    )

    @property
    def is_superadmin(self) -> bool:
        """Superadmin bypasses all permission checks."""
        return self.role == UserRole.SUPERADMIN.value

    def __repr__(self) -> str:
        return f"<User id={self.id!s:.8} phone={self.phone!r} role={self.role!r}>"


class UserPermissionOverride(UUIDPrimaryKeyMixin, TimestampMixin, Base):
    """
    Explicit per-user permission grant or denial.

    Takes precedence over RolePermission.default_granted for the
    specific (user, permission) combination.

    Creating a row with is_granted=True grants the permission,
    even if the role default is False.

    Creating a row with is_granted=False denies the permission,
    even if the role default is True.

    To remove an override and fall back to role default: delete the row.

    set_by: the admin (User) who created this override — for audit.
            SET NULL on admin deletion: we lose who set it, but keep the override.
    """

    __tablename__ = "user_permission_override"
    __table_args__ = (
        UniqueConstraint(
            "user_id",
            "permission_id",
            name="uq_user_permission_override_user_id_permission_id",
        ),
    )

    user_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("user.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
        comment="The staff user this override applies to",
    )
    permission_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("permission.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
        comment="The permission being explicitly granted or denied",
    )
    is_granted: Mapped[bool] = mapped_column(
        Boolean,
        nullable=False,
        comment="True = explicitly granted. False = explicitly denied.",
    )
    set_by: Mapped[uuid.UUID | None] = mapped_column(
        UUID(as_uuid=True),
        # Using sa.ForeignKey string form to avoid ambiguity with multiple FKs to user
        ForeignKey("user.id", ondelete="SET NULL"),
        nullable=True,
        index=True,
        comment="FK → user.id of the admin who created this override (audit trail)",
    )

    # Relationships
    user: Mapped["User"] = relationship(
        back_populates="permission_overrides",
        foreign_keys=[user_id],
        lazy="selectin",
    )
    permission: Mapped["Permission"] = relationship(
        back_populates="user_overrides",
        lazy="selectin",
    )
    set_by_user: Mapped["User | None"] = relationship(
        foreign_keys=[set_by],
        lazy="selectin",
    )

    def __repr__(self) -> str:
        status = "granted" if self.is_granted else "denied"
        return (
            f"<UserPermissionOverride "
            f"user_id={self.user_id!s:.8} "
            f"permission_id={self.permission_id!s:.8} "
            f"{status}>"
        )
