import { Injectable, inject } from '@angular/core';
import { Action, Store } from '@ngrx/store';
import { Subscription } from 'rxjs';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { ServerDirectoryFacade } from '../../../server-directory';
import { resolveRoomPermission } from '../../../access-control';
import { AttachmentFacade } from '../../../attachment';
import { VoiceConnectionFacade } from '../../../voice-connection/application/facades/voice-connection.facade';
import type {
  Channel,
  ChatEvent,
  Message,
  MessageRevision,
  PluginCapabilityId,
  PluginEventEnvelope,
  TojuPluginManifest,
  User
} from '../../../../shared-kernel';
import { MessagesActions } from '../../../../store/messages/messages.actions';
import { CHUNK_SIZE, chunkArray } from '../../../../store/messages/messages.helpers';
import { selectCurrentRoomMessages } from '../../../../store/messages/messages.selectors';
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
import {
  selectActiveChannelId,
  selectCurrentRoom,
  selectCurrentRoomChannels,
  selectCurrentRoomId
} from '../../../../store/rooms/rooms.selectors';
import { UsersActions } from '../../../../store/users/users.actions';
import { selectAllUsers, selectCurrentUser } from '../../../../store/users/users.selectors';
import { defaultChannels } from '../../../../store/rooms/room-channels.defaults';
import { isChannelNameTaken, normalizeChannelName } from '../../../../store/rooms/room-channels.rules';
import type {
  PluginApiAvatarUpdate,
  PluginApiActionContext,
  PluginApiActionSource,
  PluginApiAttachmentImportRequest,
  PluginApiChannelRequest,
  PluginApiCustomStreamRequest,
  PluginApiMessageAsPluginUserRequest,
  PluginApiServerSettingsUpdate,
  PluginApiSlashCommandContext,
  PluginApiTypingEvent,
  TojuClientPluginApi,
  TojuPluginDisposable
} from '../../domain/models/plugin-api.models';
import { PluginCapabilityService } from './plugin-capability.service';
import { PluginLoggerService } from './plugin-logger.service';
import { PluginMessageBusService } from './plugin-message-bus.service';
import { PluginStorageService } from './plugin-storage.service';
import { PluginUiRegistryService } from './plugin-ui-registry.service';
import { MessageRevisionService } from '../../../chat/application/services/message-revision.service';
import { materializeMessageFromRevision } from '../../../chat/domain/rules/message-revision.builder.rules';

@Injectable({ providedIn: 'root' })
export class PluginClientApiService {
  private readonly attachments = inject(AttachmentFacade);
  private readonly capabilities = inject(PluginCapabilityService);
  private readonly db = inject(DatabaseService);
  private readonly logger = inject(PluginLoggerService);
  private readonly messageBus = inject(PluginMessageBusService);
  private readonly realtime = inject(RealtimeSessionFacade);
  private readonly serverDirectory = inject(ServerDirectoryFacade);
  private readonly store = inject(Store);
  private readonly storage = inject(PluginStorageService);
  private readonly uiRegistry = inject(PluginUiRegistryService);
  private readonly voice = inject(VoiceConnectionFacade);
  private readonly messageRevisions = inject(MessageRevisionService);

  private readonly currentMessages = this.store.selectSignal(selectCurrentRoomMessages);
  private readonly currentRoom = this.store.selectSignal(selectCurrentRoom);
  private readonly currentRoomChannels = this.store.selectSignal(selectCurrentRoomChannels);
  private readonly currentRoomId = this.store.selectSignal(selectCurrentRoomId);
  private readonly activeChannelId = this.store.selectSignal(selectActiveChannelId);
  private readonly currentUser = this.store.selectSignal(selectCurrentUser);
  private readonly users = this.store.selectSignal(selectAllUsers);

