from dataclasses import dataclass
from lchttp.json import json_loads
from pgpubsub.channel import BaseChannel
from pgpubsub.listeners import _trigger_action_listener
import pgtrigger
from typing import Callable, Optional, TYPE_CHECKING
from uuid import UUID

from django.conf import settings
from django.utils.module_loading import import_string

from .rooms import RoomChannelRegistry

if TYPE_CHECKING:
    from .models import ConcordEvent, SyncableModel
    from console_base.api.serializers import LCModelSerializer

ROOM_CHANNEL_REGISTRY = RoomChannelRegistry()
try:
    tenanted_publish_rooms = import_string(settings.CONCORD_MQ_TENANTED_PUBLISH_ROOMS)
except (AttributeError, ImportError):
    # print('settings does not define CONCORD_MQ_TENANTED_PUBLISH_ROOMS')

    def tenanted_publish_rooms(*args, **kwargs):
        return []


@dataclass
class ConcordTriggerChannel(BaseChannel):
    """
    Trigger for Concord Event history, deserializing
    JSON record data to a dict.
    """

    model: 'ConcordEvent'
    new: dict

    # attributes for handling Clarion messages
    serializer: type['LCModelSerializer']

    @classmethod
    def room_name(cls) -> str:
        if (name := cls.model._meta.model_name or '').startswith('concord_'):
            return name[8:]
        return name

    @classmethod
    def listen_safe_name(cls) -> str:
        """
        Postgres LISTEN protocol accepts channel names
        which are at most 63 characters long.
        """
        model_name = cls.model().__class__.__name__.lower()  # type: ignore[operator]
        if model_name.endswith('concordevent'):
            model_name = model_name[:-12]
        return f'concord_{model_name}'

    @classmethod
    def deserialize(cls, payload: dict | str) -> dict:
        """
        Override method to use faster JSON parsing and to return
        a dict that will correctly initialize Channel dataclass.
        """
        if isinstance(payload, str):
            payload = json_loads(payload)

        return {
            'new': payload['new'],  # type: ignore
            'model': cls.model,
            'serializer': cls.serializer,
        }

    @classmethod
    def register(cls, callback: Callable) -> None:
        super().register(callback)

        # TODO design way to handle registry uniquely
        # if cls.room_name in ROOM_CHANNEL_REGISTRY:
        #     raise ValueError(f'{cls.room_name} is already found in the registry')
        #
        # ROOM_CHANNEL_REGISTRY[cls.room_name] = cls

    @classmethod
    def publish_room_names(
        cls,
        record: Optional['SyncableModel'] = None,
        origin_appliance_cid: str | UUID = '',
    ) -> list[str]:
        """
        Return all Room Names to Publish new Messages.

        When we're the Sync Publisher, publish to all Sync
        Subscribers that contain the relevant tenant.

        When we're the Sync Subscriber, only publish message
        to the Sync Publisher, which will handle distribution.

        When a message originated from a Subscriber appliance, we
        don't want to include that appliance's room in the list.
        """
        if settings.SYNC_PUBLISHER:
            return tenanted_publish_rooms(record, cls.room_name, origin_appliance_cid)

        return [f'{settings.PUBLISHER_ROOM_PREFIX}/{cls.room_name}/{settings.CONCORD_MQ_TENANT}']

    @classmethod
    def subscribe_room_name(cls) -> str:
        """
        Return complete room name to use when Subscribing to this Room.
        """
        # Sync Publishers should only subscribe to Rooms specifically prefixed for them
        if settings.SYNC_PUBLISHER:
            raise NotImplementedError('sync publisher room name not calculated yet')

        raise NotImplementedError('subscribe room name not calculated yet')

    @classmethod
    def deploy_room_name(cls) -> str:
        """
        Return room name prefix for Sync Publishers to use
        when broadcasting / publishing successful changes on
        the Publisher to all Sync Subscribers.
        """
        raise NotImplementedError('deploy room name not calculated yet')


def post_update_event_listener(channel: type[ConcordTriggerChannel] | str):  # type: ignore[no-untyped-def]
    return _trigger_action_listener(
        channel=channel,
        when=pgtrigger.After,
        operation=pgtrigger.Update,
    )


def post_insert_event_listener(channel: type[ConcordTriggerChannel] | str):  # type: ignore[no-untyped-def]
    return _trigger_action_listener(
        channel=channel,
        when=pgtrigger.After,
        operation=pgtrigger.Insert,
    )


def post_delete_event_listener(channel: type[ConcordTriggerChannel] | str):  # type: ignore[no-untyped-def]
    return _trigger_action_listener(
        channel=channel,
        when=pgtrigger.After,
        operation=pgtrigger.Delete,
    )


__all__ = (
    'ConcordTriggerChannel',
    'post_update_event_listener',
    'post_insert_event_listener',
    'post_delete_event_listener',
    'ROOM_CHANNEL_REGISTRY',
    'RoomChannelRegistry',
)
