import { Injectable, inject } from '@angular/core';
import { take } from 'rxjs';
import { Store } from '@ngrx/store';
import { selectCurrentRoomName } from '../../../../store/rooms/rooms.selectors';
import { DatabaseService } from '../../../../infrastructure/persistence';
import { AttachmentStorageService } from '../../infrastructure/services/attachment-storage.service';
import type { Attachment, AttachmentMeta } from '../../domain/models/attachment.model';
import { LEGACY_ATTACHMENTS_STORAGE_KEY } from '../../domain/constants/attachment-transfer.constants';
import {
  ATTACHMENT_BLOB_READ_CHUNK_SIZE_BYTES,
  decodeBase64ToUint8Array,
  yieldToAttachmentHydrationLoop
} from '../../domain/logic/attachment-blob.rules';
import { isBlobObjectUrl, needsBlobObjectUrlForInlineDisplay } from '../../domain/logic/attachment-display-url.rules';
import { mergeAttachmentLocalPaths } from '../../domain/logic/attachment-persistence.rules';
import { AttachmentRuntimeStore } from './attachment-runtime.store';

@Injectable({ providedIn: 'root' })
export class AttachmentPersistenceService {
  private initPromise: Promise<void> | null = null;

  private readonly runtimeStore = inject(AttachmentRuntimeStore);
  private readonly ngrxStore = inject(Store);
  private readonly attachmentStorage = inject(AttachmentStorageService);
  private readonly database = inject(DatabaseService);

  async deleteForMessage(messageId: string): Promise<void> {
    const attachments = this.runtimeStore.getAttachmentsForMessage(messageId);
    const hadCachedAttachments = attachments.length > 0 || this.runtimeStore.hasAttachmentsForMessage(messageId);
    const retainedSavedPaths = await this.getRetainedSavedPathsForOtherMessages(messageId);
    const savedPathsToDelete = new Set<string>();

    for (const attachment of attachments) {
      if (attachment.objectUrl) {
        try {
          URL.revokeObjectURL(attachment.objectUrl);
        } catch { /* ignore */ }
      }

      if (attachment.savedPath && !retainedSavedPaths.has(attachment.savedPath)) {
        savedPathsToDelete.add(attachment.savedPath);
      }
    }

    this.runtimeStore.deleteAttachmentsForMessage(messageId);
    this.runtimeStore.deleteMessageRoom(messageId);
    this.runtimeStore.clearMessageScopedState(messageId);

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

    if (this.database.isReady()) {
      await this.database.deleteAttachmentsForMessage(messageId);
    }

    for (const diskPath of savedPathsToDelete) {
      await this.attachmentStorage.deleteFile(diskPath);
    }
  }

  whenReady(): Promise<void> {
    if (this.database.isReady()) {
      return this.initFromDatabase();
    }

    return this.initPromise ?? Promise.resolve();
  }

  async persistAttachmentMeta(attachment: Attachment): Promise<void> {
    if (!this.database.isReady())
      return;

    try {
      const storedRecords = await this.database.getAttachmentsForMessage(attachment.messageId);
      const storedRecord = storedRecords.find((record) => record.id === attachment.id);
      const localPaths = mergeAttachmentLocalPaths(attachment, storedRecord);

      attachment.filePath = localPaths.filePath ?? undefined;
      attachment.savedPath = localPaths.savedPath ?? undefined;

      await this.database.saveAttachment({
        id: attachment.id,
        messageId: attachment.messageId,
        filename: attachment.filename,
        size: attachment.size,
        mime: attachment.mime,
        isImage: attachment.isImage,
        uploaderPeerId: attachment.uploaderPeerId,
        filePath: localPaths.filePath ?? undefined,
        savedPath: localPaths.savedPath ?? undefined
      });
    } catch { /* persistence is best-effort */ }
  }

  async saveFileToDisk(attachment: Attachment, blob: Blob): Promise<string | null> {
    try {
      const storageContainer = await this.resolveStorageContainerName(attachment);
      const diskPath = await this.attachmentStorage.saveBlob(attachment, blob, storageContainer);

      if (!diskPath)
        return null;

      attachment.savedPath = diskPath;
      void this.persistAttachmentMeta(attachment);
      return diskPath;
    } catch { /* disk save is best-effort */ }

    return null;
  }

  async initFromDatabase(): Promise<void> {
    if (!this.initPromise) {
      this.initPromise = this.runInitFromDatabase();
    }

    return this.initPromise;
  }

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

    if (restored) {
      attachment.requestError = undefined;
    }