  createApi(manifest: TojuPluginManifest): TojuClientPluginApi {
    const pluginId = manifest.id;
    const requireCapability = (capability: PluginCapabilityId): void => this.capabilities.assert(pluginId, capability);
    const assertEvent = (eventName: string): void => this.assertDeclaredEvent(manifest, eventName);

    return deepFreeze<TojuClientPluginApi>({
      commands: {
        list: () => {
          requireCapability('ui.commands');
          return this.uiRegistry.slashCommands();
        },
        register: (id, contribution) => {
          requireCapability('ui.commands');
          return this.uiRegistry.registerSlashCommand(pluginId, id, contribution);
        }
      },
      channels: {
        addAudioChannel: (request) => {
          requireCapability('channels.manage');
          this.addPluginManagedChannel(pluginId, createChannel(request, 'voice'));
        },
        addTextChannel: (request) => {
          requireCapability('channels.manage');
          this.addPluginManagedChannel(pluginId, createChannel(request, 'text'));
        },
        addVideoChannel: (request) => {
          requireCapability('channels.manage');
          this.uiRegistry.registerChannelSection(pluginId, request.id ?? request.name, {
            label: request.name,
            order: request.position,
            type: 'video'
          });
        },
        list: () => {
          requireCapability('channels.read');
          return this.currentRoomChannels();
        },
        remove: (channelId) => {
          requireCapability('channels.manage');
          this.store.dispatch(RoomsActions.removeChannel({ channelId }));
        },
        rename: (channelId, name) => {
          requireCapability('channels.manage');
          this.store.dispatch(RoomsActions.renameChannel({ channelId, name }));
        },
        select: (channelId) => {
          requireCapability('channels.read');
          this.store.dispatch(RoomsActions.selectChannel({ channelId }));
        }
      },
      events: {
        publishP2p: (eventName, payload) => {
          requireCapability('events.p2p.publish');
          assertEvent(eventName);
          this.broadcastPluginEvent(pluginId, eventName, payload, 'p2p');
        },
        publishServer: (eventName, payload) => {
          requireCapability('events.server.publish');
          assertEvent(eventName);
          this.publishServerPluginEvent(pluginId, eventName, payload);
        },
        subscribeP2p: (subscription) => {
          requireCapability('events.p2p.subscribe');
          assertEvent(subscription.eventName);
          return this.rememberSubscription(pluginId, subscription.eventName);
        },
        subscribeServer: (subscription) => {
          requireCapability('events.server.subscribe');
          assertEvent(subscription.eventName);
          return this.subscribeServerPluginEvent(pluginId, subscription.eventName, subscription.handler);
        }
      },
      logger: {
        debug: (message, data) => this.logger.debug(pluginId, message, data),
        error: (message, data) => this.logger.error(pluginId, message, data),
        info: (message, data) => this.logger.info(pluginId, message, data),
        warn: (message, data) => this.logger.warn(pluginId, message, data)
      },
      context: {
        getCurrent: () => this.createActionContext('manual')
      },
      clientData: {
        read: async (key) => {
          requireCapability('storage.local');
          return await this.storage.readClientData(pluginId, key);
        },
        remove: async (key) => {
          requireCapability('storage.local');
          await this.storage.removeClientData(pluginId, key);
        },
        write: async (key, value) => {
          requireCapability('storage.local');
          await this.storage.writeClientData(pluginId, key, value);
        }
      },
      attachments: {
        import: async (request: PluginApiAttachmentImportRequest) => {
          requireCapability('messages.sync');
          const roomId = this.requireRoomId();

          this.attachments.rememberMessageRoom(request.messageId, roomId);
          await this.attachments.publishAttachments(request.messageId, request.files, this.currentUser()?.id);
        }
      },
      media: {
        addCustomAudioStream: async (request) => {
          requireCapability('media.addAudioStream');
          await this.voice.setLocalStream(request.stream);
        },
        addCustomVideoStream: async (_request: PluginApiCustomStreamRequest) => {
          requireCapability('media.addVideoStream');
          this.logger.info(pluginId, 'Video stream contribution registered');
        },
        playAudioClip: async (request) => {
          requireCapability('media.playAudio');
          await playAudioClip(request.url, request.volume);
        },
        setInputVolume: (volume) => {
          requireCapability('audio.volume');
          this.voice.setInputVolume(volume);
        },
        setOutputVolume: (volume) => {
          requireCapability('audio.volume');
          this.voice.setOutputVolume(volume);
        }
      },
      messages: {
        delete: (messageId) => {
          requireCapability('messages.deleteOwn');
          this.deletePluginMessage(pluginId, messageId);
        },
        edit: (messageId, content) => {
          requireCapability('messages.editOwn');
          this.editPluginMessage(pluginId, messageId, content);
        },
        moderateDelete: (messageId) => {
          requireCapability('messages.moderate');
          this.store.dispatch(MessagesActions.adminDeleteMessage({ messageId }));
        },
        readCurrent: () => {
          requireCapability('messages.read');
          return this.currentMessages();
        },
        send: (content, channelId) => {
          requireCapability('messages.send');
          return this.sendPluginMessage(pluginId, content, channelId);
        },
        sendAsPluginUser: (request) => {
          requireCapability('messages.send');
          this.receivePluginUserMessage(pluginId, request);
        },
        import: async (messages) => {
          requireCapability('messages.sync');
          await this.importPluginMessages(pluginId, messages);
        },
        setTyping: (isTyping, channelId) => {
          requireCapability('messages.send');
          this.setTyping(pluginId, isTyping, channelId);
        },
        subscribeTyping: (handler) => {
          requireCapability('messages.read');
          return this.subscribeTyping(pluginId, handler);
        },
        sync: (messages) => {
          requireCapability('messages.sync');
          this.store.dispatch(MessagesActions.syncMessages({ messages }));
        }
      },
      messageBus: {
        publish: (request) => {
          requireCapability('events.p2p.publish');

          if (request.includeLatestMessages) {
            requireCapability('messages.read');
          }

          return this.messageBus.publish(pluginId, request);
        },
        sendLatestMessages: (request = {}) => {
          requireCapability('events.p2p.publish');
          requireCapability('messages.read');
          return this.messageBus.sendLatestMessages(pluginId, request);
        },
        subscribe: (subscription) => {
          requireCapability('events.p2p.subscribe');

          if (subscription.replayLatest) {
            requireCapability('messages.read');
          }

          return this.messageBus.subscribe(pluginId, subscription);
        }
      },
      p2p: {
        broadcastData: (eventName, payload) => {
          requireCapability('p2p.data');
          this.broadcastPluginEvent(pluginId, eventName, payload, 'p2p');
        },
        connectedPeers: () => {
          requireCapability('p2p.data');
          return this.voice.getConnectedPeers();
        },
        sendData: (peerId, eventName, payload) => {
          requireCapability('p2p.data');
          this.broadcastPluginEvent(pluginId, eventName, { payload, peerId }, 'p2p');
        }
      },
      profile: {
        getCurrent: () => {
          requireCapability('profile.read');
          return this.currentUser() ?? null;
        },
        update: (profile) => {
          requireCapability('profile.write');
          this.store.dispatch(UsersActions.updateCurrentUserProfile({
            profile: {
              ...profile,
              profileUpdatedAt: Date.now()
            }
          }));
        },
        updateAvatar: (avatar: PluginApiAvatarUpdate) => {
          requireCapability('profile.write');
          this.store.dispatch(UsersActions.updateCurrentUserAvatar({
            avatar: {
              ...avatar,
              avatarUpdatedAt: Date.now()
            }
          }));
        }
      },
      roles: {
        list: () => {
          requireCapability('roles.read');
          return this.currentRoom()?.roles ?? [];
        },
        setAssignments: (assignments) => {
          requireCapability('roles.manage');
          this.updateRoomAccessControl({ roleAssignments: assignments });
        }
      },
      server: {
        getCurrent: () => {
          requireCapability('server.read');
          return this.currentRoom();
        },
        registerPluginUser: (request) => {
          requireCapability('users.manage');
          const userId = request.id ?? `${pluginId}:${slug(request.displayName)}`;

          this.store.dispatch(UsersActions.userJoined({
            user: {
              avatarUrl: request.avatarUrl,
              displayName: request.displayName,
              id: userId,
              isOnline: true,
              joinedAt: Date.now(),
              oderId: userId,
              role: 'member',
              status: 'online',
              username: userId
            }
          }));

          return userId;
        },
        updateIcon: async (icon) => {
          requireCapability('server.manage');
          const room = this.currentRoom();
          const currentUser = this.currentUser();

          if (!room) {
            throw new Error('Room not found');
          }

          if (!currentUser) {
            throw new Error('Not logged in');
          }

          const isOwner = room.hostId === currentUser.id || room.hostId === currentUser.oderId;
          const isServerAdmin = currentUser.role === 'admin' || currentUser.role === 'host';
          const canByRole = resolveRoomPermission(room, currentUser, 'manageIcon');

          if (!isOwner && !isServerAdmin && !canByRole) {
            throw new Error('Permission denied');
          }

          const iconUpdatedAt = Date.now();

          await this.db.updateRoom(room.id, { icon, iconUpdatedAt });

          this.store.dispatch(RoomsActions.updateServerIconSuccess({ roomId: room.id, icon, iconUpdatedAt }));

          this.realtime.broadcastMessage({
            type: 'server-icon-update',
            roomId: room.id,
            icon,
            iconUpdatedAt
          });

          this.realtime.sendRawMessage({
            type: 'server_icon_available',
            serverId: room.id,
            iconUpdatedAt
          });

          this.serverDirectory.updateServer(room.id, {
            actingRole: isOwner ? 'host' : undefined,
            icon,
            iconUpdatedAt
          }, {
            sourceId: room.sourceId,
            sourceUrl: room.sourceUrl
          }).subscribe({
            error: () => {}
          });
        },
        updatePermissions: (permissions) => {
          requireCapability('server.manage');
          this.store.dispatch(RoomsActions.updateRoomPermissions({ roomId: this.requireRoomId(), permissions }));
        },
        updateSettings: (settings: PluginApiServerSettingsUpdate) => {
          requireCapability('server.manage');
          this.store.dispatch(RoomsActions.updateRoomSettings({
            roomId: this.requireRoomId(),
            settings: {
              description: settings.description,
              hasPassword: !!settings.password,
              isPrivate: settings.isPrivate ?? this.currentRoom()?.isPrivate ?? false,
              maxUsers: settings.maxUsers,
              name: settings.name ?? this.currentRoom()?.name ?? 'Server',
              password: settings.password,
              rules: [],
              topic: settings.topic
            }
          }));
        }
      },
      serverData: {
        read: async (key) => {
          requireCapability('storage.serverData.read');
          return await this.storage.readServerData(pluginId, key);
        },
        remove: async (key) => {
          requireCapability('storage.serverData.write');
          await this.storage.removeServerData(pluginId, key);
        },
        write: async (key, value) => {
          requireCapability('storage.serverData.write');
          await this.storage.writeServerData(pluginId, key, value);
        }
      },
      storage: {
        get: (key) => {
          requireCapability('storage.local');
          return this.storage.getLocal(pluginId, key);
        },
        remove: (key) => {
          requireCapability('storage.local');
          this.storage.removeLocal(pluginId, key);
        },
        set: (key, value) => {
          requireCapability('storage.local');
          this.storage.setLocal(pluginId, key, value);
        }
      },
      ui: {
        registerAppPage: (id, contribution) => {
          requireCapability('ui.pages');
          return this.uiRegistry.registerAppPage(pluginId, id, contribution);
        },
        registerChannelSection: (id, contribution) => {
          requireCapability('ui.channelsSection');
          return this.uiRegistry.registerChannelSection(pluginId, id, contribution);
        },
        registerComposerAction: (id, contribution) => {
          requireCapability('ui.pages');
          return this.uiRegistry.registerComposerAction(pluginId, id, contribution);
        },
        registerEmbedRenderer: (id, contribution) => {
          requireCapability('ui.embeds');
          return this.uiRegistry.registerEmbedRenderer(pluginId, id, contribution);
        },
        mountElement: (id, request) => {
          requireCapability('ui.dom');
          return this.uiRegistry.mountElement(pluginId, id, request);
        },
        registerProfileAction: (id, contribution) => {
          requireCapability('ui.pages');
          return this.uiRegistry.registerProfileAction(pluginId, id, contribution);
        },
        registerSettingsPage: (id, contribution) => {
          requireCapability('ui.settings');
          return this.uiRegistry.registerSettingsPage(pluginId, id, contribution);
        },
        registerSidePanel: (id, contribution) => {
          requireCapability('ui.sidePanel');
          return this.uiRegistry.registerSidePanel(pluginId, id, contribution);
        },
        registerToolbarAction: (id, contribution) => {
          requireCapability('ui.pages');
          return this.uiRegistry.registerToolbarAction(pluginId, id, contribution);
        }
      },
      users: {
        ban: (userId, reason) => {
          requireCapability('users.manage');
          this.store.dispatch(UsersActions.banUser({ reason, userId }));
        },
        getCurrent: () => {
          requireCapability('users.read');
          return this.currentUser() ?? null;
        },
        kick: (userId) => {
          requireCapability('users.manage');
          this.store.dispatch(UsersActions.kickUser({ userId }));
        },
        list: () => {
          requireCapability('users.read');
          return this.users();
        },
        readMembers: () => {
          requireCapability('users.read');
          return this.currentRoom()?.members ?? [];
        },
        setRole: (userId, role: User['role']) => {
          requireCapability('roles.manage');
          this.store.dispatch(UsersActions.updateUserRole({ role, userId }));
        }
      }
    });
  }

