import {CHATS_LOCAL_STORAGE_KEYS} from '@modules/chats/constants';
import {Storage} from '@services/mmkvFactory';
import {compareUlid, maxUlid} from '@shared/utils/ulid';
import {sentryLogs} from '@shared/utils';

import {PendingMessagesTracker} from './PendingMessagesTracker';
import {sortMessagesByTsHelper} from '../utils';

export interface MessageRange {
  oldestTs: string;
  newestTs: string;
  isStart: boolean; // True if this range starts at the beginning of conversation
  isEnd: boolean; // True if this range ends at the live edge (latest message)
}

class MessageRangesHandlerClass {
  private storage: Storage;
  // In-memory cache to avoid reading from MMKV every time
  private cache: Map<string, MessageRange[]> = new Map();
  // Current user ID - used as prefix for storage keys
  private currentUserId: string | null = null;

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

  /**
   * Set the current user ID. Must be called before using any other methods.
   * This allows per-user storage of message ranges.
   */
  public setUserId(userId: string): void {
    if (this.currentUserId !== userId) {
      // Clear in-memory cache when switching users
      this.cache.clear();
      this.currentUserId = userId;
    }
  }

  /**
   * Get the storage key for a conversation, prefixed with userId
   */
  private getStorageKey(conversationId: string): string {
    if (!this.currentUserId) {
      sentryLogs.warn({
        message: 'UserId not set, using conversationId directly',
        module: 'CHATS',
        component: 'MessageRangesHandler.getStorageKey',
      });
      return conversationId;
    }
    return `${this.currentUserId}:${conversationId}`;
  }

  /**
   * Read ranges for a conversation from MMKV
   */
  private readFromStorage(conversationId: string): MessageRange[] {
    const storageKey = this.getStorageKey(conversationId);

    // Check cache first
    const cached = this.cache.get(storageKey);
    if (cached !== undefined) {
      return cached;
    }

    // Read from MMKV
    const persisted = this.storage.getString(storageKey);
    if (!persisted) {
      this.cache.set(storageKey, []);
      return [];
    }

    try {
      const ranges = JSON.parse(persisted) as MessageRange[];
      this.cache.set(storageKey, ranges);
      return ranges;
    } catch {
      this.cache.set(storageKey, []);
      // Corrupted data - clear it
      this.storage.remove(storageKey);
      return [];
    }
  }

  /**
   * Write ranges for a conversation to MMKV
   */
  private writeToStorage(conversationId: string, ranges: MessageRange[]): void {
    const storageKey = this.getStorageKey(conversationId);
    this.cache.set(storageKey, ranges);
    if (ranges.length === 0) {
      this.storage.remove(storageKey);
    } else {
      this.storage.set(storageKey, JSON.stringify(ranges));
    }
  }

  /**
   * Check if two ranges touch (overlap or are adjacent)
   * Two ranges touch if: a.newest >= b.oldest AND b.newest >= a.oldest
   */
  private rangesTouch(a: MessageRange, b: MessageRange): boolean {
    return a.newestTs >= b.oldestTs && b.newestTs >= a.oldestTs;
  }

  /**
   * Consolidate ranges - sort and merge any that touch
   */
  private consolidateRanges(ranges: MessageRange[]): MessageRange[] {
    if (ranges.length <= 1) {
      return ranges;
    }

    // Sort by oldestTs
    const sorted = [...ranges].sort((a, b) =>
      compareUlid(a.oldestTs, b.oldestTs),
    );

    const consolidated: MessageRange[] = [sorted[0]];

    for (let i = 1; i < sorted.length; i++) {
      const last = consolidated[consolidated.length - 1];
      const current = sorted[i];

      if (this.rangesTouch(last, current)) {
        // Merge current into last
        last.newestTs = maxUlid(last.newestTs, current.newestTs);
        last.isStart = last.isStart || current.isStart;
        last.isEnd = last.isEnd || current.isEnd;
      } else {
        consolidated.push(current);
      }
    }

    return consolidated;
  }

  /**
   * Get all ranges for a conversation
   */
  public get(conversationId: string): MessageRange[] {
    return this.readFromStorage(conversationId);
  }

  /**
   * Find the range that contains a specific timestamp
   */
  public findRangeForTs(
    conversationId: string,
    ts: string,
  ): MessageRange | undefined {
    const ranges = this.get(conversationId);
    const found = ranges.find(r => r.oldestTs <= ts && ts <= r.newestTs);

    return found;
  }

  /**
   * Check if a timestamp is within any loaded range
   */
  public isInLoadedRange(conversationId: string, ts: string): boolean {
    return this.findRangeForTs(conversationId, ts) !== undefined;
  }

  /**
   * Check if the range containing the given timestamp is at the conversation start
   */
  public isAtConversationStart(conversationId: string, ts: string): boolean {
    const range = this.findRangeForTs(conversationId, ts);
    return range?.isStart === true;
  }

