/* eslint-disable @typescript-eslint/member-ordering */
import {
  Injectable,
  computed,
  inject,
  signal
} from '@angular/core';
import { Store } from '@ngrx/store';
import type { Message, Room } from '../../../../shared-kernel';
import { NotificationAudioService, AppSound } from '../../../../core/services/notification-audio.service';
import { AppI18nService } from '../../../../core/i18n';
import { TimeSyncService } from '../../../../core/services/time-sync.service';
import { DatabaseService } from '../../../../infrastructure/persistence';
import {
  selectActiveChannelId,
  selectCurrentRoom,
  selectSavedRooms
} from '../../../../store/rooms/rooms.selectors';
import { selectCurrentUser } from '../../../../store/users/users.selectors';
import {
  buildNotificationDisplayPayload,
  calculateUnreadForRoom,
  DEFAULT_TEXT_CHANNEL_ID,
  getRoomById,
  getRoomTextChannelIds,
  getRoomTrackingBaseline,
  isChannelMuted,
  isRoomMuted,
  isMessageVisibleInActiveView,
  resolveMessageChannelId,
  shouldDeliverNotification
} from '../../domain/logic/notification.logic';
import {
  createDefaultNotificationSettings,
  createEmptyUnreadState,
  type NotificationDeliveryContext,
  type NotificationsSettings,
  type NotificationsUnreadState
} from '../../domain/models/notification.model';
import { DesktopNotificationService } from '../../infrastructure/services/desktop-notification.service';
import { NotificationSettingsStorageService } from '../../infrastructure/services/notification-settings-storage.service';

type DesktopPlatform = 'linux' | 'mac' | 'unknown' | 'windows';

const MAX_NOTIFIED_MESSAGE_IDS = 500;

@Injectable({ providedIn: 'root' })
export class NotificationsService {
  private readonly store = inject(Store);
  private readonly db = inject(DatabaseService);
  private readonly audio = inject(NotificationAudioService);
  private readonly appI18n = inject(AppI18nService);
  private readonly timeSync = inject(TimeSyncService);
  private readonly desktopNotifications = inject(DesktopNotificationService);
  private readonly storage = inject(NotificationSettingsStorageService);

  private readonly currentRoom = this.store.selectSignal(selectCurrentRoom);
  private readonly activeChannelId = this.store.selectSignal(selectActiveChannelId);
  private readonly savedRooms = this.store.selectSignal(selectSavedRooms);
  private readonly currentUser = this.store.selectSignal(selectCurrentUser);

  private readonly _settings = signal<NotificationsSettings>(createDefaultNotificationSettings());
  private readonly _unread = signal<NotificationsUnreadState>(createEmptyUnreadState());
  private readonly _windowFocused = signal<boolean>(typeof document === 'undefined' ? true : document.hasFocus());
  private readonly _documentVisible = signal<boolean>(typeof document === 'undefined' ? true : document.visibilityState === 'visible');
  private readonly _windowMinimized = signal<boolean>(false);
  private readonly platformKind = detectPlatform();
  private readonly notifiedMessageIds = new Set<string>();
  private readonly notifiedMessageOrder: string[] = [];
  private attentionActive = false;
  private windowStateCleanup: (() => void) | null = null;
  private initialised = false;

  readonly settings = computed(() => this._settings());
  readonly unread = computed(() => this._unread());

  async initialize(): Promise<void> {
    if (this.initialised) {
      return;
    }

    this._settings.set(this.storage.load());
    this.initialised = true;
    this.registerWindowListeners();
    this.registerWindowStateListener();
    this.syncRoomCatalog(this.savedRooms());
    await this.hydrateUnreadCounts(this.savedRooms());
    this.markCurrentChannelReadIfActive();
  }

