import {CHATS_LOCAL_STORAGE_KEYS, PinDuration} from '@modules/chats/constants';
import {
  getPinnedMessages,
  addPinnedMessage,
  removePinnedMessage,
  PinRaceConditionError,
} from '@modules/chats/services';
import {Message} from '@modules/chats/interfaces';
import {PinnedMessageItem} from '@modules/chats/services/interfaces';
import {getMessageByTsWithAssets} from '@modules/chats/db';
import {
  getExpiresAtForPinDuration,
  mapMessageFromDB,
  mapMessageFromServer,
} from '@modules/chats/utils';
import {usePinnedMessages} from '@modules/chats/store/zustand/usePinnedMessages';
import {Storage} from '@services/mmkvFactory';
import {sentryLogs} from '@shared/utils';

import {MessagesOrchestrator} from '../MessagesOrchestrator';

class PinnedMessagesUpdaterServiceClass {
  /** MMKV persistence for pinned message ts lists, survives app restarts */
  private storage: Storage;
  private currentUserId: string | null = null;
  /**
   * Per-channel epoch counter to prevent stale getPinnedMessages responses
   * from overwriting optimistic updates made by add()/remove().
   */
  private fetchEpoch: Map<string, number> = new Map();

  constructor() {
    this.storage = new Storage({
      id: CHATS_LOCAL_STORAGE_KEYS.PinnedMessagesStorage,
    });
  }

  private getEpoch(channel: string): number {
    return this.fetchEpoch.get(channel) ?? 0;
  }

  private incrementEpoch(channel: string): void {
    this.fetchEpoch.set(channel, this.getEpoch(channel) + 1);
  }

  public setUserId(userId: string): void {
    if (this.currentUserId !== userId) {
      this.currentUserId = userId;
      usePinnedMessages.getState().clear();
    }
  }

  private getStorageKey(channel: string): string {
    if (!this.currentUserId) {
      sentryLogs.warn({
        message: 'UserId not set, using channel directly',
        module: 'CHATS',
        component: 'PinnedMessagesUpdaterService.getStorageKey',
      });
      return channel;
    }
    return `${this.currentUserId}:${channel}`;
  }

  private readTsListFromStorage(channel: string): string[] | null {
    const storageKey = this.getStorageKey(channel);
    const persisted = this.storage.getString(storageKey);
    if (!persisted) {
      return null;
    }

    try {
      return JSON.parse(persisted) as string[];
    } catch {
      this.storage.remove(storageKey);
      return null;
    }
  }

  private writeTsListToStorage(channel: string, tsList: string[]): void {
    const storageKey = this.getStorageKey(channel);
    this.storage.set(storageKey, JSON.stringify(tsList));
  }

  private async syncFromResponse(
    channel: string,
    items: PinnedMessageItem[],
  ): Promise<void> {
    const messageEvents = items.map(item => item.message);
    const messages = await Promise.all(messageEvents.map(mapMessageFromServer));
    this.writeTsListToStorage(
      channel,
      messageEvents.map(m => m.hu_data.message_ts),
    );
    usePinnedMessages.getState().setPinned(channel, messages);
  }

  private async loadFromDb(tsList: string[]): Promise<Message[]> {
    const results = await Promise.all(
      tsList.map(async ts => {
        const dbRecord = await getMessageByTsWithAssets(ts);
        if (!dbRecord) {
          return null;
        }
        return mapMessageFromDB({message: dbRecord, assets: dbRecord.assets});
      }),
    );
    return results.filter((m): m is Message => m !== null);
  }

  async getPinnedMessages(channel: string): Promise<void> {
    const epochAtStart = this.getEpoch(channel);
    try {
      const response = await getPinnedMessages({channel});

      if (this.getEpoch(channel) !== epochAtStart) {
        return;
      }

      if (!response.items?.length) {
        this.writeTsListToStorage(channel, []);
        usePinnedMessages.getState().setPinned(channel, []);
        return;
      }

      await this.syncFromResponse(channel, response.items);
      this.triggerLoadMessagesAroundTs(
        channel,
        response.items.map(item => item.message.hu_data.message_ts),
      );
    } catch {
      // Cached data remains available via getCachedPinnedMessages
    }
  }

  getCachedPinnedMessages(channel: string): Message[] {
    return usePinnedMessages.getState().pinnedByChannel[channel] ?? [];
  }

  isPinned(channel: string, ts: string): boolean {
    return this.getCachedPinnedMessages(channel).some(m => m.ts === ts);
  }

  private isChannelInStore(channel: string): boolean {
    return channel in usePinnedMessages.getState().pinnedByChannel;
  }

  private isChannelKnown(channel: string): boolean {
    return (
      this.isChannelInStore(channel) ||
      this.readTsListFromStorage(channel) !== null
    );
  }

  private updatePinnedList(
    channel: string,
    updater: (items: Message[]) => Message[],
    {skipStorage = false}: {skipStorage?: boolean} = {},
  ): void {
    const currentItems = this.getCachedPinnedMessages(channel);
    const updatedItems = updater(currentItems);
    usePinnedMessages.getState().setPinned(channel, updatedItems);
    if (!skipStorage) {
      this.writeTsListToStorage(
        channel,
        updatedItems.map(m => m.ts),
      );
    }
  }

  private updateTsListOnly(
    channel: string,
    updater: (tsList: string[]) => string[],
  ): void {
    const currentTsList = this.readTsListFromStorage(channel) ?? [];
    this.writeTsListToStorage(channel, updater(currentTsList));
  }