    return restored;
  }

  async ensureInlineDisplayObjectUrl(attachment: Attachment): Promise<boolean> {
    if (!needsBlobObjectUrlForInlineDisplay(attachment.objectUrl)) {
      return true;
    }

    let diskPath = await this.attachmentStorage.resolveExistingPath(attachment);

    if (!diskPath) {
      const roomName = await this.resolveStorageContainerName(attachment);

      diskPath = await this.attachmentStorage.resolveCanonicalStoredPath(attachment, roomName);

      if (diskPath) {
        attachment.savedPath = diskPath;
        void this.persistAttachmentMeta(attachment);
      }
    }

    if (!diskPath) {
      return false;
    }

    this.revokeAttachmentObjectUrl(attachment);

    const restored = await this.restoreAttachmentBlobFromDiskPath(attachment, diskPath);

    return restored;
  }

  async persistUploadCopyFromSourcePath(attachment: Attachment, sourcePath: string): Promise<string | null> {
    try {
      const storageContainer = await this.resolveStorageContainerName(attachment);
      const diskPath = await this.attachmentStorage.createWritableFile(attachment, storageContainer);

      if (!diskPath) {
        return null;
      }

      const copied = await this.attachmentStorage.copyFile(sourcePath, diskPath);

      if (!copied) {
        await this.attachmentStorage.deleteFile(diskPath);
        return null;
      }

      attachment.savedPath = diskPath;
      await this.persistAttachmentMeta(attachment);
      return diskPath;
    } catch {
      return null;
    }
  }

  async resolveMessageRoomId(messageId: string): Promise<string | null> {
    const cachedRoomId = this.runtimeStore.getMessageRoomId(messageId);

    if (cachedRoomId)
      return cachedRoomId;

    if (!this.database.isReady())
      return null;

    try {
      const message = await this.database.getMessageById(messageId);

      if (!message?.roomId)
        return null;

      this.runtimeStore.rememberMessageRoom(messageId, message.roomId);
      return message.roomId;
    } catch {
      return null;
    }
  }

  async resolveCurrentRoomName(): Promise<string> {
    return new Promise<string>((resolve) => {
      this.ngrxStore
        .select(selectCurrentRoomName)
        .pipe(take(1))
        .subscribe((name) => resolve(name || ''));
    });
  }

  async resolveStorageContainerName(attachment: Pick<Attachment, 'messageId'>): Promise<string> {
    return this.runtimeStore.getMessageRoomId(attachment.messageId) ?? await this.resolveCurrentRoomName();
  }

  private async loadFromDatabase(): Promise<void> {
    try {
      const allRecords: AttachmentMeta[] = await this.database.getAllAttachments();
      const grouped = new Map<string, Attachment[]>();

      for (const record of allRecords) {
        const attachment: Attachment = { ...record,
          available: false };
        const bucket = grouped.get(record.messageId) ?? [];

        bucket.push(attachment);
        grouped.set(record.messageId, bucket);
      }

      this.runtimeStore.replaceAttachments(grouped);
      this.runtimeStore.touch();
    } catch { /* load is best-effort */ }
  }

  private async migrateFromLocalStorage(): Promise<void> {
    try {
      const raw = localStorage.getItem(LEGACY_ATTACHMENTS_STORAGE_KEY);

      if (!raw)
        return;

      const legacyRecords: AttachmentMeta[] = JSON.parse(raw);

      for (const meta of legacyRecords) {
        const existing = [...this.runtimeStore.getAttachmentsForMessage(meta.messageId)];

        if (!existing.find((entry) => entry.id === meta.id)) {
          const attachment: Attachment = { ...meta,
            available: false };

          existing.push(attachment);
          this.runtimeStore.setAttachmentsForMessage(meta.messageId, existing);
          void this.persistAttachmentMeta(attachment);
        }
      }

      localStorage.removeItem(LEGACY_ATTACHMENTS_STORAGE_KEY);
      this.runtimeStore.touch();
    } catch { /* migration is best-effort */ }
  }

  private async runInitFromDatabase(): Promise<void> {
    await this.loadFromDatabase();
    await this.migrateFromLocalStorage();
  }

  private async restoreAttachmentBlobFromDiskPath(attachment: Attachment, diskPath: string): Promise<boolean> {
    if (this.attachmentStorage.canReadFileChunks()) {
      const fileSize = await this.attachmentStorage.getFileSize(diskPath);

      if (!fileSize || fileSize < 1) {
        return false;
      }

      const blobParts: Uint8Array[] = [];

      for (let start = 0; start < fileSize; start += ATTACHMENT_BLOB_READ_CHUNK_SIZE_BYTES) {
        const end = Math.min(start + ATTACHMENT_BLOB_READ_CHUNK_SIZE_BYTES, fileSize);
        const chunkBase64 = await this.attachmentStorage.readFileChunk(diskPath, start, end);

        if (!chunkBase64) {
          return false;
        }

        blobParts.push(decodeBase64ToUint8Array(chunkBase64));

        if (end < fileSize) {
          await yieldToAttachmentHydrationLoop();
        }
      }

      this.applyAttachmentBlob(attachment, new Blob(blobParts as BlobPart[], { type: attachment.mime }));
      return true;
    }

    const base64 = await this.attachmentStorage.readFile(diskPath);

    if (!base64) {
      return false;
    }

    const bytes = decodeBase64ToUint8Array(base64);

    this.applyAttachmentBlob(
      attachment,
      new Blob([bytes.buffer as ArrayBuffer], { type: attachment.mime })
    );

    return true;
  }

  private applyAttachmentBlob(attachment: Attachment, blob: Blob): void {
    attachment.objectUrl = URL.createObjectURL(blob);
    attachment.available = true;

    this.runtimeStore.setOriginalFile(
      `${attachment.messageId}:${attachment.id}`,
      new File([blob], attachment.filename, { type: attachment.mime })
    );
  }

  private revokeAttachmentObjectUrl(attachment: Attachment): void {
    if (!attachment.objectUrl || !isBlobObjectUrl(attachment.objectUrl)) {
      return;
    }

    try {
      URL.revokeObjectURL(attachment.objectUrl);
    } catch { /* ignore */ }
  }

  private async getRetainedSavedPathsForOtherMessages(messageId: string): Promise<Set<string>> {
    const retainedSavedPaths = new Set<string>();

    for (const [existingMessageId, attachments] of this.runtimeStore.getAttachmentEntries()) {
      if (existingMessageId === messageId)
        continue;

      for (const attachment of attachments) {
        if (attachment.savedPath) {
          retainedSavedPaths.add(attachment.savedPath);
        }
      }
    }

    if (!this.database.isReady()) {
      return retainedSavedPaths;
    }

    const persistedAttachments = await this.database.getAllAttachments();

    for (const attachment of persistedAttachments) {
      if (attachment.messageId !== messageId && attachment.savedPath) {
        retainedSavedPaths.add(attachment.savedPath);
      }
    }

    return retainedSavedPaths;
  }

}
