/* eslint-disable @typescript-eslint/member-ordering */
import {
  Injectable,
  computed,
  inject,
  signal
} from '@angular/core';
import { v4 as uuidv4 } from 'uuid';
import { DatabaseService } from '../../../infrastructure/persistence';
import { RealtimeSessionFacade } from '../../../core/realtime';
import {
  CustomEmoji,
  CustomEmojiTransferManifest,
  EmojiShortcutEntry
} from '../../../shared-kernel';
import {
  assembleCustomEmojiDataUrl,
  buildCustomEmojiMessageToken,
  buildCustomEmojiSummary,
  canInlineCustomEmojiTransfer,
  CUSTOM_EMOJI_ALLOWED_MIME_TYPES,
  CUSTOM_EMOJI_DATA_CHANNEL_CHUNK_SIZE_BYTES,
  CUSTOM_EMOJI_MAX_SIZE_BYTES,
  CUSTOM_EMOJI_MAX_TRANSFER_CHUNKS,
  customEmojiKey,
  extractCustomEmojiIds,
  getCustomEmojiDataUrlByteSize,
  selectEmojiShortcutEntries,
  shouldRequestCustomEmoji,
  splitCustomEmojiDataUrl,
  unicodeEmojiKey,
  validateCustomEmojiFile
} from '../domain/custom-emoji.rules';

const USAGE_STORAGE_PREFIX = 'metoyou_custom_emoji_usage:';

interface PendingCustomEmojiTransfer {
  chunks: (string | undefined)[];
  manifest: CustomEmojiTransferManifest;
  total: number;
}

@Injectable({ providedIn: 'root' })
export class CustomEmojiService {
  private readonly db = inject(DatabaseService);
  private readonly webrtc = inject(RealtimeSessionFacade);
  private readonly emojisState = signal<CustomEmoji[]>([]);
  private readonly usageState = signal<ReadonlyMap<string, number>>(new Map());
  private readonly pendingTransfers = new Map<string, PendingCustomEmojiTransfer>();
  private loaded = false;

  readonly emojis = computed(() => this.emojisState().filter((emoji) => this.isSavedEmoji(emoji)));
  readonly shortcutEntries = computed(() => selectEmojiShortcutEntries({
    customEmojis: this.emojis(),
    usage: this.usageState()
  }));

  async loadForUser(userId: string | null | undefined): Promise<void> {
    const emojis = await this.db.getCustomEmojis();
    const merged = new Map(this.emojisState().map((emoji) => [emoji.id, emoji]));

    for (const emoji of emojis) {
      if (await this.isValidRemoteEmoji(emoji)) {
        const existing = merged.get(emoji.id);

        if (!existing || existing.updatedAt < emoji.updatedAt) {
          merged.set(emoji.id, emoji);
        }
      } else {
        await this.db.deleteCustomEmoji(emoji.id);
        merged.delete(emoji.id);
      }
    }

    this.emojisState.set([...merged.values()].sort((first, second) => second.updatedAt - first.updatedAt));
    this.usageState.set(this.readUsage(userId));
    this.loaded = true;
  }

  async ensureLoaded(userId?: string | null): Promise<void> {
    if (this.loaded) {
      return;
    }

    await this.loadForUser(userId);
  }

  async createFromFile(file: File, userId: string): Promise<CustomEmoji> {
    const validation = validateCustomEmojiFile(file);

    if (!validation.valid) {
      throw new Error(validation.reason ?? 'Invalid emoji image.');
    }

    const dataUrl = await this.readFileAsDataUrl(file);
    const now = Date.now();
    const emoji: CustomEmoji = {
      id: uuidv4(),
      name: this.normaliseEmojiName(file.name),
      creatorUserId: userId,
      dataUrl,
      hash: await this.hashText(dataUrl),
      mime: file.type.toLowerCase(),
      size: file.size,
      createdAt: now,
      updatedAt: now,
      savedByUser: true
    };

    await this.saveRemoteEmoji(emoji);
    await this.broadcastEmoji(emoji);

    return emoji;
  }

  async saveRemoteEmoji(emoji: CustomEmoji): Promise<void> {
    const existing = this.emojisState().find((entry) => entry.id === emoji.id);

    if (existing && existing.updatedAt >= emoji.updatedAt) {
      return;
    }

    if (!(await this.isValidRemoteEmoji(emoji))) {
      return;
    }

    const nextEmoji: CustomEmoji = {
      ...emoji,
      savedByUser: existing?.savedByUser || emoji.savedByUser === true
    };

    await this.db.saveCustomEmoji(nextEmoji);

    const nextEmojis = [nextEmoji, ...this.emojisState().filter((entry) => entry.id !== nextEmoji.id)];

    this.emojisState.set(nextEmojis.sort((first, second) => second.updatedAt - first.updatedAt));
  }