  createActionContext(source: PluginApiActionSource): PluginApiActionContext {
    const user = this.currentUser() ?? null;
    const server = this.currentRoom();
    const channels = this.currentRoomChannels();
    const activeChannelId = this.activeChannelId() ?? 'general';
    const voiceChannelId = user?.voiceState?.roomId ?? null;

    return {
      server,
      source,
      textChannel: channels.find((channel) => channel.type === 'text' && channel.id === activeChannelId) ?? null,
      user,
      voiceChannel: voiceChannelId
        ? channels.find((channel) => channel.type === 'voice' && channel.id === voiceChannelId) ?? null
        : null
    };
  }

  createSlashCommandContext(request: { args: Record<string, string>; command: string; rawArgs: string }): PluginApiSlashCommandContext {
    return {
      ...this.createActionContext('slashCommand'),
      args: request.args,
      command: request.command,
      rawArgs: request.rawArgs
    };
  }

  private assertDeclaredEvent(manifest: TojuPluginManifest, eventName: string): void {
    const declared = manifest.events?.some((event) => event.eventName === eventName) ?? false;

    if (!declared) {
      throw new Error(`Plugin ${manifest.id} did not declare event ${eventName}`);
    }
  }

  private broadcastPluginEvent(pluginId: string, eventName: string, payload: unknown, target: 'p2p' | 'server'): void {
    const roomId = this.currentRoomId() ?? 'local';
    const event: PluginEventEnvelope = {
      emittedAt: Date.now(),
      eventId: createId(),
      eventName,
      payload,
      pluginId,
      serverId: roomId,
      type: 'plugin_event'
    };

    this.voice.broadcastMessage({
      data: JSON.stringify({ event, target }),
      roomId,
      timestamp: Date.now(),
      type: 'plugin-event'
    } as unknown as ChatEvent);
  }