  /**
   * Add messages and update ranges accordingly
   * This will create new ranges or extend/merge existing ones
   *
   * bridgeNewestTs: Use this ts as the newestTs of the range (for loadAbove - connects with existing range)
   * bridgeOldestTs: Use this ts as the oldestTs of the range (for loadBelow - connects with existing range)
   * skipPendingCheck: If true, skip the pending messages check (used for confirmed batch updates)
   */
  public addMessagesAndUpdateRanges(
    conversationId: string,
    messages: {ts: string}[],
    meta: {
      isStart?: boolean;
      isEnd?: boolean;
      bridgeNewestTs?: string;
      bridgeOldestTs?: string;
      skipPendingCheck?: boolean;
    } = {},
  ): void {
    if (messages.length === 0) {
      return;
    }

    // Sort messages by ts to get oldest and newest
    const sorted = [...messages].sort(sortMessagesByTsHelper);
    const messagesOldestTs = sorted[0].ts;
    const messagesNewestTs = sorted[sorted.length - 1].ts;

    // Check if there are pending messages before the oldest message we're trying to add
    // This prevents extending the range past messages that haven't been confirmed as saved
    if (!meta.skipPendingCheck) {
      const hasPendingBefore = PendingMessagesTracker.hasPendingMessagesBefore(
        conversationId,
        messagesOldestTs,
      );

      if (hasPendingBefore) {
        // Don't extend the range - there are unconfirmed messages that need to be saved first
        return;
      }
    }

    // Use bridge values if provided (to connect with existing ranges)
    const useBridgeOldest =
      meta.bridgeOldestTs && meta.bridgeOldestTs < messagesOldestTs;
    const useBridgeNewest =
      meta.bridgeNewestTs && meta.bridgeNewestTs > messagesNewestTs;

    const oldestTs = useBridgeOldest ? meta.bridgeOldestTs! : messagesOldestTs;
    const newestTs = useBridgeNewest ? meta.bridgeNewestTs! : messagesNewestTs;

    const newRange: MessageRange = {
      oldestTs,
      newestTs,
      isStart: meta.isStart ?? false,
      isEnd: meta.isEnd ?? false,
    };

    // Get existing ranges
    const existing = this.readFromStorage(conversationId);

    const allRanges = [...existing, newRange];

    // Consolidate will sort and merge any touching ranges
    let consolidated = this.consolidateRanges(allRanges);

    // Ensure only ONE range has isEnd: true - the one with the highest newestTs
    // This handles cases where isEnd might be set on multiple ranges after consolidation
    const rangesWithIsEnd = consolidated.filter(r => r.isEnd);
    if (rangesWithIsEnd.length > 1) {
      // Find the range with the highest newestTs
      const liveEdgeRange = rangesWithIsEnd.reduce((max, r) =>
        r.newestTs > max.newestTs ? r : max,
      );
      // Remove isEnd from all others
      consolidated = consolidated.map(r => ({
        ...r,
        isEnd: r === liveEdgeRange,
      }));
    }

    // Ensure only ONE range has isStart: true - the one with the lowest oldestTs
    // This handles cases where isStart might be set on multiple ranges after consolidation
    // Note: isStart is only set manually when we load the beginning of the chat
    const rangesWithIsStart = consolidated.filter(r => r.isStart);
    if (rangesWithIsStart.length > 1) {
      // Find the range with the lowest oldestTs
      const startRange = rangesWithIsStart.reduce((min, r) =>
        r.oldestTs < min.oldestTs ? r : min,
      );
      // Remove isStart only from ranges that had it but are not the oldest
      consolidated = consolidated.map(r => ({
        ...r,
        isStart: r.isStart && r === startRange, //We need to verify that isStart was there before
      }));
    }

    this.writeToStorage(conversationId, consolidated);
  }

  /**
   * Mark that a range reaches the start of the conversation
   */
  public markAsStart(conversationId: string, ts: string): void {
    const ranges = this.readFromStorage(conversationId);
    const range = ranges.find(r => r.oldestTs <= ts && ts <= r.newestTs);
    if (range) {
      range.isStart = true;
      this.writeToStorage(conversationId, ranges);
    }
  }

  /**
   * Mark that a range reaches the live edge (latest message)
   */
  public markAsEnd(conversationId: string, ts: string): void {
    const ranges = this.readFromStorage(conversationId);
    const range = ranges.find(r => r.oldestTs <= ts && ts <= r.newestTs);
    if (range) {
      range.isEnd = true;
      this.writeToStorage(conversationId, ranges);
    }
  }

  /**
   * Clear ranges for a specific conversation
   */
  public clear(conversationId: string): void {
    const storageKey = this.getStorageKey(conversationId);
    this.cache.delete(storageKey);
    this.storage.remove(storageKey);
  }
}

export const MessageRangesHandler = new MessageRangesHandlerClass();
