"""Dispatch application services — Phase 10.5."""
from __future__ import annotations

from datetime import datetime, timezone
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import TransactionalEventPublisher
from app.events.types import DeliveryEventType
from app.models.dispatch import DispatchJob, JobAssignment
from app.models.types import AuditAction, JobAssignmentStatus
from app.policies.dispatch import DispatchJobPolicy
from app.repositories.dispatch import DispatchJobRepository, JobAssignmentRepository
from app.repositories.drivers import DriverRepository
from app.repositories.foundation import (
    DeliveryHubRepository,
    DeliveryOrganizationRepository,
)
from app.repositories.fleet import VehicleRepository
from app.schemas.dispatch import (
    DispatchJobCreate,
    DispatchJobStatusRequest,
    DispatchJobUpdate,
    JobAssignmentCreate,
    JobAssignmentUpdate,
)
from app.services._helpers import actor_id, jsonable
from app.services.audit_service import AuditService
from app.specifications.dispatch import DispatchJobListSpec, JobAssignmentListSpec
from app.validators import ensure_optimistic_version, validate_code
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


class DispatchJobService:
    def __init__(
        self,
        session: AsyncSession,
        *,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = DispatchJobRepository(session)
        self.orgs = DeliveryOrganizationRepository(session)
        self.hubs = DeliveryHubRepository(session)
        self.audit = AuditService(session)
        self.publisher = publisher or TransactionalEventPublisher(session)
        self.policy = DispatchJobPolicy()

    async def create(
        self, tenant_id: UUID, body: DispatchJobCreate, *, actor: CurrentUser | None = None
    ) -> DispatchJob:
        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 = DispatchJob(
            tenant_id=tenant_id,
            organization_id=body.organization_id,
            hub_id=body.hub_id,
            code=code,
            external_order_ref=body.external_order_ref,
            external_source=body.external_source,
            priority=body.priority,
            pickup_address_ref=body.pickup_address_ref,
            dropoff_address_ref=body.dropoff_address_ref,
            scheduled_at=body.scheduled_at,
            metadata_json=body.metadata_json,
            created_by=actor_id(actor),
            updated_by=actor_id(actor),
        )
        await self.repo.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="dispatch_job",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=actor_id(actor),
            changes=jsonable(body.model_dump()),
        )
        await self.publisher.publish(
            event_type=DeliveryEventType.DISPATCH_JOB_CREATED,
            aggregate_type="dispatch_job",
            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, job_id: UUID) -> DispatchJob:
        entity = await self.repo.get(tenant_id, job_id)
        if entity is None:
            raise NotFoundError("کار ارسال یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20, spec: DispatchJobListSpec | None = None
    ) -> tuple[list[DispatchJob], 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, job_id: UUID, body: DispatchJobUpdate, *, actor: CurrentUser | None = None
    ) -> DispatchJob:
        entity = await self.get(tenant_id, job_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        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.DISPATCH_JOB_UPDATED,
            aggregate_type="dispatch_job",
            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 apply_status(
        self,
        tenant_id: UUID,
        job_id: UUID,
        body: DispatchJobStatusRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> DispatchJob:
        entity = await self.get(tenant_id, job_id)
        ensure_optimistic_version(entity, body.version)
        from_status = entity.status
        to_status, reason = self.policy.assert_transition(
            action=body.action, current=from_status, reason=body.reason
        )
        entity.status = to_status
        entity.status_reason = reason
        entity.version += 1
        entity.updated_by = actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="dispatch_job",
            entity_id=entity.id,
            action=AuditAction.STATUS_CHANGE,
            actor_user_id=actor_id(actor),
            changes={
                "from_status": from_status.value,
                "to_status": to_status.value,
                "action": body.action.value,
            },
        )
        await self.publisher.publish(
            event_type=DeliveryEventType.DISPATCH_JOB_STATUS_CHANGED,
            aggregate_type="dispatch_job",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "action": body.action.value,
                "from_status": from_status.value,
                "to_status": to_status.value,
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

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


class JobAssignmentService:
    def __init__(
        self,
        session: AsyncSession,
        *,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = JobAssignmentRepository(session)
        self.jobs = DispatchJobRepository(session)
        self.drivers = DriverRepository(session)
        self.vehicles = VehicleRepository(session)
        self.publisher = publisher or TransactionalEventPublisher(session)

    async def create(
        self, tenant_id: UUID, body: JobAssignmentCreate, *, actor: CurrentUser | None = None
    ) -> JobAssignment:
        if await self.jobs.get(tenant_id, body.dispatch_job_id) is None:
            raise NotFoundError("کار ارسال یافت نشد")
        if await self.drivers.get(tenant_id, body.driver_id) is None:
            raise NotFoundError("راننده یافت نشد")
        if body.vehicle_id and await self.vehicles.get(tenant_id, body.vehicle_id) is None:
            raise NotFoundError("خودرو یافت نشد")
        entity = JobAssignment(
            tenant_id=tenant_id,
            dispatch_job_id=body.dispatch_job_id,
            driver_id=body.driver_id,
            vehicle_id=body.vehicle_id,
            status=JobAssignmentStatus.PENDING,
            assigned_at=datetime.now(timezone.utc),
            notes=body.notes,
            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.JOB_ASSIGNMENT_CREATED,
            aggregate_type="job_assignment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"dispatch_job_id": str(body.dispatch_job_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, assignment_id: UUID) -> JobAssignment:
        entity = await self.repo.get(tenant_id, assignment_id)
        if entity is None:
            raise NotFoundError("تخصیص کار یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20, spec: JobAssignmentListSpec | None = None
    ) -> tuple[list[JobAssignment], 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,
        assignment_id: UUID,
        body: JobAssignmentUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> JobAssignment:
        entity = await self.get(tenant_id, assignment_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        old_status = entity.status
        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.JOB_ASSIGNMENT_UPDATED,
            aggregate_type="job_assignment",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"dispatch_job_id": str(entity.dispatch_job_id)},
        )
        if "status" in data and entity.status != old_status:
            await self.publisher.publish(
                event_type=DeliveryEventType.JOB_ASSIGNMENT_STATUS_CHANGED,
                aggregate_type="job_assignment",
                aggregate_id=entity.id,
                tenant_id=tenant_id,
                payload={"status": entity.status.value},
            )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity
