"""Driver management application services — Phase 10.1."""
from __future__ import annotations

from datetime import date, datetime, timezone
from typing import Any
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.drivers import (
    Driver,
    DriverCredential,
    DriverDocument,
    DriverLifecycleEvent,
)
from app.models.types import AuditAction, DriverLifecycleAction, DriverStatus
from app.policies.drivers import DriverLifecyclePolicy
from app.repositories.drivers import (
    DriverCredentialRepository,
    DriverDocumentRepository,
    DriverLifecycleEventRepository,
    DriverRepository,
)
from app.repositories.foundation import (
    DeliveryHubRepository,
    DeliveryOrganizationRepository,
)
from app.schemas.drivers import (
    DriverCreate,
    DriverCredentialCreate,
    DriverDocumentCreate,
    DriverLifecycleRequest,
    DriverUpdate,
)
from app.services.audit_service import AuditService
from app.specifications.drivers import DriverListSpec
from app.validators import ensure_optimistic_version, validate_code
from app.validators.drivers import (
    validate_credential_dates,
    validate_display_name,
    validate_email,
    validate_mobile,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser

_ACTION_EVENTS: dict[DriverLifecycleAction, DeliveryEventType] = {
    DriverLifecycleAction.ACTIVATE: DeliveryEventType.DRIVER_ACTIVATED,
    DriverLifecycleAction.SUSPEND: DeliveryEventType.DRIVER_SUSPENDED,
    DriverLifecycleAction.RESUME: DeliveryEventType.DRIVER_RESUMED,
    DriverLifecycleAction.DEACTIVATE: DeliveryEventType.DRIVER_DEACTIVATED,
    DriverLifecycleAction.BLOCK: DeliveryEventType.DRIVER_BLOCKED,
    DriverLifecycleAction.UNBLOCK: DeliveryEventType.DRIVER_UNBLOCKED,
    DriverLifecycleAction.ARCHIVE: DeliveryEventType.DRIVER_ARCHIVED,
}

_ACTION_AUDIT: dict[DriverLifecycleAction, AuditAction] = {
    DriverLifecycleAction.ACTIVATE: AuditAction.ACTIVATE,
    DriverLifecycleAction.SUSPEND: AuditAction.SUSPEND,
    DriverLifecycleAction.RESUME: AuditAction.RESUME,
    DriverLifecycleAction.DEACTIVATE: AuditAction.DEACTIVATE,
    DriverLifecycleAction.BLOCK: AuditAction.BLOCK,
    DriverLifecycleAction.UNBLOCK: AuditAction.UNBLOCK,
    DriverLifecycleAction.ARCHIVE: AuditAction.ARCHIVE,
}


def _actor_id(actor: CurrentUser | None) -> str | None:
    return actor.user_id if actor else None


def _jsonable(data: dict[str, Any]) -> dict[str, Any]:
    out: dict[str, Any] = {}
    for key, value in data.items():
        if isinstance(value, UUID):
            out[key] = str(value)
        elif isinstance(value, (datetime, date)):
            out[key] = value.isoformat()
        elif hasattr(value, "value"):
            out[key] = value.value
        else:
            out[key] = value
    return out


class DriverService:
    def __init__(
        self,
        session: AsyncSession,
        *,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = DriverRepository(session)
        self.credentials = DriverCredentialRepository(session)
        self.documents = DriverDocumentRepository(session)
        self.lifecycle = DriverLifecycleEventRepository(session)
        self.orgs = DeliveryOrganizationRepository(session)
        self.hubs = DeliveryHubRepository(session)
        self.audit = AuditService(session)
        self.publisher = publisher or TransactionalEventPublisher(session)
        self.policy = DriverLifecyclePolicy()

    async def create(
        self,
        tenant_id: UUID,
        body: DriverCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> Driver:
        org = await self.orgs.get(tenant_id, body.organization_id)
        if org is None:
            raise NotFoundError("سازمان یافت نشد")
        if body.hub_id is not None:
            hub = await self.hubs.get(tenant_id, body.hub_id)
            if hub is None:
                raise NotFoundError("هاب یافت نشد")
            if hub.organization_id != body.organization_id:
                raise AppError(
                    "هاب به سازمان انتخاب‌شده تعلق ندارد",
                    status_code=422,
                    error_code="hub_organization_mismatch",
                )
        code = validate_code(body.code)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError(
                "کد راننده تکراری است",
                error_code="duplicate_code",
                status_code=409,
            )
        if body.status not in {DriverStatus.PENDING, DriverStatus.ACTIVE}:
            raise AppError(
                "وضعیت اولیه فقط pending یا active مجاز است",
                status_code=422,
                error_code="invalid_initial_status",
            )
        entity = Driver(
            tenant_id=tenant_id,
            organization_id=body.organization_id,
            hub_id=body.hub_id,
            code=code,
            display_name=validate_display_name(body.display_name),
            first_name=body.first_name,
            last_name=body.last_name,
            mobile=validate_mobile(body.mobile),
            email=validate_email(body.email),
            status=body.status,
            external_user_ref=body.external_user_ref,
            external_crm_contact_ref=body.external_crm_contact_ref,
            hire_date=body.hire_date,
            notes=body.notes,
            capability_tags=body.capability_tags,
            metadata_json=body.metadata_json,
            status_changed_at=datetime.now(timezone.utc),
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.repo.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "کد راننده تکراری است",
                error_code="duplicate_code",
                status_code=409,
            ) from exc
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="driver",
            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.DRIVER_CREATED,
            aggregate_type="driver",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "code": entity.code,
                "status": entity.status.value,
                "organization_id": str(entity.organization_id),
            },
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

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

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

    async def update(
        self,
        tenant_id: UUID,
        driver_id: UUID,
        body: DriverUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> Driver:
        entity = await self.get(tenant_id, driver_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "hub_id" in data and data["hub_id"] is not None:
            hub = await self.hubs.get(tenant_id, data["hub_id"])
            if hub is None:
                raise NotFoundError("هاب یافت نشد")
            if hub.organization_id != entity.organization_id:
                raise AppError(
                    "هاب به سازمان راننده تعلق ندارد",
                    status_code=422,
                    error_code="hub_organization_mismatch",
                )
        if "display_name" in data and data["display_name"] is not None:
            data["display_name"] = validate_display_name(data["display_name"])
        if "mobile" in data:
            data["mobile"] = validate_mobile(data["mobile"])
        if "email" in data:
            data["email"] = validate_email(data["email"])
        for key, value in data.items():
            setattr(entity, key, value)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="driver",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable(data),
        )
        await self.publisher.publish(
            event_type=DeliveryEventType.DRIVER_UPDATED,
            aggregate_type="driver",
            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_lifecycle(
        self,
        tenant_id: UUID,
        driver_id: UUID,
        action: DriverLifecycleAction,
        body: DriverLifecycleRequest,
        *,
        actor: CurrentUser | None = None,
    ) -> Driver:
        entity = await self.get(tenant_id, driver_id)
        ensure_optimistic_version(entity, body.version)
        from_status = entity.status
        to_status, reason = self.policy.assert_transition(
            action=action, current=from_status, reason=body.reason
        )
        entity.status = to_status
        entity.status_reason = reason
        entity.status_changed_at = datetime.now(timezone.utc)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        history = DriverLifecycleEvent(
            tenant_id=tenant_id,
            driver_id=entity.id,
            action=action,
            from_status=from_status,
            to_status=to_status,
            reason=reason,
            actor_user_id=_actor_id(actor),
        )
        await self.lifecycle.add(history)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="driver",
            entity_id=entity.id,
            action=_ACTION_AUDIT[action],
            actor_user_id=_actor_id(actor),
            changes={
                "from_status": from_status.value,
                "to_status": to_status.value,
                "reason": reason,
            },
            message=f"driver.{action.value}",
        )
        await self.publisher.publish(
            event_type=DeliveryEventType.DRIVER_STATUS_CHANGED,
            aggregate_type="driver",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={
                "action": action.value,
                "from_status": from_status.value,
                "to_status": to_status.value,
            },
        )
        await self.publisher.publish(
            event_type=_ACTION_EVENTS[action],
            aggregate_type="driver",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "status": to_status.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def soft_delete(
        self,
        tenant_id: UUID,
        driver_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> Driver:
        entity = await self.get(tenant_id, driver_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="driver",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.publisher.publish(
            event_type=DeliveryEventType.DRIVER_DELETED,
            aggregate_type="driver",
            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 list_lifecycle(
        self, tenant_id: UUID, driver_id: UUID, *, limit: int = 100
    ) -> list[DriverLifecycleEvent]:
        await self.get(tenant_id, driver_id)
        return list(
            await self.lifecycle.list_for_driver(tenant_id, driver_id, limit=limit)
        )

    async def add_credential(
        self,
        tenant_id: UUID,
        driver_id: UUID,
        body: DriverCredentialCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> DriverCredential:
        await self.get(tenant_id, driver_id)
        validate_credential_dates(issued_at=body.issued_at, expires_at=body.expires_at)
        number = body.number.strip()
        if not number:
            raise AppError(
                "شماره مدرک الزامی است",
                status_code=422,
                error_code="credential_number_required",
            )
        entity = DriverCredential(
            tenant_id=tenant_id,
            driver_id=driver_id,
            kind=body.kind,
            number=number,
            issued_at=body.issued_at,
            expires_at=body.expires_at,
            is_verified=body.is_verified,
            document_file_ref=body.document_file_ref,
            metadata_json=body.metadata_json,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        try:
            await self.credentials.add(entity)
        except IntegrityError as exc:
            raise AppError(
                "مدرک تکراری است",
                error_code="duplicate_credential",
                status_code=409,
            ) from exc
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="driver_credential",
            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.DRIVER_CREDENTIAL_ADDED,
            aggregate_type="driver_credential",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"driver_id": str(driver_id), "kind": entity.kind.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_credentials(
        self, tenant_id: UUID, driver_id: UUID
    ) -> list[DriverCredential]:
        await self.get(tenant_id, driver_id)
        return list(await self.credentials.list_for_driver(tenant_id, driver_id))

    async def add_document(
        self,
        tenant_id: UUID,
        driver_id: UUID,
        body: DriverDocumentCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> DriverDocument:
        await self.get(tenant_id, driver_id)
        title = body.title.strip()
        ref = body.storage_file_ref.strip()
        if not title or not ref:
            raise AppError(
                "عنوان و مرجع فایل الزامی است",
                status_code=422,
                error_code="document_fields_required",
            )
        entity = DriverDocument(
            tenant_id=tenant_id,
            driver_id=driver_id,
            kind=body.kind,
            title=title,
            storage_file_ref=ref,
            notes=body.notes,
            created_by=_actor_id(actor),
            updated_by=_actor_id(actor),
        )
        await self.documents.add(entity)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="driver_document",
            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.DRIVER_DOCUMENT_ATTACHED,
            aggregate_type="driver_document",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"driver_id": str(driver_id), "kind": entity.kind.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_documents(
        self, tenant_id: UUID, driver_id: UUID
    ) -> list[DriverDocument]:
        await self.get(tenant_id, driver_id)
        return list(await self.documents.list_for_driver(tenant_id, driver_id))