  async loadOrFetch(channel: string): Promise<void> {
    const store = usePinnedMessages.getState();

    // Already loaded (even if empty), skip redundant fetch
    if (channel in store.pinnedByChannel) {
      return;
    }

    const tsList = this.readTsListFromStorage(channel);

    if (tsList !== null) {
      const fromDb = tsList.length > 0 ? await this.loadFromDb(tsList) : [];
      usePinnedMessages.getState().setPinned(channel, fromDb);
      return;
    }

    store.setLoading(channel, true);
    try {
      await this.getPinnedMessages(channel);
    } finally {
      store.setLoading(channel, false);
    }
  }

  async add(params: {
    channel: string;
    message: Message;
    duration: PinDuration;
    idempotencyKey?: string;
    tsToReplace?: string;
  }): Promise<void> {
    const {channel, message, tsToReplace} = params;
    const ts = message.ts;

    if (this.isPinned(channel, ts)) {
      return;
    }

    const expiresAt = getExpiresAtForPinDuration(params.duration);
    const currentItems = this.getCachedPinnedMessages(channel);

    this.incrementEpoch(channel);
    this.updatePinnedList(
      channel,
      items => {
        const baseItems = tsToReplace
          ? items.filter(m => m.ts !== tsToReplace)
          : items;
        return [...baseItems, message];
      },
      {skipStorage: true},
    );
    this.triggerLoadMessagesAroundTs(channel, [ts]);

    try {
      const response = await addPinnedMessage({
        channel,
        ts,
        expiresAt,
        idempotencyKey: params.idempotencyKey ?? `${ts}-${Date.now()}`,
      });

      if (response.items) {
        await this.syncFromResponse(channel, response.items);
      }
    } catch (error) {
      if (error instanceof PinRaceConditionError) {
        this.getPinnedMessages(channel).catch(() => {});
        return;
      }
      sentryLogs.error({
        message: 'Failed to add pinned message',
        module: 'CHATS',
        component: 'PinnedMessagesUpdaterService.add',
        error,
        data: {channel, ts, tsToReplace},
      });
      usePinnedMessages.getState().setPinned(channel, currentItems);
      throw error;
    }
  }

  async remove(params: {
    channel: string;
    ts: string;
    idempotencyKey?: string;
  }): Promise<void> {
    const {channel, ts} = params;

    if (!this.isPinned(channel, ts)) {
      return;
    }

    const currentItems = this.getCachedPinnedMessages(channel);

    this.incrementEpoch(channel);
    this.updatePinnedList(channel, items => items.filter(m => m.ts !== ts), {
      skipStorage: true,
    });

    try {
      const response = await removePinnedMessage({
        channel,
        ts,
        idempotencyKey: params.idempotencyKey ?? `${ts}-${Date.now()}`,
      });

      if (response.items) {
        await this.syncFromResponse(channel, response.items);
      }
    } catch (error) {
      if (error instanceof PinRaceConditionError) {
        this.getPinnedMessages(channel).catch(() => {});
        return;
      }
      sentryLogs.error({
        message: 'Failed to remove pinned message',
        module: 'CHATS',
        component: 'PinnedMessagesUpdaterService.remove',
        error,
        data: {channel, ts},
      });
      usePinnedMessages.getState().setPinned(channel, currentItems);
      throw error;
    }
  }

  addFromSocket(channel: string, message: Message): void {
    if (!this.isChannelKnown(channel)) {
      return;
    }

    const ts = message.ts;
    this.incrementEpoch(channel);

    if (this.isChannelInStore(channel)) {
      if (this.isPinned(channel, ts)) {
        return;
      }
      this.updatePinnedList(channel, items => [...items, message]);
    } else {
      this.updateTsListOnly(channel, tsList =>
        tsList.includes(ts) ? tsList : [...tsList, ts],
      );
    }

    this.triggerLoadMessagesAroundTs(channel, [ts]);
  }

  removeFromSocket(channel: string, ts: string): void {
    if (!this.isChannelKnown(channel)) {
      return;
    }

    // The add event for this ts is likely still in-flight (async mapping).
    // Throw so the sync retry mechanism (SyncEvents.pendingEvents) can
    // re-process this remove once the add has completed.
    const notFoundError = new Error(
      `Cannot remove pin ${ts}: not found for channel ${channel}`,
    );

    this.incrementEpoch(channel);

    if (this.isChannelInStore(channel)) {
      if (!this.isPinned(channel, ts)) {
        throw notFoundError;
      }
      this.updatePinnedList(channel, items => items.filter(m => m.ts !== ts));
    } else {
      const tsList = this.readTsListFromStorage(channel) ?? [];
      if (!tsList.includes(ts)) {
        throw notFoundError;
      }
      this.writeTsListToStorage(
        channel,
        tsList.filter(t => t !== ts),
      );
    }
  }

  updateMessage(channel: string, message: Message): void {
    usePinnedMessages.getState().updateMessage(channel, message);
  }

  private triggerLoadMessagesAroundTs(channel: string, tsList: string[]): void {
    tsList.forEach(ts => {
      MessagesOrchestrator.loadMessagesAroundTs(channel, ts).catch(() => {});
    });
  }
}

export const PinnedMessagesUpdaterService =
  new PinnedMessagesUpdaterServiceClass();
