"""Delivery foundation application services — Phase 10.0."""
from __future__ import annotations

from datetime import date, datetime
from typing import Any
from uuid import UUID

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

from app.events.publisher import get_event_publisher
from app.events.types import DeliveryEventType
from app.models.foundation import (
    DeliveryConfiguration,
    DeliveryHub,
    DeliveryOrganization,
    DeliverySetting,
    ExternalProviderConfig,
    RoutingEngineRegistration,
)
from app.models.types import AuditAction
from app.repositories.foundation import (
    DeliveryConfigurationRepository,
    DeliveryHubRepository,
    DeliveryOrganizationRepository,
    DeliverySettingRepository,
    ExternalProviderConfigRepository,
    RoutingEngineRegistrationRepository,
)
from app.schemas.foundation import (
    DeliveryConfigurationCreate,
    DeliveryConfigurationUpdate,
    DeliveryHubCreate,
    DeliveryHubUpdate,
    DeliveryOrganizationCreate,
    DeliveryOrganizationUpdate,
    DeliverySettingUpsert,
    ExternalProviderConfigCreate,
    ExternalProviderConfigUpdate,
    RoutingEngineRegistrationCreate,
    RoutingEngineRegistrationUpdate,
)
from app.services.audit_service import AuditService
from app.validators import (
    ensure_optimistic_version,
    validate_code,
    validate_currency_code,
    validate_non_empty,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


def _apply_update(entity, data: dict) -> None:
    for key, value in data.items():
        setattr(entity, key, value)


def _jsonable_changes(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


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


class DeliveryOrganizationService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = DeliveryOrganizationRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: DeliveryOrganizationCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> DeliveryOrganization:
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        currency = validate_currency_code(body.currency_code)
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError(
                "کد سازمان تکراری است",
                error_code="duplicate_code",
                status_code=409,
            )
        entity = DeliveryOrganization(
            tenant_id=tenant_id,
            code=code,
            name=name,
            description=body.description,
            status=body.status,
            legal_name=body.legal_name,
            timezone=body.timezone,
            language=body.language,
            currency_code=currency,
            settings=body.settings,
            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="delivery_organization",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=DeliveryEventType.ORGANIZATION_CREATED,
            aggregate_type="delivery_organization",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "name": entity.name},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

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

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[DeliveryOrganization]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: DeliveryOrganizationUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> DeliveryOrganization:
        entity = await self.get(tenant_id, entity_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_non_empty(data["name"], "name")
        if "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="delivery_organization",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=DeliveryEventType.ORGANIZATION_UPDATED,
            aggregate_type="delivery_organization",
            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,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> DeliveryOrganization:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="delivery_organization",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class DeliveryHubService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = DeliveryHubRepository(session)
        self.orgs = DeliveryOrganizationRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: DeliveryHubCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> DeliveryHub:
        org = await self.orgs.get(tenant_id, body.organization_id)
        if org is None:
            raise NotFoundError("سازمان یافت نشد")
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        entity = DeliveryHub(
            tenant_id=tenant_id,
            organization_id=body.organization_id,
            code=code,
            name=name,
            description=body.description,
            status=body.status,
            address=body.address,
            timezone=body.timezone,
            working_hours=body.working_hours,
            metadata_json=body.metadata_json,
            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="delivery_hub",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=DeliveryEventType.HUB_CREATED,
            aggregate_type="delivery_hub",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "organization_id": str(entity.organization_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

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

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[DeliveryHub]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: DeliveryHubUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> DeliveryHub:
        entity = await self.get(tenant_id, entity_id)
        data = body.model_dump(exclude_unset=True)
        if "name" in data and data["name"] is not None:
            data["name"] = validate_non_empty(data["name"], "name")
        _apply_update(entity, data)
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="delivery_hub",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=DeliveryEventType.HUB_UPDATED,
            aggregate_type="delivery_hub",
            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,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> DeliveryHub:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="delivery_hub",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class ExternalProviderConfigService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = ExternalProviderConfigRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: ExternalProviderConfigCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExternalProviderConfig:
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        adapter_key = validate_non_empty(body.adapter_key, "adapter_key")
        if await self.repo.get_by_code(tenant_id, code):
            raise AppError(
                "کد ارائه‌دهنده تکراری است",
                error_code="duplicate_code",
                status_code=409,
            )
        entity = ExternalProviderConfig(
            tenant_id=tenant_id,
            organization_id=body.organization_id,
            code=code,
            name=name,
            provider_kind=body.provider_kind,
            adapter_key=adapter_key,
            status=body.status,
            priority=body.priority,
            credentials_ref=body.credentials_ref,
            config=body.config,
            capabilities=body.capabilities,
            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="external_provider_config",
            entity_id=entity.id,
            action=AuditAction.REGISTER,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=DeliveryEventType.EXTERNAL_PROVIDER_REGISTERED,
            aggregate_type="external_provider_config",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "adapter_key": entity.adapter_key},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

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

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[ExternalProviderConfig]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: ExternalProviderConfigUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ExternalProviderConfig:
        entity = await self.get(tenant_id, entity_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_non_empty(data["name"], "name")
        if "adapter_key" in data and data["adapter_key"] is not None:
            data["adapter_key"] = validate_non_empty(data["adapter_key"], "adapter_key")
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="external_provider_config",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=DeliveryEventType.EXTERNAL_PROVIDER_UPDATED,
            aggregate_type="external_provider_config",
            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,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> ExternalProviderConfig:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="external_provider_config",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class RoutingEngineRegistrationService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = RoutingEngineRegistrationRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: RoutingEngineRegistrationCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> RoutingEngineRegistration:
        code = validate_code(body.code)
        name = validate_non_empty(body.name, "name")
        adapter_key = validate_non_empty(body.adapter_key, "adapter_key")
        entity = RoutingEngineRegistration(
            tenant_id=tenant_id,
            organization_id=body.organization_id,
            code=code,
            name=name,
            adapter_key=adapter_key,
            status=body.status,
            is_default=body.is_default,
            config=body.config,
            capabilities=body.capabilities,
            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="routing_engine_registration",
            entity_id=entity.id,
            action=AuditAction.REGISTER,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=DeliveryEventType.ROUTING_ENGINE_REGISTERED,
            aggregate_type="routing_engine_registration",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"code": entity.code, "adapter_key": entity.adapter_key},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, entity_id: UUID) -> RoutingEngineRegistration:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("موتور مسیریابی یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[RoutingEngineRegistration]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: RoutingEngineRegistrationUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> RoutingEngineRegistration:
        entity = await self.get(tenant_id, entity_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_non_empty(data["name"], "name")
        if "adapter_key" in data and data["adapter_key"] is not None:
            data["adapter_key"] = validate_non_empty(data["adapter_key"], "adapter_key")
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="routing_engine_registration",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=DeliveryEventType.ROUTING_ENGINE_UPDATED,
            aggregate_type="routing_engine_registration",
            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,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> RoutingEngineRegistration:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="routing_engine_registration",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class DeliveryConfigurationService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = DeliveryConfigurationRepository(session)
        self.orgs = DeliveryOrganizationRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def create(
        self,
        tenant_id: UUID,
        body: DeliveryConfigurationCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> DeliveryConfiguration:
        org = await self.orgs.get(tenant_id, body.organization_id)
        if org is None:
            raise NotFoundError("سازمان یافت نشد")
        code = validate_code(body.code)
        currency = validate_currency_code(body.currency_code)
        entity = DeliveryConfiguration(
            tenant_id=tenant_id,
            organization_id=body.organization_id,
            hub_id=body.hub_id,
            code=code,
            working_hours=body.working_hours,
            dispatch_policies=body.dispatch_policies,
            tracking_policies=body.tracking_policies,
            settlement_policies=body.settlement_policies,
            custom_fields=body.custom_fields,
            timezone=body.timezone,
            language=body.language,
            currency_code=currency,
            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="delivery_configuration",
            entity_id=entity.id,
            action=AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=DeliveryEventType.CONFIGURATION_CREATED,
            aggregate_type="delivery_configuration",
            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, entity_id: UUID) -> DeliveryConfiguration:
        entity = await self.repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError("پیکربندی یافت نشد")
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[DeliveryConfiguration]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def update(
        self,
        tenant_id: UUID,
        entity_id: UUID,
        body: DeliveryConfigurationUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> DeliveryConfiguration:
        entity = await self.get(tenant_id, entity_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        if "currency_code" in data and data["currency_code"] is not None:
            data["currency_code"] = validate_currency_code(data["currency_code"])
        _apply_update(entity, data)
        entity.version += 1
        entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="delivery_configuration",
            entity_id=entity.id,
            action=AuditAction.UPDATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(data),
        )
        self.events.publish(
            event_type=DeliveryEventType.CONFIGURATION_UPDATED,
            aggregate_type="delivery_configuration",
            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,
        entity_id: UUID,
        *,
        actor: CurrentUser | None = None,
    ) -> DeliveryConfiguration:
        entity = await self.get(tenant_id, entity_id)
        await self.repo.soft_delete(entity, deleted_by=_actor_id(actor))
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="delivery_configuration",
            entity_id=entity.id,
            action=AuditAction.DELETE,
            actor_user_id=_actor_id(actor),
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class DeliverySettingService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = DeliverySettingRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()

    async def upsert(
        self,
        tenant_id: UUID,
        body: DeliverySettingUpsert,
        *,
        actor: CurrentUser | None = None,
    ) -> DeliverySetting:
        key = validate_non_empty(body.key, "key")
        existing = await self.repo.get_by_key(
            tenant_id,
            key,
            organization_id=body.organization_id,
            hub_id=body.hub_id,
        )
        if existing is None:
            entity = DeliverySetting(
                tenant_id=tenant_id,
                organization_id=body.organization_id,
                hub_id=body.hub_id,
                key=key,
                value=body.value,
                description=body.description,
                created_by=_actor_id(actor),
                updated_by=_actor_id(actor),
            )
            await self.repo.add(entity)
        else:
            entity = existing
            entity.value = body.value
            entity.description = body.description
            entity.updated_by = _actor_id(actor)
        await self.audit.record(
            tenant_id=tenant_id,
            entity_type="delivery_setting",
            entity_id=entity.id,
            action=AuditAction.UPDATE if existing else AuditAction.CREATE,
            actor_user_id=_actor_id(actor),
            changes=_jsonable_changes(body.model_dump()),
        )
        self.events.publish(
            event_type=DeliveryEventType.SETTING_UPSERTED,
            aggregate_type="delivery_setting",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"key": entity.key},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list(
        self, tenant_id: UUID, *, offset: int = 0, limit: int = 20
    ) -> list[DeliverySetting]:
        return list(await self.repo.list_by_tenant(tenant_id, offset=offset, limit=limit))