  private publishServerPluginEvent(pluginId: string, eventName: string, payload: unknown): void {
    this.realtime.sendRawMessage({
      type: 'plugin_event',
      eventId: createId(),
      eventName,
      payload,
      pluginId,
      serverId: this.requireRoomId()
    });
  }

  private subscribeServerPluginEvent(
    pluginId: string,
    eventName: string,
    handler: (event: PluginEventEnvelope) => void
  ) {
    const subscription = new Subscription();

    subscription.add(this.realtime.onSignalingMessage.subscribe((message) => {
      const record = message as Record<string, unknown>;

      if (record['type'] !== 'plugin_event' || record['pluginId'] !== pluginId || record['eventName'] !== eventName) {
        return;
      }

      handler(message as PluginEventEnvelope);
    }));

    this.logger.info(pluginId, `Subscribed to server event ${eventName}`);

    return {
      dispose: () => {
        subscription.unsubscribe();
        this.logger.info(pluginId, `Unsubscribed from server event ${eventName}`);
      }
    };
  }

  private receivePluginUserMessage(pluginId: string, request: PluginApiMessageAsPluginUserRequest): void {
    const roomId = this.requireRoomId();
    const timestamp = Date.now();
    const draftMessage: Message = {
      channelId: request.channelId ?? this.activeChannelId() ?? undefined,
      content: request.content,
      id: createId(),
      isDeleted: false,
      reactions: [],
      roomId,
      senderId: request.pluginUserId,
      senderName: request.pluginUserId,
      timestamp,
      revision: 0
    };

    void this.emitPluginMessageRevision(pluginId, {
      draftMessage,
      type: 'create',
      actorId: request.pluginUserId,
      editedAt: timestamp,
      pluginId,
      sign: false,
      dispatch: (message) => MessagesActions.receiveMessage({ message })
    });
  }

