"""QR Menu & QR Ordering application services — Phase 12.2.

Shells only: no POS / kitchen / payment posting. Submitting an ordering
session merely flips its status to SUBMITTED for downstream consumers.
"""
from __future__ import annotations

import secrets
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.qr import Cart, CartLine, QrCode, QrMenuSession, QrOrderingSession
from app.models.types import (
    AuditAction,
    CartStatus,
    LifecycleStatus,
    OrderingSessionStatus,
    QrSessionStatus,
    QrTargetType,
)
from app.repositories.foundation import MenuItemRepository, MenuRepository, VenueRepository
from app.repositories.qr import (
    CartLineRepository,
    CartRepository,
    QrCodeRepository,
    QrMenuSessionRepository,
    QrOrderingSessionRepository,
)
from app.schemas.qr import (
    CartCreate,
    CartLineCreate,
    QrCodeCreate,
    QrMenuSessionCreate,
    QrOrderingSessionCreate,
)
from app.services.audit_service import AuditService
from app.validators import (
    validate_code,
    validate_currency_code,
    validate_lifecycle_status,
    validate_non_empty,
    validate_positive_int,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser

TOKEN_BYTES = 24


class _QrBase:
    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 QrCodeService(_QrBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = QrCodeRepository(session)
        self.menus = MenuRepository(session)

    async def create(
        self, tenant_id: UUID, body: QrCodeCreate, *, actor: CurrentUser | None = None
    ) -> QrCode:
        await self._require_venue(tenant_id, body.venue_id)
        target_type = (
            body.target_type
            if isinstance(body.target_type, QrTargetType)
            else QrTargetType(body.target_type)
        )
        if target_type == QrTargetType.MENU:
            if body.menu_id is None:
                raise AppError(
                    "menu_id برای نوع menu الزامی است",
                    status_code=422,
                    error_code="qr_menu_id_required",
                )
            if await self.menus.get(tenant_id, body.menu_id) is None:
                raise NotFoundError("منو یافت نشد", error_code="menu_not_found")
        if target_type == QrTargetType.TABLE and body.table_id is None:
            raise AppError(
                "table_id برای نوع table الزامی است",
                status_code=422,
                error_code="qr_table_id_required",
            )
        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(
                "کد QR تکراری است", status_code=409, error_code="qr_code_exists"
            )
        token = secrets.token_urlsafe(TOKEN_BYTES)
        entity = QrCode(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            code=code,
            token=token,
            name=validate_non_empty(body.name, field="name"),
            description=body.description,
            status=validate_lifecycle_status(body.status),
            target_type=target_type,
            menu_id=body.menu_id,
            dining_area_id=body.dining_area_id,
            table_id=body.table_id,
            ordering_enabled=bool(body.ordering_enabled),
            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="qr_code",
            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.QR_CODE_CREATED,
            aggregate_type="qr_code",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "target_type": entity.target_type.value},
        )
        return entity

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

    async def get_by_token(self, tenant_id: UUID, token: str) -> QrCode:
        entity = await self.repo.get_by_token(tenant_id, token)
        if entity is None:
            raise NotFoundError("QR یافت نشد", error_code="qr_code_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 QrMenuSessionService(_QrBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = QrMenuSessionRepository(session)
        self.qr_codes = QrCodeRepository(session)

    async def create(
        self,
        tenant_id: UUID,
        body: QrMenuSessionCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> QrMenuSession:
        await self._require_venue(tenant_id, body.venue_id)
        qr_code = await self.qr_codes.get(tenant_id, body.qr_code_id)
        if qr_code is None:
            raise NotFoundError("QR یافت نشد", error_code="qr_code_not_found")
        actor_id = actor.user_id if actor else None
        token = secrets.token_urlsafe(TOKEN_BYTES)
        entity = QrMenuSession(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            qr_code_id=body.qr_code_id,
            menu_id=body.menu_id if body.menu_id is not None else qr_code.menu_id,
            table_id=body.table_id if body.table_id is not None else qr_code.table_id,
            session_token=token,
            status=QrSessionStatus.OPEN,
            locale=body.locale,
            guest_label=body.guest_label,
            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="qr_menu_session",
            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.QR_MENU_SESSION_CREATED,
            aggregate_type="qr_menu_session",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"qr_code_id": str(entity.qr_code_id)},
        )
        return entity

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

    async def get_by_token(self, tenant_id: UUID, token: str) -> QrMenuSession:
        entity = await self.repo.get_by_token(tenant_id, token)
        if entity is None:
            raise NotFoundError(
                "جلسه مشاهده منو یافت نشد", error_code="qr_menu_session_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 QrOrderingSessionService(_QrBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = QrOrderingSessionRepository(session)
        self.qr_codes = QrCodeRepository(session)
        self.menu_sessions = QrMenuSessionRepository(session)

    async def create(
        self,
        tenant_id: UUID,
        body: QrOrderingSessionCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> QrOrderingSession:
        await self._require_venue(tenant_id, body.venue_id)
        qr_code = await self.qr_codes.get(tenant_id, body.qr_code_id)
        if qr_code is None:
            raise NotFoundError("QR یافت نشد", error_code="qr_code_not_found")
        if not qr_code.ordering_enabled:
            raise AppError(
                "سفارش‌گیری برای این QR فعال نیست",
                status_code=422,
                error_code="qr_ordering_disabled",
            )
        if body.qr_menu_session_id is not None:
            menu_session = await self.menu_sessions.get(tenant_id, body.qr_menu_session_id)
            if menu_session is None:
                raise NotFoundError(
                    "جلسه مشاهده منو یافت نشد", error_code="qr_menu_session_not_found"
                )
        actor_id = actor.user_id if actor else None
        token = secrets.token_urlsafe(TOKEN_BYTES)
        entity = QrOrderingSession(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            qr_code_id=body.qr_code_id,
            qr_menu_session_id=body.qr_menu_session_id,
            menu_id=body.menu_id if body.menu_id is not None else qr_code.menu_id,
            table_id=body.table_id if body.table_id is not None else qr_code.table_id,
            session_token=token,
            status=OrderingSessionStatus.DRAFT,
            guest_label=body.guest_label,
            notes=body.notes,
            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="qr_ordering_session",
            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.QR_ORDERING_SESSION_CREATED,
            aggregate_type="qr_ordering_session",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"qr_code_id": str(entity.qr_code_id)},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> QrOrderingSession:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError(
                "جلسه سفارش‌گیری یافت نشد", error_code="qr_ordering_session_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 submit(
        self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None
    ) -> QrOrderingSession:
        """Marks the ordering session as submitted — shell only, no POS/kitchen dispatch."""
        entity = await self.get(tenant_id, entity_id)
        if entity.status in (OrderingSessionStatus.CANCELLED, OrderingSessionStatus.CLOSED):
            raise AppError(
                "جلسه سفارش‌گیری بسته یا لغو شده است",
                status_code=409,
                error_code="qr_ordering_session_closed",
            )
        if entity.status == OrderingSessionStatus.SUBMITTED:
            raise AppError(
                "جلسه سفارش‌گیری قبلاً ثبت شده است",
                status_code=409,
                error_code="qr_ordering_session_already_submitted",
            )
        actor_id = actor.user_id if actor else None
        entity.status = OrderingSessionStatus.SUBMITTED
        entity.updated_by = actor_id
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="qr_ordering_session",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=actor_id,
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=HospitalityEventType.QR_ORDERING_SESSION_SUBMITTED,
            aggregate_type="qr_ordering_session",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"status": entity.status.value},
        )
        return entity


class CartService(_QrBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = CartRepository(session)
        self.ordering_sessions = QrOrderingSessionRepository(session)

    async def create(
        self, tenant_id: UUID, body: CartCreate, *, actor: CurrentUser | None = None
    ) -> Cart:
        await self._require_venue(tenant_id, body.venue_id)
        ordering_session = await self.ordering_sessions.get(tenant_id, body.ordering_session_id)
        if ordering_session is None:
            raise NotFoundError(
                "جلسه سفارش‌گیری یافت نشد", error_code="qr_ordering_session_not_found"
            )
        if await self.repo.get_by_ordering_session(tenant_id, body.ordering_session_id) is not None:
            raise AppError(
                "سبد خرید برای این جلسه از قبل وجود دارد",
                status_code=409,
                error_code="cart_exists",
            )
        actor_id = actor.user_id if actor else None
        entity = Cart(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            ordering_session_id=body.ordering_session_id,
            status=CartStatus.ACTIVE,
            currency_code=validate_currency_code(body.currency_code),
            notes=body.notes,
            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="cart",
            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.CART_CREATED,
            aggregate_type="cart",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"ordering_session_id": str(entity.ordering_session_id)},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> Cart:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("سبد خرید یافت نشد", error_code="cart_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 CartLineService(_QrBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = CartLineRepository(session)
        self.carts = CartRepository(session)
        self.items = MenuItemRepository(session)

    async def add(
        self, tenant_id: UUID, body: CartLineCreate, *, actor: CurrentUser | None = None
    ) -> CartLine:
        await self._require_venue(tenant_id, body.venue_id)
        cart = await self.carts.get(tenant_id, body.cart_id)
        if cart is None:
            raise NotFoundError("سبد خرید یافت نشد", error_code="cart_not_found")
        if cart.status != CartStatus.ACTIVE:
            raise AppError(
                "سبد خرید فعال نیست", status_code=409, error_code="cart_not_active"
            )
        item = await self.items.get(tenant_id, body.menu_item_id)
        if item is None:
            raise NotFoundError("آیتم منو یافت نشد", error_code="menu_item_not_found")
        quantity = validate_positive_int(body.quantity, field="quantity")
        actor_id = actor.user_id if actor else None
        entity = CartLine(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            cart_id=body.cart_id,
            menu_item_id=body.menu_item_id,
            quantity=quantity,
            unit_price=item.base_price,
            currency_code=item.currency_code,
            modifier_option_ids=(
                [str(v) for v in body.modifier_option_ids]
                if body.modifier_option_ids
                else None
            ),
            line_notes=body.line_notes,
            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="cart_line",
            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.CART_LINE_ADDED,
            aggregate_type="cart_line",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"cart_id": str(entity.cart_id), "menu_item_id": str(entity.menu_item_id)},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> CartLine:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("خط سبد خرید یافت نشد", error_code="cart_line_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)
