"""Availability application services — Phase 10.3."""
from __future__ import annotations

from uuid import UUID

from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import TransactionalEventPublisher
from app.events.types import DeliveryEventType
from app.models.availability import (
    DriverAvailability,
    Shift,
    ShiftAssignment,
    WorkingZone,
)
from app.models.types import AuditAction, ShiftAssignmentStatus
from app.repositories.availability import (
    DriverAvailabilityRepository,
    ShiftAssignmentRepository,
    ShiftRepository,
    WorkingZoneRepository,
)
from app.repositories.drivers import DriverRepository
from app.repositories.foundation import (
    DeliveryHubRepository,
    DeliveryOrganizationRepository,
)
from app.schemas.availability import (
    DriverAvailabilityUpsert,
    ShiftAssignmentCreate,
    ShiftAssignmentUpdate,
    ShiftCreate,
    ShiftUpdate,
    WorkingZoneCreate,
    WorkingZoneUpdate,
)
from app.services._helpers import actor_id, jsonable
from app.services.audit_service import AuditService
from app.specifications.availability import ShiftListSpec, WorkingZoneListSpec
from app.validators import ensure_optimistic_version, validate_code
from app.validators.availability import (
    validate_availability_window,
    validate_shift_name,
    validate_shift_window,
    validate_zone_name,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


class AvailabilityService:
    def __init__(
        self,
        session: AsyncSession,
        *,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = DriverAvailabilityRepository(session)
        self.drivers = DriverRepository(session)
        self.audit = AuditService(session)
        self.publisher = publisher or TransactionalEventPublisher(session)

    async def upsert_for_driver(
        self,
        tenant_id: UUID,
        driver_id: UUID,
        body: DriverAvailabilityUpsert,
        *,
        actor: CurrentUser | None = None,
    ) -> DriverAvailability:
        if await self.drivers.get(tenant_id, driver_id) is None:
            raise NotFoundError("راننده یافت نشد")
        validate_availability_window(
            effective_from=body.effective_from, effective_until=body.effective_until
        )
        entity = await self.repo.get_for_driver(tenant_id, driver_id)
        if entity is None:
            entity = DriverAvailability(
                tenant_id=tenant_id,
                driver_id=driver_id,
                status=body.status,
                effective_from=body.effective_from,
                effective_until=body.effective_until,
                notes=body.notes,
                metadata_json=body.metadata_json,
                created_by=actor_id(actor),
                updated_by=actor_id(actor),
            )
            await self.repo.add(entity)
        else:
            if body.version is not None:
                ensure_optimistic_version(entity, body.version)
            entity.status = body.status
            entity.effective_from = body.effective_from
            entity.effective_until = body.effective_until
            entity.notes = body.notes
            entity.metadata_json = body.metadata_json
            entity.version += 1
            entity.updated_by = actor_id(actor)
        await self.publisher.publish(
            event_type=DeliveryEventType.DRIVER_AVAILABILITY_UPDATED,
            aggregate_type="driver_availability",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"driver_id": str(driver_id), "status": entity.status.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get_for_driver(
        self, tenant_id: UUID, driver_id: UUID
    ) -> DriverAvailability:
        if await self.drivers.get(tenant_id, driver_id) is None:
            raise NotFoundError("راننده یافت نشد")
        entity = await self.repo.get_for_driver(tenant_id, driver_id)
        if entity is None:
            raise NotFoundError("دسترسی راننده یافت نشد")
        return entity


class ShiftService:
    def __init__(
        self,
        session: AsyncSession,
        *,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = ShiftRepository(session)
        self.assignments = ShiftAssignmentRepository(session)
        self.drivers = DriverRepository(session)
        self.orgs = DeliveryOrganizationRepository(session)
        self.hubs = DeliveryHubRepository(session)
        self.audit = AuditService(session)
        self.publisher = publisher or TransactionalEventPublisher(session)

    async def create(
        self, tenant_id: UUID, body: ShiftCreate, *, actor: CurrentUser | None = None
    ) -> Shift:
        if await self.orgs.get(tenant_id, body.organization_id) is None:
            raise NotFoundError("سازمان یافت نشد")
        validate_shift_window(starts_at=body.starts_at, ends_at=body.ends_at)
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError("کد شیفت تکراری است", error_code="duplicate_code", status_code=409)
        entity = Shift(
            tenant_id=tenant_id,
            organization_id=body.organization_id,
            hub_id=body.hub_id,
            code=code,
            name=validate_shift_name(body.name),
            status=body.status,
            starts_at=body.starts_at,
            ends_at=body.ends_at,
            metadata_json=body.metadata_json,
            created_by=actor_id(actor),
            updated_by=actor_id(actor),
        )
        await self.repo.add(entity)
        await self.publisher.publish(
            event_type=DeliveryEventType.SHIFT_CREATED,
            aggregate_type="shift",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

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

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20, spec: ShiftListSpec | None = None
    ) -> tuple[list[Shift], int]:
        items = list(await self.repo.list_filtered(tenant_id, offset=offset, limit=limit, spec=spec))
        return items, await self.repo.count_filtered(tenant_id, spec=spec)

    async def update(
        self, tenant_id: UUID, shift_id: UUID, body: ShiftUpdate, *, actor: CurrentUser | None = None
    ) -> Shift:
        entity = await self.get(tenant_id, shift_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        starts = data.get("starts_at", entity.starts_at)
        ends = data.get("ends_at", entity.ends_at)
        validate_shift_window(starts_at=starts, ends_at=ends)
        if "name" in data and data["name"] is not None:
            data["name"] = validate_shift_name(data["name"])
        for key, value in data.items():
            setattr(entity, key, value)
        entity.version += 1
        entity.updated_by = actor_id(actor)
        await self.publisher.publish(
            event_type=DeliveryEventType.SHIFT_UPDATED,
            aggregate_type="shift",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self, tenant_id: UUID, shift_id: UUID, *, actor: CurrentUser | None = None
    ) -> Shift:
        entity = await self.get(tenant_id, shift_id)
        await self.repo.soft_delete(entity, deleted_by=actor_id(actor))
        await self.publisher.publish(
            event_type=DeliveryEventType.SHIFT_DELETED,
            aggregate_type="shift",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def assign_driver(
        self,
        tenant_id: UUID,
        shift_id: UUID,
        body: ShiftAssignmentCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ShiftAssignment:
        await self.get(tenant_id, shift_id)
        if await self.drivers.get(tenant_id, body.driver_id) is None:
            raise NotFoundError("راننده یافت نشد")
        entity = ShiftAssignment(
            tenant_id=tenant_id,
            shift_id=shift_id,
            driver_id=body.driver_id,
            status=ShiftAssignmentStatus.ASSIGNED,
            notes=body.notes,
            metadata_json=body.metadata_json,
            created_by=actor_id(actor),
            updated_by=actor_id(actor),
        )
        try:
            await self.assignments.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "تخصیص شیفت تکراری است",
                error_code="duplicate_shift_assignment",
                status_code=409,
            ) from exc
        await self.publisher.publish(
            event_type=DeliveryEventType.SHIFT_ASSIGNMENT_CREATED,
            aggregate_type="shift_assignment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"shift_id": str(shift_id), "driver_id": str(body.driver_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_assignments(
        self, tenant_id: UUID, shift_id: UUID
    ) -> list[ShiftAssignment]:
        await self.get(tenant_id, shift_id)
        return list(await self.assignments.list_for_shift(tenant_id, shift_id))

    async def update_assignment(
        self,
        tenant_id: UUID,
        shift_id: UUID,
        assignment_id: UUID,
        body: ShiftAssignmentUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ShiftAssignment:
        await self.get(tenant_id, shift_id)
        entity = await self.assignments.get(tenant_id, assignment_id)
        if entity is None or entity.shift_id != shift_id:
            raise NotFoundError("تخصیص شیفت یافت نشد")
        data = body.model_dump(exclude_unset=True)
        for key, value in data.items():
            setattr(entity, key, value)
        entity.updated_by = actor_id(actor)
        await self.publisher.publish(
            event_type=DeliveryEventType.SHIFT_ASSIGNMENT_UPDATED,
            aggregate_type="shift_assignment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"shift_id": str(shift_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class WorkingZoneService:
    def __init__(
        self,
        session: AsyncSession,
        *,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = WorkingZoneRepository(session)
        self.orgs = DeliveryOrganizationRepository(session)
        self.publisher = publisher or TransactionalEventPublisher(session)

    async def create(
        self, tenant_id: UUID, body: WorkingZoneCreate, *, actor: CurrentUser | None = None
    ) -> WorkingZone:
        if await self.orgs.get(tenant_id, body.organization_id) is None:
            raise NotFoundError("سازمان یافت نشد")
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError("کد منطقه کاری تکراری است", error_code="duplicate_code", status_code=409)
        entity = WorkingZone(
            tenant_id=tenant_id,
            organization_id=body.organization_id,
            code=code,
            name=validate_zone_name(body.name),
            status=body.status,
            geo_boundary_ref=body.geo_boundary_ref,
            metadata_json=body.metadata_json,
            created_by=actor_id(actor),
            updated_by=actor_id(actor),
        )
        await self.repo.add(entity)
        await self.publisher.publish(
            event_type=DeliveryEventType.WORKING_ZONE_CREATED,
            aggregate_type="working_zone",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, zone_id: UUID) -> WorkingZone:
        entity = await self.repo.get(tenant_id, zone_id)
        if entity is None:
            raise NotFoundError("منطقه کاری یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20, spec: WorkingZoneListSpec | None = None
    ) -> tuple[list[WorkingZone], int]:
        items = list(await self.repo.list_filtered(tenant_id, offset=offset, limit=limit, spec=spec))
        return items, await self.repo.count_filtered(tenant_id, spec=spec)

    async def update(
        self, tenant_id: UUID, zone_id: UUID, body: WorkingZoneUpdate, *, actor: CurrentUser | None = None
    ) -> WorkingZone:
        entity = await self.get(tenant_id, zone_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "name" in data and data["name"] is not None:
            data["name"] = validate_zone_name(data["name"])
        for key, value in data.items():
            setattr(entity, key, value)
        entity.version += 1
        entity.updated_by = actor_id(actor)
        await self.publisher.publish(
            event_type=DeliveryEventType.WORKING_ZONE_UPDATED,
            aggregate_type="working_zone",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self, tenant_id: UUID, zone_id: UUID, *, actor: CurrentUser | None = None
    ) -> WorkingZone:
        entity = await self.get(tenant_id, zone_id)
        await self.repo.soft_delete(entity, deleted_by=actor_id(actor))
        await self.publisher.publish(
            event_type=DeliveryEventType.WORKING_ZONE_DELETED,
            aggregate_type="working_zone",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity
