import { Injectable, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import type {
  ChatEvent,
  Message,
  User
} from '../../../../shared-kernel';
import { selectCurrentRoomMessages } from '../../../../store/messages/messages.selectors';
import { selectCurrentRoomId } from '../../../../store/rooms/rooms.selectors';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
import type {
  PluginApiMessageBusEnvelope,
  PluginApiMessageBusLatestRequest,
  PluginApiMessageBusPublishRequest,
  PluginApiMessageBusSubscription,
  TojuPluginDisposable
} from '../../domain/models/plugin-api.models';

const DEFAULT_LATEST_MESSAGE_LIMIT = 50;
const MAX_LATEST_MESSAGE_LIMIT = 250;
const LATEST_MESSAGES_TOPIC = 'latest-messages';

@Injectable({ providedIn: 'root' })
export class PluginMessageBusService {
  private readonly realtime = inject(RealtimeSessionFacade);
  private readonly store = inject(Store);
  private readonly currentMessages = this.store.selectSignal(selectCurrentRoomMessages);
  private readonly currentRoomId = this.store.selectSignal(selectCurrentRoomId);
  private readonly currentUser = this.store.selectSignal(selectCurrentUser);
  private readonly localSubscriptions = new Map<string, Set<PluginApiMessageBusSubscription>>();

  publish(pluginId: string, request: PluginApiMessageBusPublishRequest): PluginApiMessageBusEnvelope {
    const envelope = this.createEnvelope(pluginId, request.topic, request.payload, request, request.includeLatestMessages === true);

    this.sendEnvelope(envelope, request.targetPeerId);

    if (request.includeSelf) {
      this.dispatchLocal(envelope);
    }

    return envelope;
  }

  sendLatestMessages(pluginId: string, request: PluginApiMessageBusLatestRequest = {}): PluginApiMessageBusEnvelope {
    const envelope = this.createEnvelope(pluginId, request.topic ?? LATEST_MESSAGES_TOPIC, undefined, request, true);

    this.sendEnvelope(envelope, request.targetPeerId);

    return envelope;
  }

  subscribe(pluginId: string, subscription: PluginApiMessageBusSubscription): TojuPluginDisposable {
    const pluginSubscriptions = this.localSubscriptions.get(pluginId) ?? new Set<PluginApiMessageBusSubscription>();
    const realtimeSubscription = new Subscription();

    pluginSubscriptions.add(subscription);
    this.localSubscriptions.set(pluginId, pluginSubscriptions);

    realtimeSubscription.add(this.realtime.onMessageReceived.subscribe((event) => {
      const envelope = readPluginMessageBusEnvelope(event);

      if (!envelope || envelope.pluginId !== pluginId || !matchesSubscription(envelope, subscription)) {
        return;
      }

      subscription.handler(envelope);
    }));

    if (subscription.replayLatest) {
      subscription.handler(this.createEnvelope(
        pluginId,
        subscription.topic ?? LATEST_MESSAGES_TOPIC,
        undefined,
        {
          channelId: subscription.channelId,
          limit: subscription.latestMessageLimit
        },
        true
      ));
    }

    return {
      dispose: () => {
        realtimeSubscription.unsubscribe();
        pluginSubscriptions.delete(subscription);

        if (pluginSubscriptions.size === 0) {
          this.localSubscriptions.delete(pluginId);
        }
      }
    };
  }

  private createEnvelope(
    pluginId: string,
    topic: string,
    payload: unknown,
    request: PluginApiMessageBusLatestRequest,
    includeMessages: boolean
  ): PluginApiMessageBusEnvelope {
    const currentUser = this.currentUser() ?? null;
    const envelope: PluginApiMessageBusEnvelope = {
      eventId: createId(),
      pluginId,
      roomId: this.requireRoomId(),
      sentAt: Date.now(),
      sourceUserId: readUserId(currentUser),
      topic
    };

    if (request.channelId) {
      envelope.channelId = request.channelId;
    }

    if (payload !== undefined) {
      envelope.payload = payload;
    }

    if (includeMessages) {
      envelope.messages = this.latestMessages(request);
    }

    return envelope;
  }

  private latestMessages(request: PluginApiMessageBusLatestRequest): Message[] {
    const limit = clampLimit(request.limit);
    const sinceTimestamp = typeof request.sinceTimestamp === 'number' ? request.sinceTimestamp : null;

    return this.currentMessages()
      .filter((message) => !request.channelId || message.channelId === request.channelId)
      .filter((message) => request.includeDeleted || !message.isDeleted)
      .filter((message) => sinceTimestamp === null || message.timestamp > sinceTimestamp)
      .slice(-limit);
  }

  private sendEnvelope(envelope: PluginApiMessageBusEnvelope, targetPeerId?: string): void {
    const event: ChatEvent = {
      pluginMessage: envelope,
      roomId: envelope.roomId,
      timestamp: envelope.sentAt,
      type: 'plugin-message-bus'
    };

    if (targetPeerId) {
      this.realtime.sendToPeer(targetPeerId, event);
      return;
    }

    this.realtime.broadcastMessage(event);
  }

  private dispatchLocal(envelope: PluginApiMessageBusEnvelope): void {
    for (const subscription of this.localSubscriptions.get(envelope.pluginId) ?? []) {
      if (matchesSubscription(envelope, subscription)) {
        subscription.handler(envelope);
      }
    }
  }

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

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

    return roomId;
  }
}

function readPluginMessageBusEnvelope(event: ChatEvent): PluginApiMessageBusEnvelope | null {
  if (event.type !== 'plugin-message-bus' || !isRecord(event.pluginMessage)) {
    return null;
  }

  const envelope = event.pluginMessage;

  if (typeof envelope['eventId'] !== 'string'
    || typeof envelope['pluginId'] !== 'string'
    || typeof envelope['roomId'] !== 'string'
    || typeof envelope['sentAt'] !== 'number'
    || typeof envelope['topic'] !== 'string') {
    return null;
  }

  return {
    channelId: typeof envelope['channelId'] === 'string' ? envelope['channelId'] : undefined,
    eventId: envelope['eventId'],
    messages: Array.isArray(envelope['messages']) ? envelope['messages'].filter(isMessage) : undefined,
    payload: envelope['payload'],
    pluginId: envelope['pluginId'],
    roomId: envelope['roomId'],
    sentAt: envelope['sentAt'],
    sourcePeerId: event.fromPeerId,
    sourceUserId: typeof envelope['sourceUserId'] === 'string' ? envelope['sourceUserId'] : undefined,
    topic: envelope['topic']
  };
}

function matchesSubscription(envelope: PluginApiMessageBusEnvelope, subscription: PluginApiMessageBusSubscription): boolean {
  return (!subscription.topic || subscription.topic === envelope.topic)
    && (!subscription.channelId || subscription.channelId === envelope.channelId);
}

function clampLimit(limit: number | undefined): number {
  if (typeof limit !== 'number' || !Number.isFinite(limit)) {
    return DEFAULT_LATEST_MESSAGE_LIMIT;
  }

  return Math.max(1, Math.min(MAX_LATEST_MESSAGE_LIMIT, Math.floor(limit)));
}

function readUserId(user: User | null): string | undefined {
  return user?.oderId || user?.id || undefined;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === 'object';
}

function isMessage(value: unknown): value is Message {
  return isRecord(value)
    && typeof value['id'] === 'string'
    && typeof value['roomId'] === 'string'
    && typeof value['senderId'] === 'string'
    && typeof value['content'] === 'string'
    && typeof value['timestamp'] === 'number';
}

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