import { Injectable, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { VoiceConnectionFacade } from '../../../voice-connection/application/facades/voice-connection.facade';
import type {
  Channel,
  ChatEvent,
  Message,
  PluginCapabilityId,
  PluginEventEnvelope,
  TojuPluginManifest,
  User
} from '../../../../shared-kernel';
import { MessagesActions } from '../../../../store/messages/messages.actions';
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 type {
  PluginApiAvatarUpdate,
  PluginApiChannelRequest,
  PluginApiCustomStreamRequest,
  PluginApiMessageAsPluginUserRequest,
  PluginApiServerSettingsUpdate,
  TojuClientPluginApi
} 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';

@Injectable({ providedIn: 'root' })
export class PluginClientApiService {
  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 store = inject(Store);
  private readonly storage = inject(PluginStorageService);
  private readonly uiRegistry = inject(PluginUiRegistryService);
  private readonly voice = inject(VoiceConnectionFacade);

  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>({
      channels: {
        addAudioChannel: (request) => {
          requireCapability('channels.manage');
          this.store.dispatch(RoomsActions.addChannel({ channel: createChannel(request, 'voice') }));
        },
        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)
      },
      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);
        }
      },
      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);
        },
        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;
        },
        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 }));
        }
      }
    });
  }

  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 message: Message = {
      channelId: request.channelId ?? this.activeChannelId() ?? undefined,
      content: request.content,
      id: createId(),
      isDeleted: false,
      reactions: [],
      roomId,
      senderId: request.pluginUserId,
      senderName: request.pluginUserId,
      timestamp: Date.now()
    };

    this.logger.info(pluginId, 'Plugin user message emitted', { messageId: message.id });
    this.persistPluginMessage(pluginId, message);
    this.store.dispatch(MessagesActions.receiveMessage({ message }));
    this.voice.broadcastMessage({ type: 'chat-message', message } as unknown as ChatEvent);
  }

  private deletePluginMessage(pluginId: string, messageId: string): void {
    this.persistPluginMessageUpdate(pluginId, messageId, {
      content: '[Message deleted]',
      editedAt: Date.now(),
      isDeleted: true
    });

    this.store.dispatch(MessagesActions.deleteMessageSuccess({ messageId }));
    this.voice.broadcastMessage({
      deletedAt: Date.now(),
      messageId,
      type: 'message-deleted'
    } as unknown as ChatEvent);
  }

  private editPluginMessage(pluginId: string, messageId: string, content: string): void {
    const editedAt = Date.now();

    this.persistPluginMessageUpdate(pluginId, messageId, { content, editedAt });

    this.store.dispatch(MessagesActions.editMessageSuccess({
      content,
      editedAt,
      messageId
    }));

    this.voice.broadcastMessage({
      content,
      editedAt,
      messageId,
      type: 'message-edited'
    } as unknown as ChatEvent);
  }

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

    this.persistPluginMessage(pluginId, message);
    this.store.dispatch(MessagesActions.sendMessageSuccess({ message }));
    this.voice.broadcastMessage({ type: 'chat-message', message } as unknown as ChatEvent);

    return message;
  }

  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 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();
}
