"""Digital Menu Catalog application services — Phase 12.1."""
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.catalog import (
    Allergen,
    AvailabilityWindow,
    MenuItemAllergen,
    MenuItemModifier,
    MenuLocalization,
    MenuMediaRef,
    ModifierGroup,
    ModifierOption,
)
from app.models.types import (
    AuditAction,
    AvailabilityScope,
    LocalizationTarget,
    MediaKind,
)
from app.repositories.catalog import (
    AllergenRepository,
    AvailabilityWindowRepository,
    MenuItemAllergenRepository,
    MenuItemModifierRepository,
    MenuLocalizationRepository,
    MenuMediaRefRepository,
    ModifierGroupRepository,
    ModifierOptionRepository,
)
from app.repositories.foundation import (
    MenuItemRepository,
    MenuRepository,
    VenueRepository,
)
from app.schemas.catalog import (
    AllergenCreate,
    AvailabilityWindowCreate,
    MenuItemAllergenLinkCreate,
    MenuItemModifierLinkCreate,
    MenuLocalizationUpsert,
    MenuMediaRefCreate,
    ModifierGroupCreate,
    ModifierOptionCreate,
)
from app.services.audit_service import AuditService
from app.validators import (
    validate_code,
    validate_currency_code,
    validate_file_ref,
    validate_lifecycle_status,
    validate_locale,
    validate_modifier_selection,
    validate_non_empty,
    validate_non_negative_int,
    validate_time_range,
    validate_weekday,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


class _CatalogBase:
    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 = await self.venues.get(tenant_id, venue_id)
        if venue is None:
            raise NotFoundError("واحد پذیرایی یافت نشد", error_code="venue_not_found")
        return venue


class AllergenService(_CatalogBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = AllergenRepository(session)

    async def create(self, tenant_id: UUID, body: AllergenCreate, *, actor: CurrentUser | None = None) -> Allergen:
        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="allergen_code_exists")
        entity = Allergen(
            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), icon_ref=body.icon_ref,
            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="allergen", 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.ALLERGEN_CREATED, aggregate_type="allergen", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Allergen:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("آلرژن یافت نشد", error_code="allergen_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 ModifierGroupService(_CatalogBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = ModifierGroupRepository(session)

    async def create(self, tenant_id: UUID, body: ModifierGroupCreate, *, actor: CurrentUser | None = None) -> ModifierGroup:
        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("کد گروه modifier تکراری است", status_code=409, error_code="modifier_group_code_exists")
        mode, min_v, max_v = validate_modifier_selection(
            body.selection_mode, min_select=body.min_select, max_select=body.max_select, is_required=body.is_required
        )
        entity = ModifierGroup(
            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), selection_mode=mode,
            min_select=min_v, max_select=max_v, is_required=bool(body.is_required),
            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="modifier_group", 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.MODIFIER_GROUP_CREATED, aggregate_type="modifier_group", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> ModifierGroup:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("گروه modifier یافت نشد", error_code="modifier_group_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 ModifierOptionService(_CatalogBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = ModifierOptionRepository(session)
        self.groups = ModifierGroupRepository(session)

    async def create(self, tenant_id: UUID, body: ModifierOptionCreate, *, actor: CurrentUser | None = None) -> ModifierOption:
        await self._require_venue(tenant_id, body.venue_id)
        group = await self.groups.get(tenant_id, body.modifier_group_id)
        if group is None:
            raise NotFoundError("گروه modifier یافت نشد", error_code="modifier_group_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.modifier_group_id, code) is not None:
            raise AppError("کد گزینه modifier تکراری است", status_code=409, error_code="modifier_option_code_exists")
        entity = ModifierOption(
            tenant_id=tenant_id, venue_id=body.venue_id, modifier_group_id=body.modifier_group_id,
            code=code, name=validate_non_empty(body.name, field="name"), description=body.description,
            status=validate_lifecycle_status(body.status),
            price_delta=int(body.price_delta),
            currency_code=validate_currency_code(body.currency_code),
            is_default=bool(body.is_default), 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="modifier_option", 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.MODIFIER_OPTION_CREATED, aggregate_type="modifier_option", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> ModifierOption:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("گزینه modifier یافت نشد", error_code="modifier_option_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 MenuItemModifierService(_CatalogBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MenuItemModifierRepository(session)
        self.items = MenuItemRepository(session)
        self.groups = ModifierGroupRepository(session)

    async def link(self, tenant_id: UUID, body: MenuItemModifierLinkCreate, *, actor: CurrentUser | None = None) -> MenuItemModifier:
        await self._require_venue(tenant_id, body.venue_id)
        item = await self.items.get(tenant_id, body.menu_item_id)
        if item is None:
            raise NotFoundError("آیتم منو یافت نشد", error_code="menu_item_not_found")
        group = await self.groups.get(tenant_id, body.modifier_group_id)
        if group is None:
            raise NotFoundError("گروه modifier یافت نشد", error_code="modifier_group_not_found")
        if await self.repo.get_link(tenant_id, body.menu_item_id, body.modifier_group_id) is not None:
            raise AppError("لینک modifier تکراری است", status_code=409, error_code="menu_item_modifier_exists")
        actor_id = actor.user_id if actor else None
        entity = MenuItemModifier(
            tenant_id=tenant_id, venue_id=body.venue_id, menu_item_id=body.menu_item_id,
            modifier_group_id=body.modifier_group_id, sort_order=body.sort_order or 0,
            is_required_override=body.is_required_override, 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_modifier", entity_id=entity.id, action=AuditAction.LINK, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.MENU_ITEM_MODIFIER_LINKED, aggregate_type="menu_item_modifier", aggregate_id=entity.id, tenant_id=tenant_id, payload={"menu_item_id": str(entity.menu_item_id)})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> MenuItemModifier:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("لینک modifier یافت نشد", error_code="menu_item_modifier_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 MenuItemAllergenService(_CatalogBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MenuItemAllergenRepository(session)
        self.items = MenuItemRepository(session)
        self.allergens = AllergenRepository(session)

    async def link(self, tenant_id: UUID, body: MenuItemAllergenLinkCreate, *, actor: CurrentUser | None = None) -> MenuItemAllergen:
        await self._require_venue(tenant_id, body.venue_id)
        item = await self.items.get(tenant_id, body.menu_item_id)
        if item is None:
            raise NotFoundError("آیتم منو یافت نشد", error_code="menu_item_not_found")
        allergen = await self.allergens.get(tenant_id, body.allergen_id)
        if allergen is None:
            raise NotFoundError("آلرژن یافت نشد", error_code="allergen_not_found")
        if await self.repo.get_link(tenant_id, body.menu_item_id, body.allergen_id) is not None:
            raise AppError("لینک آلرژن تکراری است", status_code=409, error_code="menu_item_allergen_exists")
        actor_id = actor.user_id if actor else None
        entity = MenuItemAllergen(
            tenant_id=tenant_id, venue_id=body.venue_id, menu_item_id=body.menu_item_id,
            allergen_id=body.allergen_id, is_contains=bool(body.is_contains),
            is_may_contain=bool(body.is_may_contain), 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_allergen", entity_id=entity.id, action=AuditAction.LINK, actor_user_id=actor_id)
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(event_type=HospitalityEventType.MENU_ITEM_ALLERGEN_LINKED, aggregate_type="menu_item_allergen", aggregate_id=entity.id, tenant_id=tenant_id, payload={"menu_item_id": str(entity.menu_item_id)})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> MenuItemAllergen:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("لینک آلرژن یافت نشد", error_code="menu_item_allergen_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 AvailabilityWindowService(_CatalogBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = AvailabilityWindowRepository(session)

    async def create(self, tenant_id: UUID, body: AvailabilityWindowCreate, *, actor: CurrentUser | None = None) -> AvailabilityWindow:
        await self._require_venue(tenant_id, body.venue_id)
        if not isinstance(body.scope, AvailabilityScope):
            try:
                scope = AvailabilityScope(body.scope)
            except ValueError as exc:
                raise AppError("scope نامعتبر است", status_code=422, error_code="invalid_availability_scope") from exc
        else:
            scope = body.scope
        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, scope, body.target_id, code) is not None:
            raise AppError("کد پنجره دسترسی تکراری است", status_code=409, error_code="availability_window_code_exists")
        start, end = validate_time_range(body.start_time, body.end_time)
        entity = AvailabilityWindow(
            tenant_id=tenant_id, venue_id=body.venue_id, scope=scope, target_id=body.target_id,
            code=code, name=validate_non_empty(body.name, field="name"),
            status=validate_lifecycle_status(body.status), weekday=validate_weekday(body.weekday),
            start_time=start, end_time=end,
            timezone=validate_non_empty(body.timezone, field="timezone"),
            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="availability_window", 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.AVAILABILITY_WINDOW_CREATED, aggregate_type="availability_window", aggregate_id=entity.id, tenant_id=tenant_id, payload={"code": entity.code, "scope": entity.scope.value})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> AvailabilityWindow:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("پنجره دسترسی یافت نشد", error_code="availability_window_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 MenuMediaRefService(_CatalogBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MenuMediaRefRepository(session)
        self.menus = MenuRepository(session)
        self.items = MenuItemRepository(session)

    async def create(self, tenant_id: UUID, body: MenuMediaRefCreate, *, actor: CurrentUser | None = None) -> MenuMediaRef:
        await self._require_venue(tenant_id, body.venue_id)
        if body.menu_id is None and body.menu_item_id is None:
            raise AppError("menu_id یا menu_item_id الزامی است", status_code=422, error_code="media_target_required")
        if body.menu_id is not None and await self.menus.get(tenant_id, body.menu_id) is None:
            raise NotFoundError("منو یافت نشد", error_code="menu_not_found")
        if body.menu_item_id is not None and await self.items.get(tenant_id, body.menu_item_id) is None:
            raise NotFoundError("آیتم منو یافت نشد", error_code="menu_item_not_found")
        actor_id = actor.user_id if actor else None
        kind = body.kind if isinstance(body.kind, MediaKind) else MediaKind(body.kind)
        entity = MenuMediaRef(
            tenant_id=tenant_id, venue_id=body.venue_id, menu_id=body.menu_id,
            menu_item_id=body.menu_item_id, kind=kind, file_ref=validate_file_ref(body.file_ref),
            alt_text=body.alt_text, sort_order=body.sort_order or 0,
            status=validate_lifecycle_status(body.status), 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_media_ref", 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_MEDIA_REF_CREATED, aggregate_type="menu_media_ref", aggregate_id=entity.id, tenant_id=tenant_id, payload={"file_ref": entity.file_ref})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> MenuMediaRef:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("رسانه منو یافت نشد", error_code="menu_media_ref_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 MenuLocalizationService(_CatalogBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = MenuLocalizationRepository(session)

    async def upsert(self, tenant_id: UUID, body: MenuLocalizationUpsert, *, actor: CurrentUser | None = None) -> MenuLocalization:
        await self._require_venue(tenant_id, body.venue_id)
        target_type = body.target_type if isinstance(body.target_type, LocalizationTarget) else LocalizationTarget(body.target_type)
        actor_id = actor.user_id if actor else None
        locale = validate_locale(body.locale)
        entity = await self.repo.get_locale(tenant_id, target_type, body.target_id, locale)
        if entity is None:
            entity = MenuLocalization(
                tenant_id=tenant_id, venue_id=body.venue_id, target_type=target_type,
                target_id=body.target_id, locale=locale, name=body.name, description=body.description,
                status=validate_lifecycle_status(body.status), created_by=actor_id, updated_by=actor_id,
            )
            await self.repo.add(entity)
            action = AuditAction.CREATE
        else:
            entity.name = body.name
            entity.description = body.description
            entity.status = validate_lifecycle_status(body.status)
            entity.updated_by = actor_id
            action = AuditAction.UPDATE
        await self.audit.record(tenant_id=tenant_id, entity_type="menu_localization", 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.MENU_LOCALIZATION_UPSERTED, aggregate_type="menu_localization", aggregate_id=entity.id, tenant_id=tenant_id, payload={"locale": entity.locale, "target_type": entity.target_type.value})
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> MenuLocalization:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("ترجمه منو یافت نشد", error_code="menu_localization_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)