  syncRoomCatalog(rooms: Room[]): void {
    if (!this.initialised) {
      return;
    }

    const now = Date.now();
    const currentSettings = this._settings();
    const nextSettings: NotificationsSettings = {
      ...currentSettings,
      mutedRooms: {},
      mutedChannels: {},
      roomBaselines: {},
      lastReadByChannel: {}
    };

    for (const room of rooms) {
      nextSettings.mutedRooms[room.id] = currentSettings.mutedRooms[room.id] === true;
      nextSettings.roomBaselines[room.id] = currentSettings.roomBaselines[room.id] ?? now;

      const hasChannelCatalog = (room.channels?.length ?? 0) > 0;
      const textChannelIds = new Set(getRoomTextChannelIds(room));
      const mutedChannels = currentSettings.mutedChannels[room.id] ?? {};
      const lastReadByChannel = currentSettings.lastReadByChannel[room.id] ?? {};

      nextSettings.mutedChannels[room.id] = hasChannelCatalog
        ? Object.fromEntries(
          Object.entries(mutedChannels)
            .filter(([channelId, muted]) => textChannelIds.has(channelId) && muted === true)
        )
        : { ...mutedChannels };

      nextSettings.lastReadByChannel[room.id] = hasChannelCatalog
        ? Object.fromEntries(
          Object.entries(lastReadByChannel)
            .filter((entry): entry is [string, number] => textChannelIds.has(entry[0]) && typeof entry[1] === 'number')
        )
        : { ...lastReadByChannel };
    }

    this.setSettings(nextSettings);
    this.pruneUnreadState(rooms);
  }

  async hydrateUnreadCounts(rooms: Room[]): Promise<void> {
    if (!this.initialised) {
      return;
    }

    const currentUserIds = this.getCurrentUserIds();
    const roomCounts: Record<string, number> = {};
    const channelCounts: Record<string, Record<string, number>> = {};

    for (const room of rooms) {
      const trackedSince = getRoomTrackingBaseline(this._settings(), room);
      const messages = await this.db.getMessagesSince(room.id, trackedSince);
      const counts = calculateUnreadForRoom(room, messages, this._settings(), currentUserIds);

      roomCounts[room.id] = counts.roomCount;
      channelCounts[room.id] = counts.channelCounts;
    }

    this._unread.set({ roomCounts, channelCounts });
    this.pruneUnreadState(rooms);
    this.markCurrentChannelReadIfActive();
  }

  refreshRoomUnreadFromMessages(roomId: string, messages: Message[]): void {
    if (!this.initialised) {
      return;
    }

    const room = getRoomById(this.savedRooms(), roomId);

    if (!room) {
      return;
    }

    const counts = calculateUnreadForRoom(room, messages, this._settings(), this.getCurrentUserIds());

    this._unread.update((state) => ({
      roomCounts: {
        ...state.roomCounts,
        [roomId]: counts.roomCount
      },
      channelCounts: {
        ...state.channelCounts,
        [roomId]: counts.channelCounts
      }
    }));

    this.markVisibleChannelReadFromMessages(roomId, messages);
    this.syncWindowAttention();
  }

  async handleIncomingMessage(message: Message): Promise<void> {
    if (!this.initialised || this.isDuplicateMessage(message.id) || this.isOwnMessage(message.senderId)) {
      return;
    }

    const room = this.getKnownRoom(message.roomId);

    if (!room) {
      return;
    }

    this.rememberMessageId(message.id);

    const channelId = resolveMessageChannelId(message);
    const baselineTimestamp = Math.max(message.timestamp - 1, 0);

    this.ensureRoomTracking(message.roomId, channelId, baselineTimestamp);

    if (message.isDeleted) {
      return;
    }

    if (isMessageVisibleInActiveView(message, this.buildContext())) {
      this.markChannelRead(message.roomId, channelId, message.timestamp);
      return;
    }

    if (message.timestamp > this.getChannelLastReadAt(message.roomId, channelId)) {
      this.incrementUnread(message.roomId, channelId);
    }

    const context = this.buildContext();

    if (!shouldDeliverNotification(this._settings(), message, context)) {
      return;
    }

    const payload = buildNotificationDisplayPayload(
      message,
      room,
      this._settings(),
      !context.isWindowFocused || !context.isDocumentVisible,
      (key, params) => this.appI18n.instant(key, params)
    );

    if (this.shouldPlayNotificationSound()) {
      this.audio.play(AppSound.Notification);
    }

    await this.desktopNotifications.showNotification(payload);
  }

