"""Theme & layout engine repositories — Phase 11.3.""" from __future__ import annotations from typing import Sequence from uuid import UUID from sqlalchemy import func, select from app.models.themes import ( ExperienceLayout, ExperienceLayoutVersion, ExperienceTheme, ExperienceThemeVersion, PageLayoutBinding, SiteThemeBinding, ) from app.repositories.base import TenantBaseRepository from app.specifications.themes import ( LayoutListSpec, LayoutVersionListSpec, PageLayoutBindingListSpec, SiteThemeBindingListSpec, ThemeListSpec, ThemeVersionListSpec, ) class ExperienceThemeRepository(TenantBaseRepository[ExperienceTheme]): model = ExperienceTheme async def get_by_code( self, tenant_id: UUID, workspace_id: UUID, code: str ) -> ExperienceTheme | None: stmt = select(self.model).where( self.model.tenant_id == tenant_id, self.model.workspace_id == workspace_id, self.model.code == code, self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def list_filtered( self, tenant_id: UUID, *, offset: int, limit: int, spec: ThemeListSpec | None = None, ) -> tuple[Sequence[ExperienceTheme], int]: base = select(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) count_base = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) if spec is not None: base = spec.apply(base) for clause in spec.filter_clauses(): count_base = count_base.where(clause) else: base = base.order_by(self.model.created_at.desc()) total = int((await self.session.execute(count_base)).scalar_one()) result = await self.session.execute(base.offset(offset).limit(limit)) return result.scalars().all(), total class ExperienceThemeVersionRepository(TenantBaseRepository[ExperienceThemeVersion]): model = ExperienceThemeVersion async def get_by_number( self, tenant_id: UUID, theme_id: UUID, version_number: int ) -> ExperienceThemeVersion | None: stmt = select(self.model).where( self.model.tenant_id == tenant_id, self.model.theme_id == theme_id, self.model.version_number == version_number, self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def max_version_number(self, tenant_id: UUID, theme_id: UUID) -> int: stmt = select(func.max(self.model.version_number)).where( self.model.tenant_id == tenant_id, self.model.theme_id == theme_id, self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) value = result.scalar_one() return int(value or 0) async def list_filtered( self, tenant_id: UUID, *, offset: int, limit: int, spec: ThemeVersionListSpec | None = None, ) -> tuple[Sequence[ExperienceThemeVersion], int]: base = select(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) count_base = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) if spec is not None: base = spec.apply(base) for clause in spec.filter_clauses(): count_base = count_base.where(clause) else: base = base.order_by(self.model.version_number.desc()) total = int((await self.session.execute(count_base)).scalar_one()) result = await self.session.execute(base.offset(offset).limit(limit)) return result.scalars().all(), total class ExperienceLayoutRepository(TenantBaseRepository[ExperienceLayout]): model = ExperienceLayout async def get_by_code( self, tenant_id: UUID, workspace_id: UUID, code: str ) -> ExperienceLayout | None: stmt = select(self.model).where( self.model.tenant_id == tenant_id, self.model.workspace_id == workspace_id, self.model.code == code, self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def list_filtered( self, tenant_id: UUID, *, offset: int, limit: int, spec: LayoutListSpec | None = None, ) -> tuple[Sequence[ExperienceLayout], int]: base = select(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) count_base = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) if spec is not None: base = spec.apply(base) for clause in spec.filter_clauses(): count_base = count_base.where(clause) else: base = base.order_by(self.model.created_at.desc()) total = int((await self.session.execute(count_base)).scalar_one()) result = await self.session.execute(base.offset(offset).limit(limit)) return result.scalars().all(), total class ExperienceLayoutVersionRepository(TenantBaseRepository[ExperienceLayoutVersion]): model = ExperienceLayoutVersion async def get_by_number( self, tenant_id: UUID, layout_id: UUID, version_number: int ) -> ExperienceLayoutVersion | None: stmt = select(self.model).where( self.model.tenant_id == tenant_id, self.model.layout_id == layout_id, self.model.version_number == version_number, self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def max_version_number(self, tenant_id: UUID, layout_id: UUID) -> int: stmt = select(func.max(self.model.version_number)).where( self.model.tenant_id == tenant_id, self.model.layout_id == layout_id, self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) value = result.scalar_one() return int(value or 0) async def list_filtered( self, tenant_id: UUID, *, offset: int, limit: int, spec: LayoutVersionListSpec | None = None, ) -> tuple[Sequence[ExperienceLayoutVersion], int]: base = select(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) count_base = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) if spec is not None: base = spec.apply(base) for clause in spec.filter_clauses(): count_base = count_base.where(clause) else: base = base.order_by(self.model.version_number.desc()) total = int((await self.session.execute(count_base)).scalar_one()) result = await self.session.execute(base.offset(offset).limit(limit)) return result.scalars().all(), total class SiteThemeBindingRepository(TenantBaseRepository[SiteThemeBinding]): model = SiteThemeBinding async def get_active_for_site( self, tenant_id: UUID, site_id: UUID ) -> SiteThemeBinding | None: stmt = select(self.model).where( self.model.tenant_id == tenant_id, self.model.site_id == site_id, self.model.is_active.is_(True), self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def count_active_by_theme(self, tenant_id: UUID, theme_id: UUID) -> int: stmt = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.theme_id == theme_id, self.model.is_active.is_(True), self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return int(result.scalar_one()) async def count_active_by_version( self, tenant_id: UUID, theme_version_id: UUID ) -> int: stmt = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.theme_version_id == theme_version_id, self.model.is_active.is_(True), self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return int(result.scalar_one()) async def list_filtered( self, tenant_id: UUID, *, offset: int, limit: int, spec: SiteThemeBindingListSpec | None = None, ) -> tuple[Sequence[SiteThemeBinding], int]: base = select(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) count_base = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) if spec is not None: base = spec.apply(base) for clause in spec.filter_clauses(): count_base = count_base.where(clause) else: base = base.order_by(self.model.created_at.desc()) total = int((await self.session.execute(count_base)).scalar_one()) result = await self.session.execute(base.offset(offset).limit(limit)) return result.scalars().all(), total class PageLayoutBindingRepository(TenantBaseRepository[PageLayoutBinding]): model = PageLayoutBinding async def get_active_for_page( self, tenant_id: UUID, page_id: UUID ) -> PageLayoutBinding | None: stmt = select(self.model).where( self.model.tenant_id == tenant_id, self.model.page_id == page_id, self.model.is_active.is_(True), self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def count_active_by_layout(self, tenant_id: UUID, layout_id: UUID) -> int: stmt = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.layout_id == layout_id, self.model.is_active.is_(True), self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return int(result.scalar_one()) async def count_active_by_version( self, tenant_id: UUID, layout_version_id: UUID ) -> int: stmt = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.layout_version_id == layout_version_id, self.model.is_active.is_(True), self.model.is_deleted.is_(False), ) result = await self.session.execute(stmt) return int(result.scalar_one()) async def list_filtered( self, tenant_id: UUID, *, offset: int, limit: int, spec: PageLayoutBindingListSpec | None = None, ) -> tuple[Sequence[PageLayoutBinding], int]: base = select(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) count_base = select(func.count()).select_from(self.model).where( self.model.tenant_id == tenant_id, self.model.is_deleted.is_(False), ) if spec is not None: base = spec.apply(base) for clause in spec.filter_clauses(): count_base = count_base.where(clause) else: base = base.order_by(self.model.created_at.desc()) total = int((await self.session.execute(count_base)).scalar_one()) result = await self.session.execute(base.offset(offset).limit(limit)) return result.scalars().all(), total