"""Consumer connectors, widgets & notify dispatches — Phase 11.9."""
from datetime import datetime, timezone
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import get_event_publisher
from app.events.types import ExperienceEventType
from app.models.integrations import (
    ExperienceConsumerConnectorDispatch,
    ExperienceConsumerConnectorRegistration,
    ExperienceNotifyDispatch,
    ExperienceWidgetDefinition,
    ExperienceWidgetInstance,
)
from app.models.types import (
    AuditAction,
    ConnectorDispatchStatus,
    ConsumerConnectorKind,
    ConsumerConnectorStatus,
    NotifyDispatchStatus,
    WidgetDefinitionStatus,
    WidgetInstanceStatus,
)
from app.policies.integrations import (
    ConsumerConnectorDispatchPolicy,
    ConsumerConnectorRegistrationPolicy,
    NotifyDispatchPolicy,
    WidgetDefinitionPolicy,
    WidgetInstancePolicy,
)
from app.providers.mocks import (
    MockCRMProvider,
    MockCommunicationProvider,
    MockHospitalityProvider,
    MockMarketplaceProvider,
    MockNotificationProvider,
    MockSportsCenterProvider,
)
from app.repositories.foundation import ExperienceWorkspaceRepository
from app.repositories.integrations import (
    ConsumerConnectorDispatchRepository,
    ConsumerConnectorRegistrationRepository,
    NotifyDispatchRepository,
    WidgetDefinitionRepository,
    WidgetInstanceRepository,
)
from app.repositories.sites import ExperiencePageRepository, ExperienceSiteRepository
from app.services.audit_service import AuditService
from app.validators import ensure_optimistic_version
from app.validators.integrations import (
    validate_config,
    validate_default_config,
    validate_metadata,
    validate_payload,
)
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser

_HOSPITALITY_KINDS = frozenset(
    {
        ConsumerConnectorKind.HOSPITALITY,
        ConsumerConnectorKind.HOTEL,
        ConsumerConnectorKind.CLINIC,
        ConsumerConnectorKind.ACADEMY,
        ConsumerConnectorKind.TOURISM,
    }
)

_MOCK_HOSPITALITY = MockHospitalityProvider()
_MOCK_MARKETPLACE = MockMarketplaceProvider()
_MOCK_SPORTS = MockSportsCenterProvider()
_MOCK_CRM = MockCRMProvider()
_MOCK_COMMUNICATION = MockCommunicationProvider()
_MOCK_NOTIFICATION = MockNotificationProvider()


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


def _jsonable(value):
    if isinstance(value, dict):
        return {key: _jsonable(item) for key, item in value.items()}
    if isinstance(value, (list, tuple)):
        return [_jsonable(item) for item in value]
    if isinstance(value, (UUID, datetime)):
        return value.isoformat() if isinstance(value, datetime) else str(value)
    if hasattr(value, "value"):
        return value.value
    return value


async def _invoke_connector_mock(
    kind: ConsumerConnectorKind, *, tenant_id: UUID, payload_ref: str | None, event_type: str
) -> dict:
    ref = payload_ref or event_type
    if kind in _HOSPITALITY_KINDS:
        return await _MOCK_HOSPITALITY.resolve_menu_ref(tenant_id=tenant_id, menu_ref=ref)
    if kind == ConsumerConnectorKind.MARKETPLACE:
        return await _MOCK_MARKETPLACE.resolve_product_ref(tenant_id=tenant_id, product_ref=ref)
    if kind == ConsumerConnectorKind.SPORTS_CENTER:
        return await _MOCK_SPORTS.resolve_member_ref(tenant_id=tenant_id, member_ref=ref)
    if kind == ConsumerConnectorKind.CRM:
        return await _MOCK_CRM.resolve_contact(tenant_id=tenant_id, contact_ref=ref)
    if kind == ConsumerConnectorKind.COMMUNICATION:
        return await _MOCK_COMMUNICATION.send(
            tenant_id=tenant_id,
            channel="connector",
            template_key=event_type,
            to=ref,
            payload={"event_type": event_type},
        )
    return {"ok": True, "provider": "mock", "kind": kind.value, "ref": ref}