  markCurrentChannelReadIfActive(): void {
    if (!this.initialised || !this._windowFocused() || !this._documentVisible()) {
      return;
    }

    const room = this.currentRoom();

    if (!room) {
      this.syncWindowAttention();
      return;
    }

    const channelId = this.activeChannelId() || DEFAULT_TEXT_CHANNEL_ID;

    this.markChannelRead(room.id, channelId);
  }

  isRoomMuted(roomId: string): boolean {
    return isRoomMuted(this._settings(), roomId);
  }

  isChannelMuted(roomId: string, channelId: string): boolean {
    return isChannelMuted(this._settings(), roomId, channelId);
  }

  roomUnreadCount(roomId: string): number {
    return this._unread().roomCounts[roomId] ?? 0;
  }

  channelUnreadCount(roomId: string, channelId: string): number {
    return this._unread().channelCounts[roomId]?.[channelId] ?? 0;
  }

  setNotificationsEnabled(enabled: boolean): void {
    this.setSettings({
      ...this._settings(),
      enabled
    });
  }

  setShowPreview(showPreview: boolean): void {
    this.setSettings({
      ...this._settings(),
      showPreview
    });
  }

  setRespectBusyStatus(respectBusyStatus: boolean): void {
    this.setSettings({
      ...this._settings(),
      respectBusyStatus
    });
  }

  setRoomMuted(roomId: string, muted: boolean): void {
    this.setSettings({
      ...this._settings(),
      mutedRooms: {
        ...this._settings().mutedRooms,
        [roomId]: muted
      }
    });
  }

  setChannelMuted(roomId: string, channelId: string, muted: boolean): void {
    this.setSettings({
      ...this._settings(),
      mutedChannels: {
        ...this._settings().mutedChannels,
        [roomId]: {
          ...(this._settings().mutedChannels[roomId] ?? {}),
          [channelId]: muted
        }
      }
    });
  }

  private registerWindowListeners(): void {
    if (typeof window === 'undefined' || typeof document === 'undefined') {
      return;
    }

    window.addEventListener('focus', this.handleWindowFocus);
    window.addEventListener('blur', this.handleWindowBlur);
    document.addEventListener('visibilitychange', this.handleVisibilityChange);
  }

  private registerWindowStateListener(): void {
    this.windowStateCleanup = this.desktopNotifications.onWindowStateChanged((state) => {
      this._windowFocused.set(state.isFocused);
      this._windowMinimized.set(state.isMinimized);

      if (state.isFocused && !state.isMinimized && this._documentVisible()) {
        this.markCurrentChannelReadIfActive();
        return;
      }

      this.syncWindowAttention();
    });
  }

  private readonly handleWindowFocus = (): void => {
    this._windowFocused.set(true);
    this._windowMinimized.set(false);
    this.markCurrentChannelReadIfActive();
  };

  private readonly handleWindowBlur = (): void => {
    this._windowFocused.set(false);
    this.syncWindowAttention();
  };

  private readonly handleVisibilityChange = (): void => {
    const isVisible = document.visibilityState === 'visible';

    this._documentVisible.set(isVisible);

    if (isVisible && this._windowFocused()) {
      this.markCurrentChannelReadIfActive();
      return;
    }

    this.syncWindowAttention();
  };

  private buildContext(): NotificationDeliveryContext {
    return {
      activeChannelId: this.activeChannelId(),
      currentRoomId: this.currentRoom()?.id ?? null,
      currentUser: this.currentUser() ?? null,
      isDocumentVisible: this._documentVisible(),
      isWindowFocused: this._windowFocused(),
      rooms: this.savedRooms()
    };
  }