  findEmoji(id: string): CustomEmoji | undefined {
    return this.emojisState().find((emoji) => emoji.id === id);
  }

  findEmojiByName(name: string): CustomEmoji | undefined {
    const normalizedName = name.trim().toLowerCase();

    return this.emojis().find((emoji) => emoji.name.toLowerCase() === normalizedName);
  }

  isEmojiInLibrary(id: string): boolean {
    const emoji = this.findEmoji(id);

    return !!emoji && this.isSavedEmoji(emoji);
  }

  async saveEmojiToLibrary(id: string): Promise<void> {
    const emoji = this.findEmoji(id);

    if (!emoji || this.isSavedEmoji(emoji)) {
      return;
    }

    const savedEmoji = {
      ...emoji,
      savedByUser: true
    };

    await this.db.saveCustomEmoji(savedEmoji);
    this.emojisState.set(this.emojisState().map((entry) => entry.id === id ? savedEmoji : entry));
  }

  async removeEmojiFromLibrary(id: string): Promise<void> {
    const emoji = this.findEmoji(id);

    if (!emoji || !this.isSavedEmoji(emoji)) {
      return;
    }

    const unsavedEmoji = {
      ...emoji,
      savedByUser: false
    };

    await this.db.saveCustomEmoji(unsavedEmoji);
    this.emojisState.set(this.emojisState().map((entry) => entry.id === id ? unsavedEmoji : entry));
  }

  recordUsage(entry: EmojiShortcutEntry, userId: string | null | undefined): void {
    const key = entry.kind === 'custom' ? customEmojiKey(entry.emoji.id) : unicodeEmojiKey(entry.emoji);
    const usage = new Map(this.usageState());

    usage.set(key, (usage.get(key) ?? 0) + 1);
    this.usageState.set(usage);
    this.writeUsage(userId, usage);
  }

  buildMessageToken(emoji: CustomEmoji): string {
    return buildCustomEmojiMessageToken(emoji);
  }

  buildSummary() {
    return this.emojisState().map((emoji) => buildCustomEmojiSummary(emoji));
  }

  missingFromSummary(summary: ReturnType<CustomEmojiService['buildSummary']>): string[] {
    const local = new Map(this.emojisState().map((emoji) => [emoji.id, emoji]));

    return summary.filter((item) => shouldRequestCustomEmoji(local.get(item.id), item)).map((item) => item.id);
  }

  async sendSummaryToPeer(peerId: string): Promise<void> {
    await this.ensureLoaded();
    this.webrtc.sendToPeer(peerId, {
      type: 'custom-emoji-summary',
      customEmojiSummaries: this.buildSummary()
    });
  }

  async sendRequestedToPeer(peerId: string, ids: string[]): Promise<void> {
    await this.ensureLoaded();

    const emojis = ids
      .map((id) => this.findEmoji(id))
      .filter((emoji): emoji is CustomEmoji => !!emoji);

    await Promise.all(emojis.map((emoji) => this.sendEmojiToPeer(peerId, emoji)));
  }

  pushEmojisInContent(content: string): void {
    void this.pushEmojisForContent(content);
  }

  async pushEmojisForContent(content: string): Promise<void> {
    await this.ensureLoaded();

    const peers = this.webrtc.getConnectedPeers();

    if (peers.length === 0) {
      return;
    }

    const emojis = extractCustomEmojiIds(content)
      .map((id) => this.findEmoji(id))
      .filter((emoji): emoji is CustomEmoji => !!emoji);

    if (emojis.length === 0) {
      return;
    }

    await Promise.all(
      peers.flatMap((peerId) => emojis.map((emoji) => this.sendEmojiToPeer(peerId, emoji)))
    );
  }

  async requestMissingFromMessageContent(peerId: string, content: string): Promise<void> {
    await this.ensureLoaded();

    const ids = extractCustomEmojiIds(content).filter((id) => !this.findEmoji(id));

    if (ids.length === 0) {
      return;
    }

    this.webrtc.sendToPeer(peerId, {
      type: 'custom-emoji-request',
      ids
    });
  }

  receiveTransferStart(manifest: CustomEmojiTransferManifest, total: number): void {
    const existing = this.emojisState().find((entry) => entry.id === manifest.id);

    if (existing && existing.updatedAt >= manifest.updatedAt) {
      return;
    }

    if (total < 1 || total > CUSTOM_EMOJI_MAX_TRANSFER_CHUNKS || manifest.size > CUSTOM_EMOJI_MAX_SIZE_BYTES || !this.isAllowedMime(manifest.mime)) {
      return;
    }

    this.pendingTransfers.set(manifest.id, {
      chunks: Array.from({ length: total }, () => undefined),
      manifest,
      total
    });
  }