  private deletePluginMessage(pluginId: string, messageId: string): void {
    void this.emitPluginMessageMutation(pluginId, messageId, {
      type: 'plugin-delete',
      apply: async (existing, editedAt) => this.messageRevisions.createSignedRevision({
        message: existing,
        type: 'plugin-delete',
        actorId: this.currentUser()?.id ?? pluginId,
        editedAt,
        isDeleted: true,
        pluginId,
        sign: false
      }),
      legacyBroadcast: (editedAt) => ({
        deletedAt: editedAt,
        messageId,
        type: 'message-deleted'
      }),
      dispatch: () => MessagesActions.deleteMessageSuccess({ messageId })
    });
  }

  private editPluginMessage(pluginId: string, messageId: string, content: string): void {
    void this.emitPluginMessageMutation(pluginId, messageId, {
      type: 'plugin-edit',
      content,
      apply: async (existing, editedAt) => this.messageRevisions.createSignedRevision({
        message: existing,
        type: 'plugin-edit',
        actorId: this.currentUser()?.id ?? pluginId,
        content,
        editedAt,
        pluginId,
        sign: false
      }),
      legacyBroadcast: (editedAt) => ({
        content,
        editedAt,
        messageId,
        type: 'message-edited'
      }),
      dispatch: (message) => MessagesActions.editMessageSuccess({
        content: message.content,
        editedAt: message.editedAt ?? Date.now(),
        messageId
      })
    });
  }

