import {AppState} from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
import {createMMKV} from 'react-native-mmkv';
import {IS_DEV} from '@shared/keys';
import {isFeatureFlagEnabled} from '@stores/featureFlags';

import {sendMessagesLogFile} from '../services';

const LOG_DIR_NAME = 'chat-logs';
const MESSAGES_LOG_FILE_NAME = 'msg-ts.log';

// ts(13) + ':'(1) + ulid(26) + ':'(1) + status(1) + '\n'(1)
const LINE_SIZE_BYTES = 43;

const LINES_CAPACITY = 50_000;

const DEFAULT_REQUESTED_BYTES = LINE_SIZE_BYTES * LINES_CAPACITY;

const LAST_SEND_TS_KEY = 'last-send-ts';
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const store = createMMKV({id: 'log-file-send-ts'});

type LogMeta = {
  cursor: number; // next write position
  capacity: number; // max lines in the file
};

type LogInfo = {
  filePath: string;
  meta: LogMeta;
};

export enum LogStatus {
  ARRIVED = 0,
  SAVED = 1,
}

declare global {
  // eslint-disable-next-line no-var
  var __NativeFileLogger: {
    /**
     * @param filePath path to the log file
     * @param requestedBytes max file size will be reserved
     * @return boolean indicating whether the file was successfully created and reserved with the requested size
     */
    init: (filePath: string, requestedBytes: number) => boolean;
    /**
     * @param ts a message_ts to log
     * @param status the status of the log entry (e.g. ARRIVED or SAVED)
     * @returns boolean indicating whether the log was successfully written to the file
     */
    append: (ts: string, status: LogStatus) => boolean;
    /**
     * @returns metadata about the log file
     * cursor - the position of the next write (index in the circular buffer)
     * capacity - the max file size (as requested in init)
     */
    get_meta: () => LogMeta;

    /**
     * Clears the log file by writing zeros to it. The file size and capacity remain unchanged.
     * The cursor is reset to 0.
     */
    clear: () => void;

    /**
     * Checks if the log file is empty (i.e. has no log entries) by checking if the first line is empty.
     * @returns boolean
     */
    is_empty: () => boolean;

    /**
     * Start writing to an in-memory buffer instead of the file. This is used to block log writes while the logs are being sent to the server,
     * to prevent log loss during the sending process.
     */
    start_buffering: () => void;

    /**
     * Stop buffering and flush the buffer to the file. This is called after the logs have been sent to the server
     * to unblock log writes and ensure that any logs that were generated during the sending process are not lost.
     */
    stop_buffering_and_flush: () => void;
  };
}

/**
 * Utility for logging message timestamps to a file for debugging purposes.
 * Each log entry is a line containing only the current log timestamp and the message.message_ts. (e.g. "<current timestamp>:<message_ts>")
 * The file has a fixed max size, and new entries will overwrite the oldest ones when the limit is reached.
 * This is designed to be lightweight and efficient for logging large numbers of messages without consuming too much disk space.
 *
 * The log file is stored in the app's document directory under "chat-logs/msg-ts.log". The logger must be initialized with a requested max size before use.
 * It tries to reserve the requested size on initialization, but the actual reserved size may be smaller depending on the device's file system limits.
 * Each log line is 43 bytes, so the number of loggable entries is approximately (requestedBytes / 43).
 *
 * logInfo can be retrieved to get the actual file path and metadata about the log file,
 * including the current cursor position (i.e. the position of the next write) and capacity (i.e. the max file size).
 *
 * @Note log timestamp is used to find the latest log and init the cursor position
 *
 * Example usage:
 * FileMessagesLogger.init(LINE_SIZE_BYTES * LINES_CAPACITY);
 * FileMessagesLogger.logMessageTs(message.message_ts);
 *
 * const logInfo = FileMessagesLogger.getInfo();
 */
export class FileMessagesLogger {
  private static initialized = false;
  private static isSending = false;
  private static listener: Nullable<
    ReturnType<typeof AppState.addEventListener>
  > = null;

  // 43byte (Line) * 50_000 lines = 2.15MB
  static init = (requestedBytes: number = DEFAULT_REQUESTED_BYTES) => {
    try {
      if (!__NativeFileLogger?.init) {
        throw new Error('NativeFileLogger is not available');
      }

      const path = this.getFilePath();

      __NativeFileLogger.init(path, requestedBytes);

      this.initialized = true;
    } catch (e) {
      this.initialized = false;
    }
  };