  async receiveTransferChunk(emojiId: string, index: number, total: number, data: string): Promise<void> {
    const transfer = this.pendingTransfers.get(emojiId);
    const invalidChunkIndex = !transfer || transfer.total !== total || index < 0 || index >= transfer.total;
    const invalidChunkData = !data || data.length > CUSTOM_EMOJI_DATA_CHANNEL_CHUNK_SIZE_BYTES;

    if (invalidChunkIndex || invalidChunkData) {
      return;
    }

    transfer.chunks[index] = data;

    if (transfer.chunks.some((chunk) => !chunk)) {
      return;
    }

    this.pendingTransfers.delete(emojiId);

    const chunks = transfer.chunks.filter((chunk): chunk is string => typeof chunk === 'string');

    if (chunks.length !== transfer.total) {
      return;
    }

    await this.saveRemoteEmoji({
      ...transfer.manifest,
      dataUrl: assembleCustomEmojiDataUrl(transfer.manifest.mime, chunks)
    });
  }

  private async broadcastEmoji(emoji: CustomEmoji): Promise<void> {
    const peers = this.webrtc.getConnectedPeers();

    await Promise.all(peers.map((peerId) => this.sendEmojiToPeer(peerId, emoji)));
  }

  private async sendEmojiToPeer(peerId: string, emoji: CustomEmoji): Promise<void> {
    if (canInlineCustomEmojiTransfer(emoji)) {
      await this.webrtc.sendToPeerBuffered(peerId, {
        type: 'custom-emoji-full',
        customEmoji: emoji
      });

      return;
    }

    const transfer = splitCustomEmojiDataUrl(emoji.dataUrl);
    const manifest: CustomEmojiTransferManifest = {
      id: emoji.id,
      name: emoji.name,
      creatorUserId: emoji.creatorUserId,
      hash: emoji.hash,
      mime: emoji.mime,
      size: emoji.size,
      createdAt: emoji.createdAt,
      updatedAt: emoji.updatedAt
    };

    await this.webrtc.sendToPeerBuffered(peerId, {
      type: 'custom-emoji-full',
      customEmojiTransfer: manifest,
      total: transfer.total
    });

    for (let chunkIndex = 0; chunkIndex < transfer.chunks.length; chunkIndex++) {
      await this.webrtc.sendToPeerBuffered(peerId, {
        type: 'custom-emoji-chunk',
        customEmojiId: emoji.id,
        index: chunkIndex,
        total: transfer.total,
        data: transfer.chunks[chunkIndex]
      });
    }
  }

  private async isValidRemoteEmoji(emoji: CustomEmoji): Promise<boolean> {
    if (emoji.size < 1 || emoji.size > CUSTOM_EMOJI_MAX_SIZE_BYTES || !this.isAllowedMime(emoji.mime)) {
      return false;
    }

    if (getCustomEmojiDataUrlByteSize(emoji.dataUrl) !== emoji.size) {
      return false;
    }

    return await this.hashText(emoji.dataUrl) === emoji.hash;
  }

  private isAllowedMime(mime: string): boolean {
    return CUSTOM_EMOJI_ALLOWED_MIME_TYPES.includes(mime.toLowerCase() as typeof CUSTOM_EMOJI_ALLOWED_MIME_TYPES[number]);
  }

  private isSavedEmoji(emoji: CustomEmoji): boolean {
    return emoji.savedByUser !== false;
  }

  private readUsage(userId: string | null | undefined): ReadonlyMap<string, number> {
    if (!userId) {
      return new Map();
    }

    try {
      const parsed = JSON.parse(localStorage.getItem(`${USAGE_STORAGE_PREFIX}${userId}`) ?? '{}') as Record<string, number>;

      return new Map(Object.entries(parsed).filter(([, value]) => Number.isFinite(value) && value > 0));
    } catch {
      return new Map();
    }
  }

  private writeUsage(userId: string | null | undefined, usage: ReadonlyMap<string, number>): void {
    if (!userId) {
      return;
    }

    localStorage.setItem(`${USAGE_STORAGE_PREFIX}${userId}`, JSON.stringify(Object.fromEntries(usage)));
  }

  private readFileAsDataUrl(file: File): Promise<string> {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();

      reader.onload = () => typeof reader.result === 'string' ? resolve(reader.result) : reject(new Error('Unable to read emoji image.'));
      reader.onerror = () => reject(reader.error ?? new Error('Unable to read emoji image.'));
      reader.readAsDataURL(file);
    });
  }

  private async hashText(value: string): Promise<string> {
    const bytes = new TextEncoder().encode(value);
    const digest = await crypto.subtle.digest('SHA-256', bytes);

    return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
  }

  private normaliseEmojiName(filename: string): string {
    const baseName = filename.replace(/\.[^.]+$/, '').toLowerCase()
      .replace(/[^a-z0-9_-]+/g, '-')
      .replace(/^-+|-+$/g, '');

    return baseName || 'emoji';
  }
}
