"""POS Lite application services — Phase 12.4.

Lightweight point-of-sale shell only: registers, shifts, tickets, and
ticket lines. No payment posting, no accounting entries, no kitchen
dispatch.
"""
from __future__ import annotations

from datetime import datetime, timezone
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.pos_lite import PosRegister, PosShift, PosTicket, PosTicketLine
from app.models.types import AuditAction, LifecycleStatus, PosShiftStatus, PosTicketStatus
from app.repositories.foundation import MenuItemRepository, VenueRepository
from app.repositories.pos_lite import (
    PosRegisterRepository,
    PosShiftRepository,
    PosTicketLineRepository,
    PosTicketRepository,
)
from app.schemas.pos_lite import (
    PosRegisterCreate,
    PosShiftCreate,
    PosTicketCreate,
    PosTicketLineCreate,
)
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


class _PosLiteBase:
    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 PosRegisterService(_PosLiteBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = PosRegisterRepository(session)

    async def create(
        self, tenant_id: UUID, body: PosRegisterCreate, *, actor: CurrentUser | None = None
    ) -> PosRegister:
        await self._require_venue(tenant_id, body.venue_id)
        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="pos_register_code_exists"
            )
        actor_id = actor.user_id if actor else None
        entity = PosRegister(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            status=validate_lifecycle_status(body.status),
            description=body.description,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="pos_register",
            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.POS_REGISTER_CREATED,
            aggregate_type="pos_register",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> PosRegister:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("صندوق یافت نشد", error_code="pos_register_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 PosShiftService(_PosLiteBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = PosShiftRepository(session)
        self.registers = PosRegisterRepository(session)

    async def create(
        self, tenant_id: UUID, body: PosShiftCreate, *, actor: CurrentUser | None = None
    ) -> PosShift:
        await self._require_venue(tenant_id, body.venue_id)
        if await self.registers.get(tenant_id, body.register_id) is None:
            raise NotFoundError("صندوق یافت نشد", error_code="pos_register_not_found")
        open_shifts = await self.repo.list_open_for_register(tenant_id, body.register_id)
        if open_shifts:
            raise AppError(
                "صندوق در حال حاضر شیفت باز دارد",
                status_code=409,
                error_code="pos_shift_already_open",
            )
        actor_id = actor.user_id if actor else None
        entity = PosShift(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            register_id=body.register_id,
            opened_by=body.opened_by,
            status=PosShiftStatus.OPEN,
            opened_at=datetime.now(timezone.utc),
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="pos_shift",
            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.POS_SHIFT_OPENED,
            aggregate_type="pos_shift",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"register_id": str(entity.register_id)},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> PosShift:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("شیفت یافت نشد", error_code="pos_shift_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 close(
        self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None
    ) -> PosShift:
        entity = await self.get(tenant_id, entity_id)
        if entity.status != PosShiftStatus.OPEN:
            raise AppError(
                "شیفت باز نیست", status_code=409, error_code="pos_shift_not_open"
            )
        actor_id = actor.user_id if actor else None
        entity.status = PosShiftStatus.CLOSED
        entity.closed_at = datetime.now(timezone.utc)
        entity.updated_by = actor_id
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="pos_shift",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=actor_id,
            changes={"status": entity.status},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=HospitalityEventType.POS_SHIFT_CLOSED,
            aggregate_type="pos_shift",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"register_id": str(entity.register_id)},
        )
        return entity


class PosTicketService(_PosLiteBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = PosTicketRepository(session)
        self.registers = PosRegisterRepository(session)
        self.shifts = PosShiftRepository(session)

    async def create(
        self, tenant_id: UUID, body: PosTicketCreate, *, actor: CurrentUser | None = None
    ) -> PosTicket:
        await self._require_venue(tenant_id, body.venue_id)
        if await self.registers.get(tenant_id, body.register_id) is None:
            raise NotFoundError("صندوق یافت نشد", error_code="pos_register_not_found")
        if body.shift_id is not None and await self.shifts.get(tenant_id, body.shift_id) is None:
            raise NotFoundError("شیفت یافت نشد", error_code="pos_shift_not_found")
        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="pos_ticket_code_exists"
            )
        actor_id = actor.user_id if actor else None
        entity = PosTicket(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            register_id=body.register_id,
            shift_id=body.shift_id,
            table_id=body.table_id,
            ordering_session_id=body.ordering_session_id,
            code=code,
            status=PosTicketStatus.OPEN,
            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="pos_ticket",
            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.POS_TICKET_CREATED,
            aggregate_type="pos_ticket",
            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) -> PosTicket:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("فیش یافت نشد", error_code="pos_ticket_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 void(
        self, tenant_id: UUID, entity_id: UUID, *, actor: CurrentUser | None = None
    ) -> PosTicket:
        entity = await self.get(tenant_id, entity_id)
        if entity.status in (PosTicketStatus.VOID, PosTicketStatus.PAID, PosTicketStatus.CANCELLED):
            raise AppError(
                "وضعیت فیش نهایی شده و قابل باطل شدن نیست",
                status_code=409,
                error_code="pos_ticket_status_final",
            )
        actor_id = actor.user_id if actor else None
        entity.status = PosTicketStatus.VOID
        entity.updated_by = actor_id
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="pos_ticket",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=actor_id,
            changes={"status": entity.status},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        self.publisher.publish(
            event_type=HospitalityEventType.POS_TICKET_VOIDED,
            aggregate_type="pos_ticket",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        return entity


class PosTicketLineService(_PosLiteBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = PosTicketLineRepository(session)
        self.tickets = PosTicketRepository(session)
        self.items = MenuItemRepository(session)

    async def add(
        self, tenant_id: UUID, body: PosTicketLineCreate, *, actor: CurrentUser | None = None
    ) -> PosTicketLine:
        await self._require_venue(tenant_id, body.venue_id)
        ticket = await self.tickets.get(tenant_id, body.ticket_id)
        if ticket is None:
            raise NotFoundError("فیش یافت نشد", error_code="pos_ticket_not_found")
        if ticket.status not in (PosTicketStatus.DRAFT, PosTicketStatus.OPEN):
            raise AppError(
                "فیش برای افزودن آیتم باز نیست", status_code=409, error_code="pos_ticket_not_open"
            )
        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 = PosTicketLine(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            ticket_id=body.ticket_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="pos_ticket_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.POS_TICKET_LINE_ADDED,
            aggregate_type="pos_ticket_line",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"ticket_id": str(entity.ticket_id), "menu_item_id": str(entity.menu_item_id)},
        )
        return entity

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