import {
  Injectable,
  effect,
  inject
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { yieldToAttachmentHydrationLoop } from '../../domain/logic/attachment-blob.rules';
import {
  getWatchedAttachmentRoomIdFromUrl,
  isDirectMessageAttachmentRoomId,
  shouldAutoRequestWhenWatched
} from '../../domain/logic/attachment.logic';
import type { Attachment, AttachmentMeta } from '../../domain/models/attachment.model';
import type {
  FileAnnouncePayload,
  FileCancelPayload,
  FileChunkPayload,
  FileNotFoundPayload,
  FileRequestPayload
} from '../../domain/models/attachment-transfer.model';
import { AttachmentPersistenceService } from './attachment-persistence.service';
import { AttachmentRuntimeStore } from './attachment-runtime.store';
import { AttachmentTransferService } from './attachment-transfer.service';

@Injectable({ providedIn: 'root' })
export class AttachmentManagerService {
  get updated() {
    return this.runtimeStore.updated;
  }

  private readonly webrtc = inject(RealtimeSessionFacade);
  private readonly router = inject(Router);
  private readonly database = inject(DatabaseService);
  private readonly runtimeStore = inject(AttachmentRuntimeStore);
  private readonly persistence = inject(AttachmentPersistenceService);
  private readonly transfer = inject(AttachmentTransferService);

  private watchedRoomId: string | null = this.extractWatchedRoomId(this.router.url);
  private isDatabaseInitialised = false;
  private autoDownloadRequestsByRoom = new Map<string, Promise<void>>();

  constructor() {
    effect(() => {
      if (this.database.isReady() && !this.isDatabaseInitialised) {
        this.isDatabaseInitialised = true;
        void this.persistence.initFromDatabase().then(() => {
          if (this.watchedRoomId) {
            void this.restoreLocalAttachmentsForRoom(this.watchedRoomId);
          }
        });
      }
    });

    this.router.events.subscribe((event) => {
      if (!(event instanceof NavigationEnd)) {
        return;
      }

      this.watchedRoomId = this.extractWatchedRoomId(event.urlAfterRedirects || event.url);

      if (this.watchedRoomId) {
        void this.restoreLocalAttachmentsForRoom(this.watchedRoomId);
        void this.requestAutoDownloadsForRoom(this.watchedRoomId);
      }
    });

    this.webrtc.onPeerConnected.subscribe(() => {
      if (this.watchedRoomId) {
        void this.restoreLocalAttachmentsForRoom(this.watchedRoomId);
        void this.requestAutoDownloadsForRoom(this.watchedRoomId);
      }
    });
  }

  getForMessage(messageId: string): Attachment[] {
    return this.runtimeStore.getAttachmentsForMessage(messageId);
  }

  rememberMessageRoom(messageId: string, roomId: string): void {
    if (!messageId || !roomId)
      return;

    this.runtimeStore.rememberMessageRoom(messageId, roomId);
  }

  queueAutoDownloadsForMessage(messageId: string, attachmentId?: string): void {
    void this.requestAutoDownloadsForMessage(messageId, attachmentId);
  }

  async requestAutoDownloadsForRoom(roomId: string): Promise<void> {
    if (!roomId || !this.isRoomWatched(roomId) || this.webrtc.getConnectedPeers().length === 0)
      return;

    const activeRequest = this.autoDownloadRequestsByRoom.get(roomId);

    if (activeRequest) {
      return activeRequest;
    }

    const request = this.runAutoDownloadsForRoom(roomId).finally(() => {
      if (this.autoDownloadRequestsByRoom.get(roomId) === request) {
        this.autoDownloadRequestsByRoom.delete(roomId);
      }
    });

    this.autoDownloadRequestsByRoom.set(roomId, request);
    return request;
  }

  async deleteForMessage(messageId: string): Promise<void> {
    await this.persistence.deleteForMessage(messageId);
  }

  getAttachmentMetasForMessages(messageIds: string[]): Record<string, AttachmentMeta[]> {
    return this.transfer.getAttachmentMetasForMessages(messageIds);
  }

  async registerSyncedAttachments(
    attachmentMap: Record<string, AttachmentMeta[]>,
    messageRoomIds?: Record<string, string>
  ): Promise<void> {
    await this.transfer.registerSyncedAttachments(attachmentMap, messageRoomIds);

    for (const [messageId, attachments] of Object.entries(attachmentMap)) {
      for (const attachment of attachments) {
        this.queueAutoDownloadsForMessage(messageId, attachment.id);
      }
    }
  }

  requestFromAnyPeer(messageId: string, attachment: Attachment): Promise<void> {
    return this.transfer.requestFromAnyPeer(messageId, attachment);
  }

  handleFileNotFound(payload: FileNotFoundPayload): void {
    this.transfer.handleFileNotFound(payload);
  }

  requestImageFromAnyPeer(messageId: string, attachment: Attachment): Promise<void> {
    return this.transfer.requestImageFromAnyPeer(messageId, attachment);
  }

  async tryRestoreAttachmentFromLocal(attachment: Attachment): Promise<boolean> {
    const restored = await this.persistence.tryRestoreAttachmentFromLocal(attachment);

    if (restored) {
      this.runtimeStore.touch();
    }

    return restored;
  }

  requestFile(messageId: string, attachment: Attachment): Promise<void> {
    return this.transfer.requestFile(messageId, attachment);
  }

  async publishAttachments(
    messageId: string,
    files: File[],
    uploaderPeerId?: string
  ): Promise<void> {
    await this.transfer.publishAttachments(messageId, files, uploaderPeerId);
  }

  handleFileAnnounce(payload: FileAnnouncePayload): void {
    this.transfer.handleFileAnnounce(payload);

    if (payload.messageId && payload.file?.id) {
      this.queueAutoDownloadsForMessage(payload.messageId, payload.file.id);
    }
  }

  handleFileChunk(payload: FileChunkPayload): void {
    this.transfer.handleFileChunk(payload);
  }

  async handleFileRequest(payload: FileRequestPayload): Promise<void> {
    await this.transfer.handleFileRequest(payload);
  }

  cancelRequest(messageId: string, attachment: Attachment): void {
    this.transfer.cancelRequest(messageId, attachment);
  }

  handleFileCancel(payload: FileCancelPayload): void {
    this.transfer.handleFileCancel(payload);
  }

  async fulfillRequestWithFile(
    messageId: string,
    fileId: string,
    targetPeerId: string,
    file: File
  ): Promise<void> {
    await this.transfer.fulfillRequestWithFile(messageId, fileId, targetPeerId, file);
  }

  private async restoreLocalAttachmentsForRoom(roomId: string): Promise<void> {
    if (!this.isRoomWatched(roomId)) {
      return;
    }

    await this.persistence.whenReady();

    const messageIds = await this.collectMessageIdsForRoom(roomId);

    let hasChanges = false;

    for (const messageId of messageIds) {
      for (const attachment of this.runtimeStore.getAttachmentsForMessage(messageId)) {
        if (await this.persistence.tryRestoreAttachmentFromLocal(attachment)) {
          hasChanges = true;
          await yieldToAttachmentHydrationLoop();
        }
      }
    }

    if (hasChanges) {
      this.runtimeStore.touch();
    }
  }

  private async collectMessageIdsForRoom(roomId: string): Promise<string[]> {
    if (isDirectMessageAttachmentRoomId(roomId)) {
      const messageIds: string[] = [];

      for (const [messageId] of this.runtimeStore.getAttachmentEntries()) {
        const attachmentRoomId = await this.persistence.resolveMessageRoomId(messageId);

        if (attachmentRoomId === roomId) {
          messageIds.push(messageId);
        }
      }

      return messageIds;
    }

    if (!this.database.isReady()) {
      return Array.from(this.runtimeStore.getAttachmentEntries())
        .filter(([messageId]) => this.runtimeStore.getMessageRoomId(messageId) === roomId)
        .map(([messageId]) => messageId);
    }

    const messages = await this.database.getMessages(roomId, 500, 0);

    for (const message of messages) {
      this.runtimeStore.rememberMessageRoom(message.id, message.roomId);
    }

    return messages.map((message) => message.id);
  }

  private async runAutoDownloadsForRoom(roomId: string): Promise<void> {
    if (!this.isRoomWatched(roomId)) {
      return;
    }

    await this.restoreLocalAttachmentsForRoom(roomId);

    if (isDirectMessageAttachmentRoomId(roomId)) {
      await this.requestAutoDownloadsForRuntimeRoom(roomId);
      return;
    }

    if (this.database.isReady()) {
      const messages = await this.database.getMessages(roomId, 500, 0);

      for (const message of messages) {
        this.runtimeStore.rememberMessageRoom(message.id, message.roomId);
        await this.requestAutoDownloadsForMessage(message.id);
      }

      return;
    }

    await this.requestAutoDownloadsForRuntimeRoom(roomId);
  }

  private async requestAutoDownloadsForRuntimeRoom(roomId: string): Promise<void> {
    for (const [messageId] of this.runtimeStore.getAttachmentEntries()) {
      const attachmentRoomId = await this.persistence.resolveMessageRoomId(messageId);

      if (attachmentRoomId === roomId) {
        await this.requestAutoDownloadsForMessage(messageId);
      }
    }
  }

  private async requestAutoDownloadsForMessage(messageId: string, attachmentId?: string): Promise<void> {
    if (!messageId)
      return;

    const roomId = await this.persistence.resolveMessageRoomId(messageId);

    if (!roomId || !this.isRoomWatched(roomId) || this.webrtc.getConnectedPeers().length === 0) {
      return;
    }

    const attachments = this.runtimeStore.getAttachmentsForMessage(messageId);

    for (const attachment of attachments) {
      if (attachmentId && attachment.id !== attachmentId)
        continue;

      if (!shouldAutoRequestWhenWatched(attachment))
        continue;

      if (attachment.available)
        continue;

      if ((attachment.receivedBytes ?? 0) > 0)
        continue;

      if (this.transfer.hasPendingRequest(messageId, attachment.id))
        continue;

      void this.transfer.requestFromAnyPeer(messageId, attachment);
    }
  }

  private extractWatchedRoomId(url: string): string | null {
    return getWatchedAttachmentRoomIdFromUrl(url);
  }

  private isRoomWatched(roomId: string | null | undefined): boolean {
    return !!roomId && roomId === this.watchedRoomId;
  }
}
