from __future__ import annotations import secrets import uuid from datetime import UTC, datetime, timedelta from sqlalchemy import select from sqlalchemy.orm import selectinload from rehearsalhub.db.models import Band, BandInvite, BandMember, BandStorage from rehearsalhub.repositories.base import BaseRepository class BandRepository(BaseRepository[Band]): model = Band async def get_by_slug(self, slug: str) -> Band | None: stmt = select(Band).where(Band.slug == slug) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def get_with_members(self, band_id: uuid.UUID) -> Band | None: stmt = ( select(Band) .options(selectinload(Band.memberships).selectinload(BandMember.member)) .where(Band.id == band_id) ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def get_member_role(self, band_id: uuid.UUID, member_id: uuid.UUID) -> str | None: stmt = select(BandMember.role).where( BandMember.band_id == band_id, BandMember.member_id == member_id ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def add_member( self, band_id: uuid.UUID, member_id: uuid.UUID, role: str = "member", instrument: str | None = None, ) -> BandMember: bm = BandMember(band_id=band_id, member_id=member_id, role=role, instrument=instrument) self.session.add(bm) await self.session.flush() return bm async def is_member(self, band_id: uuid.UUID, member_id: uuid.UUID) -> bool: return await self.get_member_role(band_id, member_id) is not None async def remove_member(self, band_id: uuid.UUID, member_id: uuid.UUID) -> None: stmt = select(BandMember).where( BandMember.band_id == band_id, BandMember.member_id == member_id ) result = await self.session.execute(stmt) bm = result.scalar_one_or_none() if bm: await self.session.delete(bm) await self.session.flush() async def create_invite( self, band_id: uuid.UUID, created_by: uuid.UUID, role: str = "member", ttl_hours: int = 72 ) -> BandInvite: invite = BandInvite( band_id=band_id, token=secrets.token_urlsafe(32), role=role, created_by=created_by, expires_at=datetime.now(UTC) + timedelta(hours=ttl_hours), ) self.session.add(invite) await self.session.flush() await self.session.refresh(invite) return invite async def get_invite_by_token(self, token: str) -> BandInvite | None: stmt = select(BandInvite).where(BandInvite.token == token) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def get_invite_by_id(self, invite_id: uuid.UUID) -> BandInvite | None: stmt = select(BandInvite).where(BandInvite.id == invite_id) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def get_invites_for_band(self, band_id: uuid.UUID) -> list[BandInvite]: """Get all invites for a specific band.""" stmt = select(BandInvite).where(BandInvite.band_id == band_id) result = await self.session.execute(stmt) return list(result.scalars().all()) async def get_by_nc_folder_prefix(self, path: str) -> Band | None: """Return the band whose active storage root_path is a prefix of *path*. Longest match wins (most-specific prefix) so nested paths resolve correctly. """ stmt = ( select(Band, BandStorage.root_path) .join( BandStorage, (BandStorage.band_id == Band.id) & BandStorage.is_active.is_(True), ) .where(BandStorage.root_path.is_not(None)) ) result = await self.session.execute(stmt) rows = result.all() best: Band | None = None best_len = 0 for band, root_path in rows: folder = root_path.rstrip("/") + "/" if path.startswith(folder) and len(folder) > best_len: best = band best_len = len(folder) return best async def list_for_member(self, member_id: uuid.UUID) -> list[Band]: stmt = ( select(Band) .join(BandMember, BandMember.band_id == Band.id) .where(BandMember.member_id == member_id) .order_by(Band.name) ) result = await self.session.execute(stmt) return list(result.scalars().all())