  // appends to the file a line "call timestamp: message_ts"
  static logMessageTs = (ts: string, status: LogStatus = LogStatus.ARRIVED) => {
    try {
      if (typeof ts !== 'string' || ts.length !== 26) {
        throw new Error(
          'FileLogger log failed: ts must be a string of length 26',
        );
      }

      if (!this.initialized) {
        throw new Error(
          'FileMessagesLogger is not initialized. Call FileMessagesLogger.init() first.',
        );
      }

      __NativeFileLogger.append(ts, status);
    } catch (e) {
      //
    }
  };

  static getMeta = (): LogMeta => {
    try {
      return __NativeFileLogger.get_meta();
    } catch (e) {
      return {
        cursor: 0,
        capacity: 0,
      };
    }
  };

  static getFilePath = () => {
    const baseDir = RNFetchBlob.fs.dirs.DocumentDir;
    return `${baseDir}/${LOG_DIR_NAME}/${MESSAGES_LOG_FILE_NAME}`;
  };

  static getInfo = (): LogInfo => {
    return {
      filePath: this.getFilePath(),
      meta: this.getMeta(),
    };
  };

  static clear = () => {
    try {
      if (!this.initialized) {
        throw new Error(
          'FileMessagesLogger is not initialized. Call FileMessagesLogger.init() first.',
        );
      }

      __NativeFileLogger.clear();
    } catch (e) {
      //
    }
  };

  static isEmpty = () => {
    try {
      if (!this.initialized) {
        throw new Error(
          'FileMessagesLogger is not initialized. Call FileMessagesLogger.init() first.',
        );
      }

      return __NativeFileLogger.is_empty();
    } catch (e) {
      return true;
    }
  };

  /**
   * Sends the logs to the server and clears the log file.
   * it blocks new log entries from being written while sending, and unblocks after sending is complete.
   * All pending logs wait for the sending to complete before being logged, to ensure that no logs are lost during the sending process.
   */
  private static sendLogs = async () => {
    if (this.isEmpty() || this.isSending) {
      return;
    }

    this.isSending = true;

    // Start writing to pending buffer to block new log entries while sending
    FileMessagesLogger.startBuffering();

    try {
      const logInfo = this.getInfo();

      const uri = `file://${logInfo.filePath}`;

      await sendMessagesLogFile({
        uri,
        cursor: logInfo.meta.cursor,
        capacity: logInfo.meta.capacity,
        fileName: MESSAGES_LOG_FILE_NAME,
      });
      this.clear();
      // eslint-disable-next-line no-useless-catch
    } catch (e) {
      // disabled for now
      // sentryLogs.error({
      //   message: 'Failed to send messages log file',
      //   module: 'CHATS',
      //   component: 'FileMessagesLogger',
      //   error: e instanceof Error ? e : undefined,
      // });
      throw e;
    } finally {
      this.isSending = false;
      // Unblock log writes after sending is complete and flush any pending logs to the file
      FileMessagesLogger.stopBufferingAndFlush();
    }
  };

  static sendDailyLogs = () => {
    if (__DEV__ || IS_DEV) {
      return;
    }

    if (!isFeatureFlagEnabled('CHATS_V2_MESSAGES_LOG')) {
      return;
    }

    const tryToSendLogs = async () => {
      try {
        const lastSendTs = store.getNumber(LAST_SEND_TS_KEY);
        const now = Date.now();

        if (lastSendTs === undefined || now - lastSendTs >= ONE_DAY_MS) {
          await this.sendLogs();
          store.set(LAST_SEND_TS_KEY, now);
        }
      } catch (e) {
        //
      }
    };

    tryToSendLogs();

    this.cleanup();

    this.listener = AppState.addEventListener('change', state => {
      if (state === 'active') {
        tryToSendLogs();
      }
    });
  };

  static cleanup = () => {
    if (this.listener) {
      this.listener.remove();
      this.listener = null;
    }
  };

  private static startBuffering = () => {
    try {
      __NativeFileLogger.start_buffering();
    } catch {
      //
    }
  };

  private static stopBufferingAndFlush = () => {
    try {
      __NativeFileLogger.stop_buffering_and_flush();
    } catch {
      //
    }
  };
}