  private sendPluginMessage(pluginId: string, content: string, channelId?: string): Message {
    const currentUser = this.currentUser();
    const roomId = this.requireRoomId();
    const timestamp = Date.now();
    const draftMessage: Message = {
      channelId: channelId ?? this.activeChannelId() ?? 'general',
      content,
      id: createId(),
      isDeleted: false,
      reactions: [],
      roomId,
      senderId: currentUser?.id ?? 'plugin',
      senderName: currentUser?.displayName || currentUser?.username || 'Plugin',
      timestamp,
      revision: 0
    };

    void this.emitPluginMessageRevision(pluginId, {
      draftMessage,
      type: 'create',
      actorId: currentUser?.id ?? pluginId,
      editedAt: timestamp,
      pluginId,
      sign: !!currentUser,
      dispatch: (message) => MessagesActions.sendMessageSuccess({ message })
    });

    return draftMessage;
  }

  private async emitPluginMessageRevision(
    pluginId: string,
    input: {
      draftMessage: Message;
      type: 'create';
      actorId: string;
      editedAt: number;
      pluginId: string;
      sign: boolean;
      dispatch: (message: Message) => Action;
    }
  ): Promise<void> {
    try {
      const revision = await this.messageRevisions.createSignedRevision({
        message: input.draftMessage,
        type: input.type,
        actorId: input.actorId,
        editedAt: input.editedAt,
        pluginId: input.pluginId,
        sign: input.sign
      });
      const message = materializeMessageFromRevision(null, revision);

      this.logger.info(pluginId, 'Plugin message emitted', { messageId: message.id });
      await this.db.saveMessage(message);
      await this.messageRevisions.persistRevision(revision);
      this.store.dispatch(input.dispatch(message));
      this.voice.broadcastMessage({ type: 'chat-message', message } as unknown as ChatEvent);
      this.messageRevisions.broadcastRevision(revision);
    } catch (error: unknown) {
      this.logger.warn(pluginId, 'Failed to emit plugin message revision', error);
    }
  }

