import {db} from '@db/instance';
import {
  clearMembershipHandlerStateStore,
  synchroniser,
} from '@config/synchroniser';
import {
  ReactionsUpdaterService,
  UnreadsUpdaterService,
} from '@modules/chats/instances/updaters';
import {FileMessagesLogger} from '@modules/chats/instances/FileMessagesLogger';
import {ConversationMediaPrefetcher} from '@modules/chats/instances/ConversationMediaPrefetcher';
import reduxStore from '@redux/store';
import {
  getPersistedInstanceId,
  persistedUserId,
  removeItemFromStorage,
} from '@services/localStorage';
import {pendingNotificationsService} from '@shared/notifications/NotificationManager/PendingNotificationsService';
import {BlockingLatch, getUlid, sentryLogs} from '@shared/utils';

import {useGroups} from '../store/zustand/useGroups';
import {CHATS_LOCAL_STORAGE_KEYS} from '../constants';
import {clearChatsStore} from '../store';
import {fileCompressionHandler} from './FileCompressionHandler';
import {fileHandler} from './FileHandler';
import {usersMapper} from './UsersMapper';
import {SyncEvents} from './SyncEvents';
import {RecentsHandler} from './Recents';
import {MessageRangesHandler} from './MessageRangesHandler';
import {ConversationDrafts} from './ConversationDrafts';
import {PinnedMessagesUpdaterService} from './updaters';
import {unprocessedEventsService} from './UnprocessedEventsService';

const CHATS_DB_LATCH = 'chats_db_ready';

class ChatsStarterClass {
  private started = false;
  private isRunning = false;

  private clearStores = () => {
    clearChatsStore();
  };

  private clearStorages = () => {
    RecentsHandler.clear();
    clearMembershipHandlerStateStore();
    fileHandler.clearCache();
    fileCompressionHandler.clearCache();
    removeItemFromStorage(CHATS_LOCAL_STORAGE_KEYS.LastFeatureShown);
    removeItemFromStorage(CHATS_LOCAL_STORAGE_KEYS.NewCallsV2ViewedStorage);
    UnreadsUpdaterService.cleanup();
    ReactionsUpdaterService.cleanup();
    // Note: MessageRangesHandler is NOT cleared on logout - it's per-user in MMKV
  };

  /**
   * Clear chats stores and storages
   */
  public clear = () => {
    this.clearStores();
    this.clearStorages();

    // Cancel any pending ConversationMediaPrefetcher debounce so it doesn't
    // re-fire against the new user's (empty) store after logout.
    ConversationMediaPrefetcher.dispose();
  };

  /**
   * Close the db connection and reset class properties
   */
  public close = () => {
    this.started = false;
    db.close();
    FileMessagesLogger.cleanup();
    unprocessedEventsService.cleanup();
  };