  private shouldPlayNotificationSound(): boolean {
    return this.platformKind === 'windows' && !this.isWindowActive();
  }

  private isWindowActive(): boolean {
    return this._windowFocused() && this._documentVisible() && !this._windowMinimized();
  }

  private ensureRoomTracking(roomId: string, channelId: string, baselineTimestamp: number): void {
    const settings = this._settings();

    if (settings.roomBaselines[roomId] !== undefined) {
      return;
    }

    this.setSettings({
      ...settings,
      roomBaselines: {
        ...settings.roomBaselines,
        [roomId]: baselineTimestamp
      },
      mutedChannels: {
        ...settings.mutedChannels,
        [roomId]: {
          ...(settings.mutedChannels[roomId] ?? {})
        }
      },
      lastReadByChannel: {
        ...settings.lastReadByChannel,
        [roomId]: {
          ...(settings.lastReadByChannel[roomId] ?? {})
        }
      }
    });

    this._unread.update((state) => ({
      roomCounts: {
        ...state.roomCounts,
        [roomId]: state.roomCounts[roomId] ?? 0
      },
      channelCounts: {
        ...state.channelCounts,
        [roomId]: {
          ...(state.channelCounts[roomId] ?? {}),
          [channelId]: state.channelCounts[roomId]?.[channelId] ?? 0
        }
      }
    }));

    this.syncWindowAttention();
  }

  private pruneUnreadState(rooms: Room[]): void {
    const nextRoomCounts: Record<string, number> = {};
    const nextChannelCounts: Record<string, Record<string, number>> = {};
    const currentUnread = this._unread();

    for (const room of rooms) {
      const validChannelIds = new Set(getRoomTextChannelIds(room));
      const currentChannels = currentUnread.channelCounts[room.id] ?? {};
      const roomChannelCounts = Object.fromEntries(
        Object.entries(currentChannels)
          .filter(([channelId]) => validChannelIds.has(channelId))
      ) as Record<string, number>;

      for (const channelId of validChannelIds) {
        roomChannelCounts[channelId] = roomChannelCounts[channelId] ?? 0;
      }

      nextChannelCounts[room.id] = roomChannelCounts;
      nextRoomCounts[room.id] = Object.values(roomChannelCounts).reduce((total, count) => total + count, 0);
    }

    this._unread.set({
      roomCounts: nextRoomCounts,
      channelCounts: nextChannelCounts
    });

    this.syncWindowAttention();
  }

  private incrementUnread(roomId: string, channelId: string): void {
    this._unread.update((state) => {
      const roomChannelCounts = {
        ...(state.channelCounts[roomId] ?? {}),
        [channelId]: (state.channelCounts[roomId]?.[channelId] ?? 0) + 1
      };

      return {
        roomCounts: {
          ...state.roomCounts,
          [roomId]: Object.values(roomChannelCounts).reduce((total, count) => total + count, 0)
        },
        channelCounts: {
          ...state.channelCounts,
          [roomId]: roomChannelCounts
        }
      };
    });

    this.syncWindowAttention();
  }

  private markChannelRead(roomId: string, channelId: string, timestamp = this.timeSync.now()): void {
    const nextReadAt = Math.max(timestamp, this.timeSync.now(), this.getChannelLastReadAt(roomId, channelId));

    this.setSettings({
      ...this._settings(),
      lastReadByChannel: {
        ...this._settings().lastReadByChannel,
        [roomId]: {
          ...(this._settings().lastReadByChannel[roomId] ?? {}),
          [channelId]: nextReadAt
        }
      }
    });

    this._unread.update((state) => {
      const roomChannelCounts = {
        ...(state.channelCounts[roomId] ?? {}),
        [channelId]: 0
      };

      return {
        roomCounts: {
          ...state.roomCounts,
          [roomId]: Object.values(roomChannelCounts).reduce((total, count) => total + count, 0)
        },
        channelCounts: {
          ...state.channelCounts,
          [roomId]: roomChannelCounts
        }
      };
    });

    this.syncWindowAttention();
  }