  private async emitPluginMessageMutation(
    pluginId: string,
    messageId: string,
    input: {
      type: 'plugin-edit' | 'plugin-delete';
      content?: string;
      apply: (existing: Message, editedAt: number) => Promise<MessageRevision>;
      legacyBroadcast: (editedAt: number) => ChatEvent;
      dispatch: (message: Message) => Action;
    }
  ): Promise<void> {
    try {
      const existing = await this.db.getMessageById(messageId);

      if (!existing) {
        this.logger.warn(pluginId, 'Plugin message mutation target not found', { messageId });

        return;
      }

      const editedAt = Date.now();
      const revision = await input.apply(existing, editedAt);
      const message = materializeMessageFromRevision(existing, revision);

      await this.db.saveMessage(message);
      await this.messageRevisions.persistRevision(revision);
      this.store.dispatch(input.dispatch(message));
      this.voice.broadcastMessage(input.legacyBroadcast(editedAt) as unknown as ChatEvent);
      this.messageRevisions.broadcastRevision(revision);
    } catch (error: unknown) {
      this.logger.warn(pluginId, 'Failed to emit plugin message mutation revision', error);
    }
  }

  private setTyping(pluginId: string, isTyping: boolean, channelId?: string): void {
    const roomId = this.requireRoomId();

    try {
      this.realtime.sendRawMessage({
        type: 'typing',
        serverId: roomId,
        channelId: channelId ?? this.activeChannelId() ?? 'general',
        isTyping
      });
    } catch (error: unknown) {
      this.logger.warn(pluginId, 'Failed to publish typing state', error);
    }
  }

  private subscribeTyping(pluginId: string, handler: (event: PluginApiTypingEvent) => void): TojuPluginDisposable {
    const subscription = new Subscription();

    subscription.add(this.realtime.onSignalingMessage.subscribe((message) => {
      const record = message as Record<string, unknown>;

      if (record['type'] !== 'user_typing') {
        return;
      }

      const serverId = typeof record['serverId'] === 'string' ? record['serverId'] : '';
      const currentServer = this.currentRoom();

      if (!serverId || serverId !== currentServer?.id) {
        return;
      }

      const userId = typeof record['oderId'] === 'string' ? record['oderId'] : '';
      const displayName = typeof record['displayName'] === 'string' ? record['displayName'] : userId;
      const channelId = typeof record['channelId'] === 'string' && record['channelId'].trim()
        ? record['channelId'].trim()
        : 'general';
      const user = this.users().find((entry) => entry.oderId === userId || entry.id === userId) ?? null;
      const channels = this.currentRoomChannels();

      handler({
        channelId,
        displayName,
        isTyping: record['isTyping'] !== false,
        server: currentServer,
        serverId,
        textChannel: channels.find((channel) => channel.type === 'text' && channel.id === channelId) ?? null,
        user,
        userId,
        voiceChannel: user?.voiceState?.roomId
          ? channels.find((channel) => channel.type === 'voice' && channel.id === user.voiceState?.roomId) ?? null
          : null
      });
    }));

    this.logger.info(pluginId, 'Subscribed to typing events');

    return {
      dispose: () => {
        subscription.unsubscribe();
        this.logger.info(pluginId, 'Unsubscribed from typing events');
      }
    };
  }

  private persistPluginMessage(pluginId: string, message: Message): void {
    void this.db.saveMessage(message).catch((error: unknown) => {
      this.logger.warn(pluginId, 'Failed to persist plugin message', error);
    });
  }

  private async importPluginMessages(pluginId: string, messages: Message[]): Promise<void> {
    const roomId = this.requireRoomId();
    const normalizedMessages = messages
      .filter((message) => message.roomId === roomId)
      .map((message) => ({
        ...message,
        channelId: message.channelId ?? this.activeChannelId() ?? 'general',
        isDeleted: message.isDeleted === true,
        reactions: message.reactions ?? []
      }));

    if (normalizedMessages.length === 0) {
      return;
    }

    for (const message of normalizedMessages) {
      await this.db.saveMessage(message);
    }

    this.store.dispatch(MessagesActions.syncMessages({ messages: normalizedMessages }));

    // Broadcast imported history to peers in CHUNK_SIZE batches so they don't
    // depend on the inventory-limited background sync to discover bulk imports.
    for (const chunk of chunkArray(normalizedMessages, CHUNK_SIZE)) {
      this.voice.broadcastMessage({
        type: 'chat-sync-batch',
        roomId,
        messages: chunk
      } as unknown as ChatEvent);
    }

    this.logger.info(pluginId, 'Historical messages imported', { count: normalizedMessages.length });
  }

