import {computed, inject, Injectable, OnDestroy, signal} from '@angular/core';
import {AuthService} from '@services/auth.service';
import {EventSourcePolyfill} from 'event-source-polyfill';
import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object';
import {RawChatMessage} from '@pages/altitude/chat/objects/raw-chat-message.object';
import {normalizeComponent} from '@pages/altitude/chat/mini-message/normalize.util';
import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
import {NotificationService} from '@pages/altitude/chat/service/chat-notification.service';
import {ChatInfoService} from '@api';

interface SsePayloadEvent {
  data: string;
}

@Injectable({providedIn: 'root'})
export class ChatService implements OnDestroy {
  private eventSource?: EventSourcePolyfill;
  private readonly notificationService: NotificationService = inject(NotificationService);
  private readonly authService: AuthService = inject(AuthService)
  private readonly chatInfoService: ChatInfoService = inject(ChatInfoService);
  private readonly _messages = signal<ChatMessage[]>([])
  private readonly _channels = signal<ChatChannel[]>([])
  public readonly channels = computed(() => this._channels().sort((a, b) => a.name.localeCompare(b.name)));
  private readonly _selectedChannel = signal<ChatChannel | null>(null);
  public readonly selectedChannel = this._selectedChannel.asReadonly()
  public readonly partieMap = signal<Map<string, string>>(new Map());
  public readonly userNameMap = signal<Map<string, string>>(new Map());
  private readonly pendingPartyNameRequests = new Set<string>();
  private readonly pendingUserNameRequests = new Set<string>();

  public readonly messages = computed(() => {
    const selected = this._selectedChannel();
    if (!selected) return [];
    return this._messages()
      .filter((message) => message.channelName === selected.name && message.channelType === selected.type)
      .sort((a, b) => b.timestamp - a.timestamp);
  });

  connect() {
    if (this.eventSource) {
      return; // already connected
    }

    const jwt = this.authService.getJwt();
    if (!jwt) {
      //TODO [Stijn] [2026-07-18]: Error when no JWT available (log in?)
      return;
    }

    const source = new EventSourcePolyfill('/api/chat/read/subscribe', {
      headers: {
        Authorization: `Bearer ${jwt}`
      },
      heartbeatTimeout: 60000
    });

    this.eventSource = source;

    this.on(source, 'connected', (event) => {
      this.processChatMessages(event);
    });

    this.on(source, 'chat', (event) => {
      const chatMessages = this.processChatMessages(event);
      chatMessages.forEach((message) => {
        const selected = this._selectedChannel();
        const isSelected = selected && selected.name === message.channelName && selected.type === message.channelType;

        const find = this._channels().find(channel => message.channelName === channel.name && message.channelType === channel.type && !isSelected);
        if (find) {
          find.unreadMessages++;
        }
      })
      new Set(chatMessages.map(message => `${message.channelType}:${message.channelName}`))
        .forEach((channelKeyStr) => {
          this.notificationService.notify(channelKeyStr);
        });
    });

    source.onerror = (err) => {
      console.error('SSE error, polyfill will auto-reconnect:', err);
    };
  }

  private processChatMessages(event: SsePayloadEvent): ChatMessage[] {
    const raw = JSON.parse(event.data) as RawChatMessage[];
    const messages: ChatMessage[] = raw.map((m) => {
      const message: ChatMessage = {
        ...m,
        messageJson: normalizeComponent(JSON.parse(m.messageJson)),
      };
      const channelKey = this.getChannelKey(message);
      if (channelKey === null) {
        return null;
      }
      message.channelName = channelKey.name;
      message.channelType = channelKey.type;
      return message;
    }).filter((message) => message !== null) as ChatMessage[];
    this._messages.update((old) => [...old, ...messages]);

    messages.forEach((message) => {
      const found = this._channels().find(existing => existing.name === message.channelName && existing.type === message.channelType);
      if (!found) {
        this._channels.update((old) => [...old, {
          name: message.channelName!,
          type: message.channelType!,
          unreadMessages: 0
        }]);
      }
    })

    return messages;
  }

  private getChannelKey(message: ChatMessage): { name: string, type: ChatChannel['type'] } | null {
    if (message.type === 'GAC') {
      return {name: 'Global Admin Chat', type: 'GAC'};
    }
    if (message.type === 'PARTY') {
      if (!message.channel) {
        throw new Error('Party name is missing');
      }
      this.updatePartyMap(message.channel)
      return {name: message.channel, type: 'PARTY'};
    }
    if (message.type === 'MSG') {
      const uuid = this.authService.getUuid();
      if (message.receiver === uuid) {
        this.updateUserNameMap(message.uuid)
        return {name: message.uuid, type: 'DM'};
      }
      this.updateUserNameMap(message.receiver)
      return {name: message.receiver, type: 'SPY'};
    }
    return {name: message.server, type: 'SERVER'};
  }

  public updatePartyMap(partyId: string): void {
    if (this.partieMap().has(partyId) || this.pendingPartyNameRequests.has(partyId)) {
      return;
    }

    this.pendingPartyNameRequests.add(partyId);

    this.chatInfoService.getPartyName(partyId).subscribe({
      next: (partyName) => {
        this.partieMap.update((old) => {
          const newMap = new Map(old);
          newMap.set(partyId, partyName.name);
          return newMap;
        });
      },
      error: (error) => {
        console.error(`Failed to load party name for ${partyId}:`, error);
      },
      complete: () => {
        this.pendingPartyNameRequests.delete(partyId);
      }
    });
  }

  public updateUserNameMap(uuid: string): void {
    if (this.userNameMap().has(uuid) || this.pendingUserNameRequests.has(uuid)) {
      return;
    }

    this.pendingUserNameRequests.add(uuid);

    this.chatInfoService.getName(uuid).subscribe({
      next: (userName) => {
        this.userNameMap.update((old) => {
          const newMap = new Map(old);
          newMap.set(uuid, userName.nickname ?? userName.name);
          return newMap;
        });
      },
      error: (error) => {
        console.error(`Failed to load username for ${uuid}:`, error);
      },
      complete: () => {
        this.pendingUserNameRequests.delete(uuid);
      }
    });
  }

  public selectChannel(channel: ChatChannel) {
    this._selectedChannel.set(channel);
    this._channels.update((old) =>
      old.map((c) =>
        c.name === channel.name && c.type === channel.type
          ? {...c, unreadMessages: 0}
          : c
      )
    );
  }

// single, deliberate escape hatch from the broken DOM/polyfill type overlap —
  // everything else in this file stays fully typed
  private on(source: EventSourcePolyfill, eventName: string, handler: (event: SsePayloadEvent) => void): void {
    (source as unknown as { addEventListener: (type: string, listener: (event: SsePayloadEvent) => void) => void })
      .addEventListener(eventName, handler);
  }

  disconnect(): void {
    this.eventSource?.close();
    this.eventSource = undefined;
  }

  ngOnDestroy(): void {
    this.disconnect();
  }
}
