"""Hospitality foundation application services — Phase 12.0."""
from __future__ import annotations

from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import InMemoryEventPublisher, get_event_publisher
from app.events.types import HospitalityEventType
from app.models.foundation import (
    Branch,
    BundleDefinition,
    DiningArea,
    DiningTable,
    FeatureToggle,
    HospitalityConfiguration,
    HospitalityEvent,
    HospitalityPermission,
    HospitalityRole,
    HospitalitySetting,
    Menu,
    MenuCategory,
    MenuItem,
    TenantBundle,
    Venue,
)
from app.models.types import AuditAction, BundleStatus
from app.repositories.foundation import (
    BranchRepository,
    BundleDefinitionRepository,
    DiningAreaRepository,
    DiningTableRepository,
    FeatureToggleRepository,
    HospitalityConfigurationRepository,
    HospitalityEventRepository,
    HospitalityPermissionRepository,
    HospitalityRoleRepository,
    HospitalitySettingRepository,
    MenuCategoryRepository,
    MenuItemRepository,
    MenuRepository,
    TenantBundleRepository,
    VenueRepository,
)
from app.schemas.foundation import (
    BranchCreate,
    BranchUpdate,
    BundleDefinitionCreate,
    DiningAreaCreate,
    DiningTableCreate,
    FeatureToggleUpsert,
    HospitalityConfigurationCreate,
    HospitalityConfigurationUpdate,
    HospitalityEventCreate,
    HospitalityPermissionCreate,
    HospitalityRoleCreate,
    HospitalitySettingUpsert,
    MenuCategoryCreate,
    MenuCreate,
    MenuItemCreate,
    MenuUpdate,
    TenantBundleActivate,
    VenueCreate,
    VenueUpdate,
)
from app.services.audit_service import AuditService
from app.validators import (
    ensure_optimistic_version,
    forbid_format_specific_engines,
    validate_bundle_key,
    validate_bundle_status,
    validate_code,
    validate_currency_code,
    validate_feature_key,
    validate_lifecycle_status,
    validate_menu_status,
    validate_non_empty,
    validate_non_negative_int,
    validate_setting_key,
    validate_table_status,
    validate_venue_format,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


def _apply_update(entity, data: dict) -> None:
    for key, value in data.items():
        setattr(entity, key, value)


class _BaseService:
    def __init__(
        self,
        session: AsyncSession,
        publisher: InMemoryEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.audit = AuditService(session)
        self.publisher = publisher or get_event_publisher()
        self.venues = VenueRepository(session)

    async def _require_venue(self, tenant_id: UUID, venue_id: UUID) -> Venue:
        venue = await self.venues.get(tenant_id, venue_id)
        if venue is None:
            raise NotFoundError("واحد پذیرایی یافت نشد", error_code="venue_not_found")
        return venue


class VenueService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = VenueRepository(session)

    async def create(self, tenant_id: UUID, body: VenueCreate, *, actor: CurrentUser | None = None) -> Venue:
        forbid_format_specific_engines(body.model_dump())
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code) is not None:
            raise AppError("کد واحد پذیرایی تکراری است", status_code=409, error_code="venue_code_exists")
        entity = Venue(
            tenant_id=tenant_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            description=body.description,
            status=validate_lifecycle_status(body.status),
            venue_format=validate_venue_format(body.venue_format),
            legal_name=body.legal_name,
            timezone=validate_non_empty(body.timezone, field="timezone"),
            language=validate_non_empty(body.language, field="language"),
            currency_code=validate_currency_code(body.currency_code),
            settings=body.settings,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id, entity_type="venue", entity_id=entity.id,
            action=AuditAction.CREATE, actor_user_id=actor_id,
            message=f"Venue {entity.code} created",
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=HospitalityEventType.VENUE_CREATED,
            aggregate_type="venue", aggregate_id=entity.id, tenant_id=tenant_id,
            payload={"code": entity.code, "status": entity.status.value, "venue_format": entity.venue_format.value},
        )
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: VenueUpdate, *, actor: CurrentUser | None = None) -> Venue:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None:
            data["status"] = validate_lifecycle_status(data["status"])
        if "venue_format" in data and data["venue_format"] is not None:
            data["venue_format"] = validate_venue_format(data["venue_format"])
        if "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(
            tenant_id=tenant_id, entity_type="venue", entity_id=entity.id,
            action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=HospitalityEventType.VENUE_UPDATED,
            aggregate_type="venue", aggregate_id=entity.id, tenant_id=tenant_id,
            payload={"code": entity.code, "status": entity.status.value},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Venue:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("واحد پذیرایی یافت نشد", error_code="venue_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Venue:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(
            tenant_id=tenant_id, entity_type="venue", entity_id=entity.id,
            action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class BranchService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = BranchRepository(session)

    async def create(self, tenant_id: UUID, body: BranchCreate, *, actor: CurrentUser | None = None) -> Branch:
        await self._require_venue(tenant_id, body.venue_id)
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.venue_id, code) is not None:
            raise AppError("کد شعبه تکراری است", status_code=409, error_code="branch_code_exists")
        entity = Branch(
            tenant_id=tenant_id, venue_id=body.venue_id, code=code,
            name=validate_non_empty(body.name, field="name"), description=body.description,
            status=validate_lifecycle_status(body.status), address=body.address,
            timezone=body.timezone, working_hours=body.working_hours,
            metadata_json=body.metadata_json, created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="branch", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.BRANCH_CREATED, aggregate_type="branch", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code, "venue_id": str(entity.venue_id)})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: BranchUpdate, *, actor: CurrentUser | None = None) -> Branch:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None:
            data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data)
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="branch", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.BRANCH_UPDATED, aggregate_type="branch", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Branch:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("شعبه یافت نشد", error_code="branch_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Branch:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="branch", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class DiningAreaService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = DiningAreaRepository(session)

    async def create(self, tenant_id: UUID, body: DiningAreaCreate, *, actor: CurrentUser | None = None) -> DiningArea:
        await self._require_venue(tenant_id, body.venue_id)
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.venue_id, code) is not None:
            raise AppError("کد فضای پذیرایی تکراری است", status_code=409, error_code="dining_area_code_exists")
        entity = DiningArea(
            tenant_id=tenant_id, venue_id=body.venue_id, branch_id=body.branch_id, code=code,
            name=validate_non_empty(body.name, field="name"), description=body.description,
            status=validate_lifecycle_status(body.status), sort_order=body.sort_order or 0,
            metadata_json=body.metadata_json, created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="dining_area", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.DINING_AREA_CREATED, aggregate_type="dining_area", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> DiningArea:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("فضای پذیرایی یافت نشد", error_code="dining_area_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class DiningTableService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = DiningTableRepository(session)

    async def create(self, tenant_id: UUID, body: DiningTableCreate, *, actor: CurrentUser | None = None) -> DiningTable:
        await self._require_venue(tenant_id, body.venue_id)
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.venue_id, code) is not None:
            raise AppError("کد میز تکراری است", status_code=409, error_code="table_code_exists")
        entity = DiningTable(
            tenant_id=tenant_id, venue_id=body.venue_id, dining_area_id=body.dining_area_id, code=code,
            name=validate_non_empty(body.name, field="name"),
            capacity=validate_non_negative_int(body.capacity, field="capacity") or 2,
            status=validate_table_status(body.status), qr_token=body.qr_token,
            metadata_json=body.metadata_json, created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="dining_table", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.TABLE_CREATED, aggregate_type="dining_table", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> DiningTable:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("میز یافت نشد", error_code="table_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class MenuService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MenuRepository(session)

    async def create(self, tenant_id: UUID, body: MenuCreate, *, actor: CurrentUser | None = None) -> Menu:
        await self._require_venue(tenant_id, body.venue_id)
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.venue_id, code) is not None:
            raise AppError("کد منو تکراری است", status_code=409, error_code="menu_code_exists")
        entity = Menu(
            tenant_id=tenant_id, venue_id=body.venue_id, code=code,
            name=validate_non_empty(body.name, field="name"), description=body.description,
            status=validate_menu_status(body.status),
            currency_code=validate_currency_code(body.currency_code),
            is_default=bool(body.is_default), metadata_json=body.metadata_json,
            created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="menu", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.MENU_CREATED, aggregate_type="menu", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: MenuUpdate, *, actor: CurrentUser | None = None) -> Menu:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None:
            data["status"] = validate_menu_status(data["status"])
        if "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="menu", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.MENU_UPDATED, aggregate_type="menu", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Menu:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("منو یافت نشد", error_code="menu_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def soft_delete(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> Menu:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=actor.user_id if actor else None)
        await self.audit.record(tenant_id=tenant_id, entity_type="menu", entity_id=entity.id, action=AuditAction.DELETE, actor_user_id=actor.user_id if actor else None)
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class MenuCategoryService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MenuCategoryRepository(session)
        self.menus = MenuRepository(session)

    async def create(self, tenant_id: UUID, body: MenuCategoryCreate, *, actor: CurrentUser | None = None) -> MenuCategory:
        await self._require_venue(tenant_id, body.venue_id)
        menu = await self.menus.get(tenant_id, body.menu_id)
        if menu is None:
            raise NotFoundError("منو یافت نشد", error_code="menu_not_found")
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.menu_id, code) is not None:
            raise AppError("کد دسته منو تکراری است", status_code=409, error_code="menu_category_code_exists")
        entity = MenuCategory(
            tenant_id=tenant_id, venue_id=body.venue_id, menu_id=body.menu_id, code=code,
            name=validate_non_empty(body.name, field="name"), description=body.description,
            status=validate_lifecycle_status(body.status), sort_order=body.sort_order or 0,
            created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="menu_category", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.MENU_CATEGORY_CREATED, aggregate_type="menu_category", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> MenuCategory:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("دسته منو یافت نشد", error_code="menu_category_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class MenuItemService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MenuItemRepository(session)
        self.menus = MenuRepository(session)

    async def create(self, tenant_id: UUID, body: MenuItemCreate, *, actor: CurrentUser | None = None) -> MenuItem:
        await self._require_venue(tenant_id, body.venue_id)
        menu = await self.menus.get(tenant_id, body.menu_id)
        if menu is None:
            raise NotFoundError("منو یافت نشد", error_code="menu_not_found")
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, body.menu_id, code) is not None:
            raise AppError("کد آیتم منو تکراری است", status_code=409, error_code="menu_item_code_exists")
        entity = MenuItem(
            tenant_id=tenant_id, venue_id=body.venue_id, menu_id=body.menu_id, category_id=body.category_id,
            code=code, name=validate_non_empty(body.name, field="name"), description=body.description,
            status=validate_lifecycle_status(body.status),
            base_price=validate_non_negative_int(body.base_price, field="base_price") or 0,
            currency_code=validate_currency_code(body.currency_code),
            is_available=bool(body.is_available), sort_order=body.sort_order or 0,
            attributes=body.attributes,
            preparation_minutes=validate_non_negative_int(body.preparation_minutes, field="preparation_minutes"),
            calories=validate_non_negative_int(body.calories, field="calories"),
            spicy_level=validate_non_negative_int(body.spicy_level, field="spicy_level") or 0,
            is_vegetarian=bool(body.is_vegetarian),
            is_vegan=bool(body.is_vegan),
            is_gluten_free=bool(body.is_gluten_free),
            tags=body.tags,
            primary_media_ref=body.primary_media_ref,
            created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="menu_item", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.MENU_ITEM_CREATED, aggregate_type="menu_item", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> MenuItem:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("آیتم منو یافت نشد", error_code="menu_item_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class BundleDefinitionService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = BundleDefinitionRepository(session)

    async def create(self, tenant_id: UUID, body: BundleDefinitionCreate, *, actor: CurrentUser | None = None) -> BundleDefinition:
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        bundle_key = validate_bundle_key(body.bundle_key)
        if await self.repo.get_by_code(tenant_id, code) is not None:
            raise AppError("کد بسته تکراری است", status_code=409, error_code="bundle_code_exists")
        if await self.repo.get_by_key(tenant_id, bundle_key) is not None:
            raise AppError("کلید بسته تکراری است", status_code=409, error_code="bundle_key_exists")
        entity = BundleDefinition(
            tenant_id=tenant_id, code=code, bundle_key=bundle_key,
            name=validate_non_empty(body.name, field="name"), description=body.description,
            status=validate_bundle_status(body.status), feature_keys=body.feature_keys,
            permission_prefixes=body.permission_prefixes, api_prefixes=body.api_prefixes,
            metadata_json=body.metadata_json, created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="bundle_definition", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.BUNDLE_DEFINITION_CREATED, aggregate_type="bundle_definition", aggregate_id=entity.id, tenant_id=tenant_id, payload={"bundle_key": entity.bundle_key.value})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> BundleDefinition:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("تعریف بسته یافت نشد", error_code="bundle_definition_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class TenantBundleService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = TenantBundleRepository(session)
        self.definitions = BundleDefinitionRepository(session)

    async def activate(self, tenant_id: UUID, body: TenantBundleActivate, *, actor: CurrentUser | None = None) -> TenantBundle:
        definition = await self.definitions.get(tenant_id, body.bundle_definition_id)
        if definition is None:
            raise NotFoundError("تعریف بسته یافت نشد", error_code="bundle_definition_not_found")
        if body.venue_id is not None:
            await self._require_venue(tenant_id, body.venue_id)
        existing = await self.repo.get_active(tenant_id, definition.bundle_key, body.venue_id)
        if existing is not None:
            raise AppError("بسته قبلاً فعال است", status_code=409, error_code="tenant_bundle_active")
        actor_id = actor.user_id if actor else None
        entity = TenantBundle(
            tenant_id=tenant_id, venue_id=body.venue_id, bundle_definition_id=definition.id,
            bundle_key=definition.bundle_key, status=BundleStatus.ACTIVE, activated_by=actor_id,
            metadata_json=body.metadata_json, created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="tenant_bundle", entity_id=entity.id, action=AuditAction.ACTIVATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.TENANT_BUNDLE_ACTIVATED, aggregate_type="tenant_bundle", aggregate_id=entity.id, tenant_id=tenant_id, payload={"bundle_key": entity.bundle_key.value})
        return entity

    async def deactivate(self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None) -> TenantBundle:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("بسته فعال یافت نشد", error_code="tenant_bundle_not_found")
        entity.status = BundleStatus.SUSPENDED
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="tenant_bundle", entity_id=entity.id, action=AuditAction.DEACTIVATE, actor_user_id=entity.updated_by)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.TENANT_BUNDLE_DEACTIVATED, aggregate_type="tenant_bundle", aggregate_id=entity.id, tenant_id=tenant_id, payload={"bundle_key": entity.bundle_key.value})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> TenantBundle:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("بسته فعال یافت نشد", error_code="tenant_bundle_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def list_active(self, tenant_id: UUID, venue_id: UUID | None = None):
        return await self.repo.list_active(tenant_id, venue_id)


class FeatureToggleService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = FeatureToggleRepository(session)

    async def upsert(self, tenant_id: UUID, body: FeatureToggleUpsert, *, actor: CurrentUser | None = None) -> FeatureToggle:
        if body.venue_id is not None:
            await self._require_venue(tenant_id, body.venue_id)
        actor_id = actor.user_id if actor else None
        key = validate_feature_key(body.feature_key)
        entity = await self.repo.get_by_key(tenant_id, key, body.venue_id)
        if entity is None:
            entity = FeatureToggle(
                tenant_id=tenant_id, venue_id=body.venue_id, feature_key=key, enabled=bool(body.enabled),
                bundle_key=validate_bundle_key(body.bundle_key) if body.bundle_key else None,
                metadata_json=body.metadata_json, created_by=actor_id, updated_by=actor_id,
            )
            await self.repo.add(entity)
            action = AuditAction.CREATE
        else:
            entity.enabled = bool(body.enabled)
            if body.bundle_key is not None:
                entity.bundle_key = validate_bundle_key(body.bundle_key)
            if body.metadata_json is not None:
                entity.metadata_json = body.metadata_json
            entity.updated_by = actor_id
            action = AuditAction.UPDATE
        await self.audit.record(tenant_id=tenant_id, entity_type="feature_toggle", entity_id=entity.id, action=action, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.FEATURE_TOGGLE_UPSERTED, aggregate_type="feature_toggle", aggregate_id=entity.id, tenant_id=tenant_id, payload={"feature_key": entity.feature_key, "enabled": entity.enabled})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> FeatureToggle:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("سوییچ قابلیت یافت نشد", error_code="feature_toggle_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)

    async def list_enabled(self, tenant_id: UUID, venue_id: UUID | None = None):
        return await self.repo.list_enabled(tenant_id, venue_id)


class HospitalityRoleService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = HospitalityRoleRepository(session)

    async def create(self, tenant_id: UUID, body: HospitalityRoleCreate, *, actor: CurrentUser | None = None) -> HospitalityRole:
        if body.venue_id is not None:
            await self._require_venue(tenant_id, body.venue_id)
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code) is not None:
            raise AppError("کد نقش تکراری است", status_code=409, error_code="role_code_exists")
        entity = HospitalityRole(
            tenant_id=tenant_id, venue_id=body.venue_id, code=code,
            name=validate_non_empty(body.name, field="name"), description=body.description,
            status=validate_lifecycle_status(body.status), permission_keys=body.permission_keys,
            created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="hospitality_role", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.ROLE_CREATED, aggregate_type="hospitality_role", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> HospitalityRole:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("نقش یافت نشد", error_code="role_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class HospitalityPermissionService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = HospitalityPermissionRepository(session)

    async def create(self, tenant_id: UUID, body: HospitalityPermissionCreate, *, actor: CurrentUser | None = None) -> HospitalityPermission:
        actor_id = actor.user_id if actor else None
        code = validate_non_empty(body.code, field="code")
        if await self.repo.get_by_code(tenant_id, code) is not None:
            raise AppError("کد مجوز تکراری است", status_code=409, error_code="permission_code_exists")
        entity = HospitalityPermission(
            tenant_id=tenant_id, code=code, name=validate_non_empty(body.name, field="name"),
            description=body.description,
            bundle_key=validate_bundle_key(body.bundle_key) if body.bundle_key else None,
            status=validate_lifecycle_status(body.status), created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="hospitality_permission", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.PERMISSION_CREATED, aggregate_type="hospitality_permission", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> HospitalityPermission:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("مجوز یافت نشد", error_code="permission_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class HospitalityConfigurationService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = HospitalityConfigurationRepository(session)

    async def create(self, tenant_id: UUID, body: HospitalityConfigurationCreate, *, actor: CurrentUser | None = None) -> HospitalityConfiguration:
        if body.venue_id is not None:
            await self._require_venue(tenant_id, body.venue_id)
        actor_id = actor.user_id if actor else None
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code) is not None:
            raise AppError("کد پیکربندی تکراری است", status_code=409, error_code="configuration_code_exists")
        entity = HospitalityConfiguration(
            tenant_id=tenant_id, venue_id=body.venue_id, branch_id=body.branch_id, code=code,
            name=validate_non_empty(body.name, field="name"), config=body.config,
            status=validate_lifecycle_status(body.status), created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="hospitality_configuration", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.CONFIGURATION_CREATED, aggregate_type="hospitality_configuration", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def update(self, tenant_id: UUID, entity_id: UUID, body: HospitalityConfigurationUpdate, *, actor: CurrentUser | None = None) -> HospitalityConfiguration:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity.version, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], field="name")
        if "status" in data and data["status"] is not None:
            data["status"] = validate_lifecycle_status(data["status"])
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = actor.user_id if actor else None
        await self.audit.record(tenant_id=tenant_id, entity_type="hospitality_configuration", entity_id=entity.id, action=AuditAction.UPDATE, actor_user_id=entity.updated_by, changes=data)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.CONFIGURATION_UPDATED, aggregate_type="hospitality_configuration", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> HospitalityConfiguration:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("پیکربندی یافت نشد", error_code="configuration_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class HospitalityEventService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = HospitalityEventRepository(session)

    async def create(self, tenant_id: UUID, body: HospitalityEventCreate, *, actor: CurrentUser | None = None) -> HospitalityEvent:
        await self._require_venue(tenant_id, body.venue_id)
        actor_id = actor.user_id if actor else None
        entity = HospitalityEvent(
            tenant_id=tenant_id, venue_id=body.venue_id,
            event_type=validate_non_empty(body.event_type, field="event_type"),
            title=validate_non_empty(body.title, field="title"), payload=body.payload,
            status=validate_lifecycle_status(body.status), created_by=actor_id, updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(tenant_id=tenant_id, entity_type="hospitality_event", entity_id=entity.id, action=AuditAction.CREATE, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.HOSPITALITY_EVENT_CREATED, aggregate_type="hospitality_event", aggregate_id=entity.id, tenant_id=tenant_id, payload={"event_type": entity.event_type})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> HospitalityEvent:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("رویداد یافت نشد", error_code="event_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)


class HospitalitySettingService(_BaseService):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = HospitalitySettingRepository(session)

    async def upsert(self, tenant_id: UUID, body: HospitalitySettingUpsert, *, actor: CurrentUser | None = None) -> HospitalitySetting:
        if body.venue_id is not None:
            await self._require_venue(tenant_id, body.venue_id)
        actor_id = actor.user_id if actor else None
        key = validate_setting_key(body.key)
        entity = await self.repo.get_by_key(tenant_id, key, body.venue_id)
        if entity is None:
            entity = HospitalitySetting(
                tenant_id=tenant_id, venue_id=body.venue_id, key=key, value=body.value,
                created_by=actor_id, updated_by=actor_id,
            )
            await self.repo.add(entity)
            action = AuditAction.CREATE
        else:
            entity.value = body.value
            entity.updated_by = actor_id
            action = AuditAction.UPDATE
        await self.audit.record(tenant_id=tenant_id, entity_type="hospitality_setting", entity_id=entity.id, action=action, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.SETTING_UPSERTED, aggregate_type="hospitality_setting", aggregate_id=entity.id, tenant_id=tenant_id, payload={"key": entity.key})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> HospitalitySetting:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("تنظیمات یافت نشد", error_code="setting_not_found")
        return entity

    async def list(self, tenant_id: UUID, *, offset: int, limit: int):
        return await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit)