  private addPluginManagedChannel(pluginId: string, channel: Channel): void {
    const room = this.currentRoom();
    const currentUser = this.currentUser();

    if (!room || !currentUser) {
      return;
    }

    const isOwner = room.hostId === currentUser.id || room.hostId === currentUser.oderId;
    const isServerAdmin = currentUser.role === 'admin' || currentUser.role === 'host';
    const canManageChannels = resolveRoomPermission(room, currentUser, 'manageChannels');

    if (!isOwner && !isServerAdmin && !canManageChannels) {
      this.logger.warn(pluginId, 'Plugin channel creation denied by room permissions', {
        channelId: channel.id,
        roomId: room.id
      });

      return;
    }

    const existingChannels = room.channels ?? defaultChannels();
    const normalizedName = normalizeChannelName(channel.name);
    const channelExists = existingChannels.some((entry) => entry.id === channel.id) ||
      isChannelNameTaken(existingChannels, normalizedName, channel.type);

    if (!normalizedName || channelExists) {
      return;
    }

    const channels = [
      ...existingChannels,
      { ...channel,
        name: normalizedName }
    ];

    this.store.dispatch(RoomsActions.updateRoom({ roomId: room.id,
      changes: { channels } }));

    void this.db.updateRoom(room.id, { channels }).catch((error: unknown) => {
      this.logger.warn(pluginId, 'Failed to persist plugin-created channel', error);
    });

    this.realtime.broadcastMessage({
      type: 'channels-update',
      roomId: room.id,
      channels
    });

    this.serverDirectory.updateServer(room.id, {
      actingRole: isOwner ? 'host' : undefined,
      channels
    }, {
      sourceId: room.sourceId,
      sourceUrl: room.sourceUrl
    }).subscribe({
      error: () => {}
    });

    this.logger.info(pluginId, 'Plugin channel created', {
      channelId: channel.id,
      roomId: room.id
    });
  }

  private persistPluginMessageUpdate(pluginId: string, messageId: string, updates: Partial<Message>): void {
    void this.db.updateMessage(messageId, updates).catch((error: unknown) => {
      this.logger.warn(pluginId, 'Failed to persist plugin message update', error);
    });
  }

  private rememberSubscription(pluginId: string, eventName: string) {
    this.logger.info(pluginId, `Subscribed to ${eventName}`);

    return {
      dispose: () => this.logger.info(pluginId, `Unsubscribed from ${eventName}`)
    };
  }

  private requireRoomId(): string {
    const roomId = this.currentRoomId();

    if (!roomId) {
      throw new Error('No active server');
    }

    return roomId;
  }

  private updateRoomAccessControl(changes: Parameters<typeof RoomsActions.updateRoomAccessControl>[0]['changes']): void {
    this.store.dispatch(RoomsActions.updateRoomAccessControl({
      changes,
      roomId: this.requireRoomId()
    }));
  }
}

function createChannel(request: PluginApiChannelRequest, type: Channel['type']): Channel {
  return {
    id: request.id ?? slug(request.name),
    name: request.name,
    position: request.position ?? Date.now(),
    type
  };
}

function createId(): string {
  return globalThis.crypto?.randomUUID?.() ?? `plugin-${Date.now()}-${Math.random().toString(36)
    .slice(2)}`;
}

function deepFreeze<TValue extends object>(value: TValue): TValue {
  for (const propertyValue of Object.values(value)) {
    if (propertyValue && typeof propertyValue === 'object') {
      deepFreeze(propertyValue as Record<string, unknown>);
    }
  }

  return Object.freeze(value);
}

async function playAudioClip(url: string, volume = 1): Promise<void> {
  const audio = new Audio(url);

  audio.volume = Math.max(0, Math.min(1, volume));
  await audio.play();
}

function slug(value: string): string {
  return value.trim().toLowerCase()
    .replace(/[^a-z0-9.-]+/g, '-')
    .replace(/(^-+|-+$)/g, '') || createId();
}