  private markVisibleChannelReadFromMessages(roomId: string, messages: Message[]): void {
    if (!this._windowFocused() || !this._documentVisible() || this.currentRoom()?.id !== roomId) {
      return;
    }

    const channelId = this.activeChannelId() || DEFAULT_TEXT_CHANNEL_ID;
    const latestVisibleMessageTimestamp = messages.reduce((latestTimestamp, message) => {
      if (resolveMessageChannelId(message) !== channelId) {
        return latestTimestamp;
      }

      return Math.max(latestTimestamp, message.timestamp);
    }, 0);

    this.markChannelRead(roomId, channelId, latestVisibleMessageTimestamp);
  }

  private getChannelLastReadAt(roomId: string, channelId: string): number {
    return this._settings().lastReadByChannel[roomId]?.[channelId]
      ?? this._settings().roomBaselines[roomId]
      ?? 0;
  }

  private getCurrentUserIds(): Set<string> {
    const ids = new Set<string>();
    const user = this.currentUser();

    if (user?.id) {
      ids.add(user.id);
    }

    if (user?.oderId) {
      ids.add(user.oderId);
    }

    return ids;
  }

  private isOwnMessage(senderId: string): boolean {
    return this.getCurrentUserIds().has(senderId);
  }

  private getKnownRoom(roomId: string): Room | null {
    return getRoomById(this.savedRooms(), roomId)
      ?? (this.currentRoom()?.id === roomId ? this.currentRoom() ?? null : null);
  }

  private setSettings(settings: NotificationsSettings): void {
    this._settings.set(settings);
    this.storage.save(settings);
    this.syncWindowAttention();
  }

  private hasActionableUnread(): boolean {
    const unread = this._unread();
    const settings = this._settings();

    for (const [roomId, roomCount] of Object.entries(unread.roomCounts)) {
      if (roomCount <= 0 || isRoomMuted(settings, roomId)) {
        continue;
      }

      const channelCounts = unread.channelCounts[roomId] ?? {};

      for (const [channelId, count] of Object.entries(channelCounts)) {
        if (count > 0 && !isChannelMuted(settings, roomId, channelId)) {
          return true;
        }
      }
    }

    return false;
  }

  private syncWindowAttention(): void {
    if (!this.initialised) {
      return;
    }

    const shouldAttention = this._settings().enabled
      && this.hasActionableUnread()
      && (this._windowMinimized() || !this._windowFocused() || !this._documentVisible());

    if (shouldAttention === this.attentionActive) {
      return;
    }

    this.attentionActive = shouldAttention;

    if (shouldAttention) {
      void this.desktopNotifications.requestAttention();
      return;
    }

    void this.desktopNotifications.clearAttention();
  }

  private isDuplicateMessage(messageId: string): boolean {
    return this.notifiedMessageIds.has(messageId);
  }

  private rememberMessageId(messageId: string): void {
    if (this.notifiedMessageIds.has(messageId)) {
      return;
    }

    this.notifiedMessageIds.add(messageId);
    this.notifiedMessageOrder.push(messageId);

    if (this.notifiedMessageOrder.length > MAX_NOTIFIED_MESSAGE_IDS) {
      const removedId = this.notifiedMessageOrder.shift();

      if (removedId) {
        this.notifiedMessageIds.delete(removedId);
      }
    }
  }
}

function detectPlatform(): DesktopPlatform {
  if (typeof navigator === 'undefined') {
    return 'unknown';
  }

  const platformInfo = `${navigator.userAgent} ${navigator.platform}`.toLowerCase();

  if (platformInfo.includes('win')) {
    return 'windows';
  }

  if (platformInfo.includes('linux')) {
    return 'linux';
  }

  if (platformInfo.includes('mac')) {
    return 'mac';
  }

  return 'unknown';
}