class ExperienceIntegrationsService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.registrations = ConsumerConnectorRegistrationRepository(session)
        self.dispatches = ConsumerConnectorDispatchRepository(session)
        self.widget_definitions = WidgetDefinitionRepository(session)
        self.widget_instances = WidgetInstanceRepository(session)
        self.notify_dispatches = NotifyDispatchRepository(session)
        self.workspaces = ExperienceWorkspaceRepository(session)
        self.sites = ExperienceSiteRepository(session)
        self.pages = ExperiencePageRepository(session)
        self.audit = AuditService(session)
        self.events = get_event_publisher()
        self.policies = {
            "consumer_connector_registration": ConsumerConnectorRegistrationPolicy(),
            "consumer_connector_dispatch": ConsumerConnectorDispatchPolicy(),
            "widget_definition": WidgetDefinitionPolicy(),
            "widget_instance": WidgetInstancePolicy(),
            "notify_dispatch": NotifyDispatchPolicy(),
        }

    async def _scope(self, tenant_id, workspace_id, site_id=None, page_id=None):
        if await self.workspaces.get(tenant_id, workspace_id) is None:
            raise NotFoundError("فضای کاری یافت نشد")
        if site_id is not None:
            site = await self.sites.get(tenant_id, site_id)
            if site is None or site.workspace_id != workspace_id:
                raise NotFoundError("سایت یافت نشد")
        if page_id is not None:
            page = await self.pages.get(tenant_id, page_id)
            if page is None:
                raise NotFoundError("صفحه یافت نشد")
            if site_id is not None and page.site_id != site_id:
                raise AppError(
                    "صفحه متعلق به سایت اعلام‌شده نیست",
                    error_code="page_site_mismatch",
                    status_code=422,
                )

    async def _finish(self, entity, kind, action, event, actor, changes=None):
        await self.audit.record(
            tenant_id=entity.tenant_id,
            entity_type=kind,
            entity_id=entity.id,
            action=action,
            actor_user_id=_actor(actor),
            changes=_jsonable(changes or {}),
        )
        self.events.publish(
            event_type=event,
            aggregate_type=kind,
            aggregate_id=entity.id,
            tenant_id=entity.tenant_id,
            payload={"status": getattr(entity.status, "value", entity.status)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def _get(self, repo, tenant_id, entity_id, label):
        entity = await repo.get(tenant_id, entity_id)
        if entity is None:
            raise NotFoundError(f"{label} یافت نشد")
        return entity

    async def _list(self, repo, tenant_id, *, offset, limit, spec):
        items, total = await repo.list_filtered(tenant_id, offset=offset, limit=limit, spec=spec)
        return list(items), total

    async def _update(self, repo, tenant_id, entity_id, body, kind, policy, event, actor, validators=None):
        entity = await self._get(repo, tenant_id, entity_id, kind)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        validators = validators or {}
        for field, validator in validators.items():
            if field in data:
                data[field] = validator(data[field])
        if "metadata_json" in data:
            data["metadata_json"] = validate_metadata(data["metadata_json"])
        if data.get("status") is not None:
            policy.assert_status_change(current=entity.status, target=data["status"])
        for field, value in data.items():
            setattr(entity, field, value)
        entity.updated_by = _actor(actor)
        entity.version += 1
        return await self._finish(entity, kind, AuditAction.UPDATE, event, actor, data)

    async def _delete(self, repo, tenant_id, entity_id, kind, event, actor):
        entity = await self._get(repo, tenant_id, entity_id, kind)
        await repo.soft_delete(entity, deleted_by=_actor(actor))
        entity.updated_by = _actor(actor)
        entity.version += 1
        return await self._finish(entity, kind, AuditAction.DELETE, event, actor)

    async def create_consumer_connector_registration(self, tenant_id, body, *, actor=None):
        await self._scope(tenant_id, body.workspace_id, body.site_id, body.page_id)
        entity = ExperienceConsumerConnectorRegistration(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            site_id=body.site_id,
            page_id=body.page_id,
            kind=body.kind,
            code=body.code,
            name=body.name,
            status=body.status,
            external_service_ref=body.external_service_ref,
            entity_ref=body.entity_ref,
            config_json=validate_config(body.config_json),
            metadata_json=validate_metadata(body.metadata_json),
            created_by=_actor(actor),
            updated_by=_actor(actor),
        )
        await self.registrations.add(entity)
        return await self._finish(
            entity,
            "consumer_connector_registration",
            AuditAction.CREATE,
            ExperienceEventType.CONSUMER_CONNECTOR_REGISTRATION_CREATED,
            actor,
        )

    async def get_consumer_connector_registration(self, tenant_id, entity_id):
        return await self._get(
            self.registrations, tenant_id, entity_id, "consumer connector registration"
        )

    async def list_consumer_connector_registrations(self, tenant_id, *, offset, limit, spec=None):
        return await self._list(self.registrations, tenant_id, offset=offset, limit=limit, spec=spec)

    async def update_consumer_connector_registration(self, tenant_id, entity_id, body, *, actor=None):
        return await self._update(
            self.registrations,
            tenant_id,
            entity_id,
            body,
            "consumer_connector_registration",
            self.policies["consumer_connector_registration"],
            ExperienceEventType.CONSUMER_CONNECTOR_REGISTRATION_UPDATED,
            actor,
            validators={"config_json": validate_config},
        )

    async def delete_consumer_connector_registration(self, tenant_id, entity_id, *, actor=None):
        return await self._delete(
            self.registrations,
            tenant_id,
            entity_id,
            "consumer_connector_registration",
            ExperienceEventType.CONSUMER_CONNECTOR_REGISTRATION_DELETED,
            actor,
        )

    async def create_consumer_connector_dispatch(self, tenant_id, body, *, actor=None):
        await self._scope(tenant_id, body.workspace_id, body.site_id, body.page_id)
        registration = await self.get_consumer_connector_registration(tenant_id, body.registration_id)
        if registration.workspace_id != body.workspace_id:
            raise AppError(
                "رابط اتصال متعلق به فضای کاری نیست",
                error_code="registration_workspace_mismatch",
                status_code=422,
            )
        entity = ExperienceConsumerConnectorDispatch(
            tenant_id=tenant_id,
            registration_id=body.registration_id,
            workspace_id=body.workspace_id,
            site_id=body.site_id,
            page_id=body.page_id,
            direction=body.direction,
            event_type=body.event_type,
            payload_ref=body.payload_ref,
            status=ConnectorDispatchStatus.PENDING,
            notes=body.notes,
            created_by=_actor(actor),
            updated_by=_actor(actor),
        )
        await self.dispatches.add(entity)
        return await self._finish(
            entity,
            "consumer_connector_dispatch",
            AuditAction.CREATE,
            ExperienceEventType.CONSUMER_CONNECTOR_DISPATCH_CREATED,
            actor,
        )

    async def get_consumer_connector_dispatch(self, tenant_id, entity_id):
        return await self._get(
            self.dispatches, tenant_id, entity_id, "consumer connector dispatch"
        )

    async def list_consumer_connector_dispatches(self, tenant_id, *, offset, limit, spec=None):
        return await self._list(self.dispatches, tenant_id, offset=offset, limit=limit, spec=spec)

    async def update_consumer_connector_dispatch(self, tenant_id, entity_id, body, *, actor=None):
        return await self._update(
            self.dispatches,
            tenant_id,
            entity_id,
            body,
            "consumer_connector_dispatch",
            self.policies["consumer_connector_dispatch"],
            ExperienceEventType.CONSUMER_CONNECTOR_DISPATCH_UPDATED,
            actor,
        )

    async def delete_consumer_connector_dispatch(self, tenant_id, entity_id, *, actor=None):
        return await self._delete(
            self.dispatches,
            tenant_id,
            entity_id,
            "consumer_connector_dispatch",
            ExperienceEventType.CONSUMER_CONNECTOR_DISPATCH_DELETED,
            actor,
        )

    async def execute_consumer_connector_dispatch(self, tenant_id, entity_id, *, actor=None):
        dispatch = await self.get_consumer_connector_dispatch(tenant_id, entity_id)
        if dispatch.status != ConnectorDispatchStatus.PENDING:
            raise AppError(
                "ارسال قبلاً پردازش شده است",
                error_code="dispatch_not_pending",
                status_code=409,
            )
        registration = await self.get_consumer_connector_registration(
            tenant_id, dispatch.registration_id
        )
        if registration.status != ConsumerConnectorStatus.ACTIVE:
            dispatch.status = ConnectorDispatchStatus.FAILED
            dispatch.response_json = {"ok": False, "reason": "registration_not_active"}
        else:
            try:
                response = await _invoke_connector_mock(
                    registration.kind,
                    tenant_id=tenant_id,
                    payload_ref=dispatch.payload_ref,
                    event_type=dispatch.event_type,
                )
                dispatch.status = ConnectorDispatchStatus.ACKNOWLEDGED
                dispatch.response_json = response
            except Exception as exc:  # noqa: BLE001 — mock boundary only
                dispatch.status = ConnectorDispatchStatus.FAILED
                dispatch.response_json = {"ok": False, "reason": str(exc)}
        dispatch.updated_by = _actor(actor)
        dispatch.version += 1
        return await self._finish(
            dispatch,
            "consumer_connector_dispatch",
            AuditAction.UPDATE,
            ExperienceEventType.CONSUMER_CONNECTOR_DISPATCH_EXECUTED,
            actor,
            {"status": dispatch.status.value},
        )

    async def create_widget_definition(self, tenant_id, body, *, actor=None):
        if body.workspace_id is not None:
            await self._scope(tenant_id, body.workspace_id)
        entity = ExperienceWidgetDefinition(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            code=body.code,
            widget_kind=body.widget_kind,
            name=body.name,
            description=body.description,
            status=body.status,
            connector_kind=body.connector_kind,
            default_config_json=validate_default_config(body.default_config_json),
            permission_prefixes=body.permission_prefixes,
            metadata_json=validate_metadata(body.metadata_json),
            created_by=_actor(actor),
            updated_by=_actor(actor),
        )
        await self.widget_definitions.add(entity)
        return await self._finish(
            entity,
            "widget_definition",
            AuditAction.CREATE,
            ExperienceEventType.WIDGET_DEFINITION_CREATED,
            actor,
        )

    async def get_widget_definition(self, tenant_id, entity_id):
        return await self._get(self.widget_definitions, tenant_id, entity_id, "widget definition")

    async def list_widget_definitions(self, tenant_id, *, offset, limit, spec=None):
        return await self._list(self.widget_definitions, tenant_id, offset=offset, limit=limit, spec=spec)

    async def update_widget_definition(self, tenant_id, entity_id, body, *, actor=None):
        return await self._update(
            self.widget_definitions,
            tenant_id,
            entity_id,
            body,
            "widget_definition",
            self.policies["widget_definition"],
            ExperienceEventType.WIDGET_DEFINITION_UPDATED,
            actor,
            validators={"default_config_json": validate_default_config},
        )

    async def delete_widget_definition(self, tenant_id, entity_id, *, actor=None):
        return await self._delete(
            self.widget_definitions,
            tenant_id,
            entity_id,
            "widget_definition",
            ExperienceEventType.WIDGET_DEFINITION_DELETED,
            actor,
        )

    async def create_widget_instance(self, tenant_id, body, *, actor=None):
        await self._scope(tenant_id, body.workspace_id, body.site_id, body.page_id)
        definition = await self.get_widget_definition(tenant_id, body.widget_definition_id)
        if definition.status != WidgetDefinitionStatus.AVAILABLE:
            raise AppError(
                "تعریف ویجت در دسترس نیست",
                error_code="widget_definition_unavailable",
                status_code=409,
            )
        if definition.widget_kind != body.widget_kind:
            raise AppError(
                "نوع ویجت با تعریف مطابقت ندارد",
                error_code="widget_kind_mismatch",
                status_code=422,
            )
        if body.connector_registration_id is not None:
            await self.get_consumer_connector_registration(tenant_id, body.connector_registration_id)
        entity = ExperienceWidgetInstance(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            site_id=body.site_id,
            page_id=body.page_id,
            widget_definition_id=definition.id,
            widget_kind=body.widget_kind,
            code=body.code,
            name=body.name,
            status=body.status,
            connector_registration_id=body.connector_registration_id,
            entity_ref=body.entity_ref,
            embed_ref=body.embed_ref,
            config_json=validate_config(body.config_json),
            metadata_json=validate_metadata(body.metadata_json),
            created_by=_actor(actor),
            updated_by=_actor(actor),
        )
        await self.widget_instances.add(entity)
        return await self._finish(
            entity,
            "widget_instance",
            AuditAction.CREATE,
            ExperienceEventType.WIDGET_INSTANCE_CREATED,
            actor,
        )

    async def get_widget_instance(self, tenant_id, entity_id):
        return await self._get(self.widget_instances, tenant_id, entity_id, "widget instance")

    async def get_widget_instance_by_embed(self, tenant_id, embed_ref):
        entity = await self.widget_instances.get_by_embed_ref(tenant_id, embed_ref)
        if entity is None:
            raise NotFoundError("ویجت یافت نشد")
        return entity

    async def list_widget_instances(self, tenant_id, *, offset, limit, spec=None):
        return await self._list(self.widget_instances, tenant_id, offset=offset, limit=limit, spec=spec)

    async def update_widget_instance(self, tenant_id, entity_id, body, *, actor=None):
        return await self._update(
            self.widget_instances,
            tenant_id,
            entity_id,
            body,
            "widget_instance",
            self.policies["widget_instance"],
            ExperienceEventType.WIDGET_INSTANCE_UPDATED,
            actor,
            validators={"config_json": validate_config},
        )

    async def delete_widget_instance(self, tenant_id, entity_id, *, actor=None):
        return await self._delete(
            self.widget_instances,
            tenant_id,
            entity_id,
            "widget_instance",
            ExperienceEventType.WIDGET_INSTANCE_DELETED,
            actor,
        )

    async def create_notify_dispatch(self, tenant_id, body, *, actor=None):
        await self._scope(tenant_id, body.workspace_id, body.site_id, body.page_id)
        if body.widget_instance_id is not None:
            await self.get_widget_instance(tenant_id, body.widget_instance_id)
        if body.connector_registration_id is not None:
            await self.get_consumer_connector_registration(tenant_id, body.connector_registration_id)
        entity = ExperienceNotifyDispatch(
            tenant_id=tenant_id,
            workspace_id=body.workspace_id,
            site_id=body.site_id,
            page_id=body.page_id,
            widget_instance_id=body.widget_instance_id,
            connector_registration_id=body.connector_registration_id,
            channel=body.channel,
            template_key=body.template_key,
            to_ref=body.to_ref,
            payload_json=validate_payload(body.payload_json),
            status=NotifyDispatchStatus.PENDING,
            metadata_json=validate_metadata(body.metadata_json),
            created_by=_actor(actor),
            updated_by=_actor(actor),
        )
        await self.notify_dispatches.add(entity)
        return await self._finish(
            entity,
            "notify_dispatch",
            AuditAction.CREATE,
            ExperienceEventType.NOTIFY_DISPATCH_CREATED,
            actor,
        )

    async def get_notify_dispatch(self, tenant_id, entity_id):
        return await self._get(self.notify_dispatches, tenant_id, entity_id, "notify dispatch")

    async def list_notify_dispatches(self, tenant_id, *, offset, limit, spec=None):
        return await self._list(self.notify_dispatches, tenant_id, offset=offset, limit=limit, spec=spec)

    async def update_notify_dispatch(self, tenant_id, entity_id, body, *, actor=None):
        return await self._update(
            self.notify_dispatches,
            tenant_id,
            entity_id,
            body,
            "notify_dispatch",
            self.policies["notify_dispatch"],
            ExperienceEventType.NOTIFY_DISPATCH_UPDATED,
            actor,
            validators={"payload_json": validate_payload},
        )

    async def delete_notify_dispatch(self, tenant_id, entity_id, *, actor=None):
        return await self._delete(
            self.notify_dispatches,
            tenant_id,
            entity_id,
            "notify_dispatch",
            ExperienceEventType.NOTIFY_DISPATCH_DELETED,
            actor,
        )

    async def send_notify_dispatch(self, tenant_id, entity_id, *, actor=None):
        dispatch = await self.get_notify_dispatch(tenant_id, entity_id)
        if dispatch.status != NotifyDispatchStatus.PENDING:
            raise AppError(
                "اعلان قبلاً ارسال شده است",
                error_code="notify_not_pending",
                status_code=409,
            )
        payload = dispatch.payload_json or {}
        try:
            if dispatch.channel in {"notification", "push"}:
                response = await _MOCK_NOTIFICATION.notify(
                    tenant_id=tenant_id,
                    channel=dispatch.channel,
                    template_key=dispatch.template_key,
                    payload=payload,
                )
            else:
                response = await _MOCK_COMMUNICATION.send(
                    tenant_id=tenant_id,
                    channel=dispatch.channel,
                    template_key=dispatch.template_key,
                    to=dispatch.to_ref or "",
                    payload=payload,
                )
            dispatch.status = NotifyDispatchStatus.SENT
            dispatch.communication_request_ref = response.get("communication_request_ref")
            dispatch.response_json = response
        except Exception as exc:  # noqa: BLE001 — mock boundary only
            dispatch.status = NotifyDispatchStatus.FAILED
            dispatch.response_json = {"ok": False, "reason": str(exc)}
        dispatch.updated_by = _actor(actor)
        dispatch.version += 1
        return await self._finish(
            dispatch,
            "notify_dispatch",
            AuditAction.UPDATE,
            ExperienceEventType.NOTIFY_DISPATCH_SENT,
            actor,
            {"status": dispatch.status.value},
        )