  /**
   * Run the starter
   */
  public run = async ({
    currentUserId,
    currentInstanceId,
  }: {
    currentUserId?: number;
    currentInstanceId?: number;
  } = {}) => {
    /**
     * If chats already started, do not run the starter again
     */
    if (this.started || this.isRunning) {
      return;
    }

    BlockingLatch.block(CHATS_DB_LATCH);
    this.isRunning = true;
    // other modules can wait for the chat sync to finish by waiting on this latch

    const {
      setIsConversationsLoaded,
      initGroups,
      enableUpdates,
      setIsError,
      setIsFetchingAllConversations,
    } = useGroups.getState();

    try {
      let userId = currentUserId || persistedUserId;
      let instanceId = currentInstanceId || (await getPersistedInstanceId());

      if (!userId || !instanceId) {
        // Try to get the user id and instance id from the store. This should be happen only when the app is opened for the first time
        const {user, instance} = reduxStore().store.getState();
        userId = user?.id;
        instanceId = instance?.id;

        if (!userId || !instanceId) {
          return;
        }
      }

      if (!userId || !instanceId) {
        throw new Error('User id or instance id not found');
      }

      // Check if this is a new database version to decide on clearing storage
      const isNewDb = await db.isNewVersion();

      // If the db version changed, clear chats local storage
      if (isNewDb) {
        this.clearStorages();
      }

      UnreadsUpdaterService.init(userId);

      // Always run migrations - let Drizzle decide which ones to execute
      // Drizzle maintains its own migration state and will skip already executed ones
      await db.open({
        userId,
        instanceId,
        forceNew: true,
        runMigrations: true, // Always true - let Drizzle handle it
      });

      FileMessagesLogger.init();

      // Set the user ID for MessageRangesHandler (per-user storage)
      MessageRangesHandler.setUserId(String(userId));
      PinnedMessagesUpdaterService.setUserId(String(userId));
      ConversationDrafts.setUserId(String(userId));

      const {needGetAllConversations, needGetRecents} =
        RecentsHandler.getFlags();

      // Get conversation list asynchronously (this only runs once per user)
      if (needGetAllConversations) {
        setIsFetchingAllConversations(true);
        RecentsHandler.getAllConversations()
          .catch(error => {
            sentryLogs.error({
              message: 'Error fetching all conversations',
              module: 'CHATS',
              component: 'ChatsStarter.run',
              error,
            });
          })
          .finally(() => {
            setIsFetchingAllConversations(false);
          });
      }

      // If recents will run, we only fetch this (we don't need to run other synchronisers)
      if (needGetRecents) {
        await RecentsHandler.getRecents();
        ConversationMediaPrefetcher.schedule();
        await this.flushPendingNotifications();
        setIsConversationsLoaded(true);
      } else {
        // First, populate store with local data and get lost events from server in parallel
        await initGroups();

        ConversationMediaPrefetcher.schedule();

        // Flush before sync but after groups init
        await this.flushPendingNotifications();

        SyncEvents.run();

        // Then, run other synchronisers, and store initial values
        synchroniser.run();
        usersMapper.init();
        ReactionsUpdaterService.init();
      }

      this.started = true;
    } catch (error) {
      sentryLogs.error({
        message: 'Chats starter failed',
        module: 'CHATS',
        component: 'ChatsStarter.run',
        error:
          error instanceof Error
            ? String(error.cause ?? error.message)
            : String(error),
      });
      setIsError(true);
    } finally {
      // Enable updates to the useGroups store after all sync finished
      enableUpdates();
      this.isRunning = false;
      FileMessagesLogger.sendDailyLogs();
      BlockingLatch.unblock(CHATS_DB_LATCH);
    }
  };

  public retry = () => {
    sentryLogs.info({
      message: 'Chats starter retry',
      module: 'CHATS',
      component: 'ChatsStarter.retry',
    });
    this.started = false;
    const {setIsError, setIsConversationsLoaded} = useGroups.getState();
    setIsError(false);
    setIsConversationsLoaded(false);
    this.run();
  };

  /**
   * Flush pending push notifications only when the chat store is ready.
   * Safe to call on every foreground transition — no-ops until `started` is true.
   */
  public flushPendingNotifications = async (
    checkIsStarted: boolean = false,
  ) => {
    if (checkIsStarted && !this.started) {
      return;
    }
    try {
      await pendingNotificationsService.flushPendingNotifications();
    } catch (error) {
      sentryLogs.error({
        message: `Failed to flush pending notifications`,
        module: 'CHATS',
        data: {
          checkIsStarted,
        },
        component: 'ChatsStarter.flushPendingNotifications',
        error: error instanceof Error ? error.message : String(error),
      });
    }
  };

  /**
   * Pulling lost events and unread state from server.
   * @important This must be used only when socket reconnects.
   */
  public runSync = async ({
    isSocketRecovered,
  }: {isSocketRecovered?: boolean} = {}) => {
    // The socket can authenticate and trigger runSync before db.open() completes (race condition).
    // Wait for the latch to ensure DB is ready before processing events.
    await BlockingLatch.wait(CHATS_DB_LATCH);

    SyncEvents.run({
      skipPullEvents: isSocketRecovered,
      catchUpFrom: !isSocketRecovered ? getUlid() : undefined,
    });
    !isSocketRecovered && UnreadsUpdaterService.runSync();
  };
}

export const ChatsStarter = new ChatsStarterClass();
