"""POS Pro application services — Phase 12.5.

Payments, discounts, tax lines, floor plans, and stations. Payment
records are local only — no accounting posting.
"""
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.pos_pro import PosDiscount, PosFloorPlan, PosPayment, PosStation, PosTaxLine
from app.models.types import AuditAction, LifecycleStatus, PosDiscountKind
from app.repositories.foundation import DiningAreaRepository, VenueRepository
from app.repositories.pos_lite import PosTicketRepository
from app.repositories.pos_pro import (
    PosDiscountRepository,
    PosFloorPlanRepository,
    PosPaymentRepository,
    PosStationRepository,
    PosTaxLineRepository,
)
from app.schemas.pos_pro import (
    PosDiscountCreate,
    PosFloorPlanCreate,
    PosPaymentCreate,
    PosStationCreate,
    PosTaxLineCreate,
)
from app.services.audit_service import AuditService
from app.validators import (
    validate_code,
    validate_currency_code,
    validate_lifecycle_status,
    validate_non_empty,
    validate_non_negative_int,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


class _PosProBase:
    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)
        self.tickets = PosTicketRepository(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

    async def _require_ticket(self, tenant_id: UUID, ticket_id: UUID):
        ticket = await self.tickets.get(tenant_id, ticket_id)
        if ticket is None:
            raise NotFoundError("فیش یافت نشد", error_code="pos_ticket_not_found")
        return ticket


class PosPaymentService(_PosProBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = PosPaymentRepository(session)

    async def create(
        self, tenant_id: UUID, body: PosPaymentCreate, *, actor: CurrentUser | None = None
    ) -> PosPayment:
        await self._require_venue(tenant_id, body.venue_id)
        await self._require_ticket(tenant_id, body.ticket_id)
        amount = validate_non_negative_int(body.amount, field="amount")
        if amount is None or amount <= 0:
            raise AppError(
                "مبلغ پرداخت باید بیشتر از صفر باشد",
                status_code=422,
                error_code="invalid_amount",
                details={"field": "amount"},
            )
        actor_id = actor.user_id if actor else None
        entity = PosPayment(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            ticket_id=body.ticket_id,
            method=body.method,
            amount=amount,
            currency_code=validate_currency_code(body.currency_code),
            external_payment_ref=body.external_payment_ref,
            status=LifecycleStatus.ACTIVE,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="pos_payment",
            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_PAYMENT_RECORDED,
            aggregate_type="pos_payment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"ticket_id": str(entity.ticket_id), "amount": entity.amount},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> PosPayment:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("پرداخت یافت نشد", error_code="pos_payment_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 PosDiscountService(_PosProBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = PosDiscountRepository(session)

    async def create(
        self, tenant_id: UUID, body: PosDiscountCreate, *, actor: CurrentUser | None = None
    ) -> PosDiscount:
        await self._require_venue(tenant_id, body.venue_id)
        await self._require_ticket(tenant_id, body.ticket_id)
        code = validate_code(body.code)
        if await self.repo.get_by_code_for_ticket(tenant_id, body.ticket_id, code) is not None:
            raise AppError(
                "کد تخفیف تکراری است", status_code=409, error_code="pos_discount_code_exists"
            )
        value = validate_non_negative_int(body.value, field="value")
        if value is None:
            value = 0
        if body.kind == PosDiscountKind.PERCENT and value > 100:
            raise AppError(
                "درصد تخفیف نمی‌تواند بیشتر از ۱۰۰ باشد",
                status_code=422,
                error_code="invalid_discount_percent",
            )
        actor_id = actor.user_id if actor else None
        entity = PosDiscount(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            ticket_id=body.ticket_id,
            code=code,
            kind=body.kind,
            value=value,
            name=body.name,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="pos_discount",
            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_DISCOUNT_APPLIED,
            aggregate_type="pos_discount",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"ticket_id": str(entity.ticket_id), "code": entity.code},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> PosDiscount:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("تخفیف یافت نشد", error_code="pos_discount_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 PosTaxLineService(_PosProBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = PosTaxLineRepository(session)

    async def create(
        self, tenant_id: UUID, body: PosTaxLineCreate, *, actor: CurrentUser | None = None
    ) -> PosTaxLine:
        await self._require_venue(tenant_id, body.venue_id)
        await self._require_ticket(tenant_id, body.ticket_id)
        rate_bps = validate_non_negative_int(body.rate_bps, field="rate_bps") or 0
        amount = validate_non_negative_int(body.amount, field="amount") or 0
        actor_id = actor.user_id if actor else None
        entity = PosTaxLine(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            ticket_id=body.ticket_id,
            code=validate_code(body.code),
            name=validate_non_empty(body.name, field="name"),
            rate_bps=rate_bps,
            amount=amount,
            created_by=actor_id,
            updated_by=actor_id,
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="pos_tax_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_TAX_LINE_ADDED,
            aggregate_type="pos_tax_line",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"ticket_id": str(entity.ticket_id), "code": entity.code},
        )
        return entity

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


class PosFloorPlanService(_PosProBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = PosFloorPlanRepository(session)
        self.dining_areas = DiningAreaRepository(session)

    async def create(
        self, tenant_id: UUID, body: PosFloorPlanCreate, *, actor: CurrentUser | None = None
    ) -> PosFloorPlan:
        await self._require_venue(tenant_id, body.venue_id)
        if body.dining_area_id is not None and await self.dining_areas.get(
            tenant_id, body.dining_area_id
        ) is None:
            raise NotFoundError("فضای پذیرایی یافت نشد", error_code="dining_area_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_floor_plan_code_exists"
            )
        actor_id = actor.user_id if actor else None
        entity = PosFloorPlan(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            dining_area_id=body.dining_area_id,
            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="pos_floor_plan",
            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_FLOOR_PLAN_CREATED,
            aggregate_type="pos_floor_plan",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> PosFloorPlan:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("پلان طبقه یافت نشد", error_code="pos_floor_plan_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 PosStationService(_PosProBase):
    def __init__(self, session: AsyncSession, publisher: InMemoryEventPublisher | None = None) -> None:
        super().__init__(session, publisher)
        self.repo = PosStationRepository(session)
        self.floor_plans = PosFloorPlanRepository(session)

    async def create(
        self, tenant_id: UUID, body: PosStationCreate, *, actor: CurrentUser | None = None
    ) -> PosStation:
        await self._require_venue(tenant_id, body.venue_id)
        if body.floor_plan_id is not None and await self.floor_plans.get(
            tenant_id, body.floor_plan_id
        ) is None:
            raise NotFoundError("پلان طبقه یافت نشد", error_code="pos_floor_plan_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_station_code_exists"
            )
        actor_id = actor.user_id if actor else None
        entity = PosStation(
            tenant_id=tenant_id,
            venue_id=body.venue_id,
            floor_plan_id=body.floor_plan_id,
            code=code,
            name=validate_non_empty(body.name, field="name"),
            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="pos_station",
            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_STATION_CREATED,
            aggregate_type="pos_station",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        return entity

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