import {
  makeAutoObservable,
  runInAction,
} from 'mobx';

import { rpc } from '../api/request';
import {
  ArgsChannelSendMessageInput,
  ShmChannelMessageType,
  ShmChannelType,
} from '../api/types';
import { IS_DEV } from '../config';
import newMessageSound from '../new.mp3';
import store from './store';

/** Максимальная длина текста сообщения (`String.length`, UTF-16). */
export const MAX_MESSAGE_TEXT_LENGTH = 4096;

const MESSAGES_POLL_FOCUSED_MS = 2500;
const MESSAGES_POLL_HIDDEN_MS = 10000;
const newMessageAudio = new Audio(newMessageSound);

/**
 * Локальное расширение API-типа сообщения. Используется только в клиентском
 * стейте для оптимистического рендера: сразу показываем bubble с часами,
 * после ответа сервера заменяем на реальное сообщение из `loadMessages`.
 */
export type LocalMessage = ShmChannelMessageType & {
  /** Локальный id вида `tmp-...` — он же присутствует в `_id`. */
  __clientId?: string;
  /** Статус доставки для исходящих. Только для tmp-сообщений. */
  __status?: 'sending' | 'failed';
};

const tmpId = () => 'tmp-' + Math.random().toString(36).slice(2, 10);

/** Id для `clientMessageId` на сервере и склейки с оптимистичным bubble. */
const newClientMessageId = () =>
  typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
    ? crypto.randomUUID()
    : tmpId();

export class ChannelStore {
  channelID: string | undefined = undefined;
  channel: ShmChannelType | null = null;
  joinInChannel: boolean | null = null;

  messagesList: ShmChannelMessageType[] = [];
  /** Оптимистически добавленные сообщения. Не персистятся. */
  pendingMessages: LocalMessage[] = [];
  messagesLoading = false;
  /** >0 пока идёт любой полный channelMessages (loadMessages или догрузка в поллинге); поллинг тикает только при 0. */
  messagesFetchInFlight = 0;
  /**
   * Десктоп: после выбора чата — false, пока не применены channelInfo и первый
   * `channelMessages` для этого id; true — можно переключить другой чат.
   */
  initialChannelSwitchReady = true;
  messagesLastHash: string | null = null;
  loadMessagesPollingInterval: ReturnType<typeof setInterval> | null = null;
  loadMessagesScrollCallback: (() => void) | null = null;
  visibilityHandler: (() => void) | null = null;

  replyMsg: ShmChannelMessageType | null = null;
  sending = false;
  messageText = '';
  recording = false;
  /** После отправки сообщения — скролл вниз после прихода `loadMessages`. */
  pendingScrollToBottom = false;

  constructor() {
    makeAutoObservable(this);
  }

  get pinnedMessage(): ShmChannelMessageType | null {
    const pinned = this.messagesList?.filter((e) => e.pinned?.enabled);
    return pinned?.[pinned.length - 1] ?? null;
  }

  /**
   * Сообщения для рендера: то, что пришло с сервера, + те, что мы только что
   * отправили и ещё ждём подтверждения. Дедуп по `_id` и по `clientMessageId`,
   * если poll/`loadMessages` уже вернул наше сообщение с тем же clientMessageId.
   */
  get displayMessages(): LocalMessage[] {
    if (!this.pendingMessages.length) return this.messagesList;
    const seenIds = new Set(
      this.messagesList.map((m) => m._id).filter(Boolean) as string[]
    );
    const seenClientIds = new Set(
      this.messagesList
        .map((m) => m.clientMessageId)
        .filter((id): id is string => typeof id === 'string' && id.length > 0)
    );
    return [
      ...this.messagesList,
      ...this.pendingMessages.filter(
        (m) =>
          m._id &&
          !seenIds.has(m._id) &&
          !(
            m.clientMessageId &&
            seenClientIds.has(m.clientMessageId)
          )
      ),
    ];
  }

  openChannel(channelID: string | undefined) {
    this.channelID = channelID;
    this.channel = null;
    this.joinInChannel = null;
    this.messagesList = [];
    this.pendingMessages = [];
    this.messagesLoading = false;
    this.messagesFetchInFlight = 0;
    this.replyMsg = null;
    this.pendingScrollToBottom = false;

    if (this.loadMessagesPollingInterval) {
      clearInterval(this.loadMessagesPollingInterval);
      this.loadMessagesPollingInterval = null;
    }

    if (channelID) {
      this.initialChannelSwitchReady = false;
      this.refreshChannel();
    } else {
      this.initialChannelSwitchReady = true;
    }
  }

  refreshChannel() {
    this.channel = null;
    this.joinInChannel = null;
    if (!this.channelID) return;

    const requestedId = this.channelID;
    rpc.channelInfo({ channel: requestedId })
      .then(({ result: data }) => {
        runInAction(() => {
          if (this.channelID !== requestedId) return;
          if (data) {
            this.channel = data;
            const isMember = data?.members?.find(
              (member) => member.owner == store.userID
            );
            this.joinInChannel = !!isMember;
          } else {
            this.initialChannelSwitchReady = true;
          }
        });
      })
      .catch(() => {
        runInAction(() => {
          if (this.channelID === requestedId) {
            this.initialChannelSwitchReady = true;
          }
        });
      });
  }

  updateChannelInfo() {
    if (!this.channelID) return;

    rpc.channelInfo({ channel: this.channelID }).then(({ result: data }) => {
      runInAction(() => {
        if (data) {
          this.channel = data;
          const isMember = data?.members?.find(
            (member) => member.owner == store.userID
          );
          this.joinInChannel = !!isMember;
        }
      });
    });
  }



  /**
   * Убрать из `pendingMessages` записи, для которых в ленте уже есть сообщение
   * с тем же `clientMessageId`. Возвращает true, если что-то сняли (для скролла).
   */
  private prunePendingSyncedWithServer(): boolean {
    const ids = new Set(
      this.messagesList
        .map((m) => m.clientMessageId)
        .filter((id): id is string => typeof id === 'string' && id.length > 0)
    );
    if (!ids.size) return false;
    const prevLen = this.pendingMessages.length;
    this.pendingMessages = this.pendingMessages.filter(
      (m) => !m.clientMessageId || !ids.has(m.clientMessageId)
    );
    return this.pendingMessages.length < prevLen;
  }

  loadMessages(): Promise<void> {
    if (!this.channel?._id) return Promise.resolve();

    const requestedChannelId = this.channel._id;
    const isInitialLoad = this.messagesList.length === 0;
    if (isInitialLoad) {
      this.messagesLoading = true;
    }

    runInAction(() => {
      this.messagesFetchInFlight++;
    });

    return rpc
      .channelMessages(
        { channel: requestedChannelId, limit: 50, offset: 0 },
        ['shmChannelMessages']
      )
      .then(({ result: json }) => {
        runInAction(() => {
          if (
            this.channelID !== requestedChannelId ||
            this.channel?._id !== requestedChannelId
          ) {
            return;
          }
          this.messagesList = json?.data ?? [];
          this.messagesLastHash = json?.hash ?? null;
          this.messagesLoading = false;
          if (this.prunePendingSyncedWithServer()) {
            this.pendingScrollToBottom = true;
          }
        });
      })
      .catch(() => {
        runInAction(() => {
          if (
            this.channelID !== requestedChannelId ||
            this.channel?._id !== requestedChannelId
          ) {
            return;
          }
          this.messagesLoading = false;
          this.pendingScrollToBottom = false;
        });
      })
      .finally(() => {
        runInAction(() => {
          this.messagesFetchInFlight--;
          if (this.channelID === requestedChannelId) {
            this.initialChannelSwitchReady = true;
          }
        });
      });
  }

  private pollTick(scrollToBottom?: () => void) {
    if (this.messagesFetchInFlight > 0) return;
    if (!this.channel?._id) return;

    rpc
      .channelMessages({ channel: this.channel._id, limit: 50, offset: 0, onlyHash: true })
      .then(({ result: newValue }) => {
        if (IS_DEV) {
          console.log('POLLING hash:', newValue?.hash, 'last:', this.messagesLastHash);
        }
        if (newValue?.hash != null && newValue.hash !== this.messagesLastHash) {
          this.updateChannelInfo();
          runInAction(() => {
            this.messagesFetchInFlight++;
          });
          rpc
            .channelMessages(
              { channel: this.channel!._id!, limit: 50, offset: 0 },
              ['shmChannelMessages']
            )
            .then(({ result: json }) => {
              runInAction(() => {
                const prevLastId = this.messagesList[this.messagesList.length - 1]?._id;
                const newList = json?.data ?? [];
                const newLastMessage = newList[newList.length - 1];
                const newLastId = newLastMessage?._id;
                if (prevLastId && newLastId && prevLastId !== newLastId) {
                  scrollToBottom?.();
                  // Звук только когда вкладка не активна / окно неактивно.
                  const isHidden = typeof document !== 'undefined' && document.hidden;
                  if (newLastMessage?.owner != store.userID && isHidden) {
                    newMessageAudio.play().catch(() => {});
                  }
                }
                this.messagesList = newList;
                this.messagesLastHash = json?.hash ?? null;
                if (this.prunePendingSyncedWithServer()) {
                  this.pendingScrollToBottom = true;
                }
              });
            })
            .finally(() => {
              runInAction(() => {
                this.messagesFetchInFlight--;
              });
            });
        }
      });
  }

  startMessagesPolling(scrollToBottom?: () => void) {
    if (!this.channel?._id) return;

    this.stopMessagesPolling();
    this.loadMessagesScrollCallback = scrollToBottom ?? null;

    const schedule = () => {
      this.stopMessagesPolling();
      const interval =
        typeof document !== 'undefined' && document.hidden
          ? MESSAGES_POLL_HIDDEN_MS
          : MESSAGES_POLL_FOCUSED_MS;
      this.loadMessagesPollingInterval = setInterval(() => {
        this.pollTick(this.loadMessagesScrollCallback ?? undefined);
      }, interval);
    };

    schedule();

    if (typeof document !== 'undefined') {
      const handler = () => {
        // На возвращении в фокус — мгновенный тик и перестроение интервала.
        if (!document.hidden) {
          this.pollTick(this.loadMessagesScrollCallback ?? undefined);
        }
        schedule();
      };
      this.visibilityHandler = handler;
      document.addEventListener('visibilitychange', handler);
    }
  }

  stopMessagesPolling() {
    if (this.loadMessagesPollingInterval) {
      clearInterval(this.loadMessagesPollingInterval);
      this.loadMessagesPollingInterval = null;
    }
    if (this.visibilityHandler && typeof document !== 'undefined') {
      document.removeEventListener('visibilitychange', this.visibilityHandler);
      this.visibilityHandler = null;
    }
  }

  setJoinInChannel(value: boolean) {
    this.joinInChannel = value;
  }

  setReplyMsg(msg: ShmChannelMessageType | null) {
    this.replyMsg = msg;
  }

  setMessageText(text: string) {
    this.messageText =
      text.length > MAX_MESSAGE_TEXT_LENGTH
        ? text.slice(0, MAX_MESSAGE_TEXT_LENGTH)
        : text;
  }

  setRecording(value: boolean) {
    this.recording = value;
  }

  setSending(value: boolean) {
    this.sending = value;
  }

  clearPendingScrollToBottom() {
    this.pendingScrollToBottom = false;
  }

  /** Удалить оптимистическое сообщение по локальному id. */
  removePending(clientId: string) {
    this.pendingMessages = this.pendingMessages.filter(
      (m) => m.__clientId !== clientId
    );
  }

  /** Пометить оптимистическое сообщение как «не доставлено» (для повторной отправки). */
  markPendingFailed(clientId: string) {
    const item = this.pendingMessages.find((m) => m.__clientId === clientId);
    if (item) item.__status = 'failed';
  }

  /** Повторная отправка не доставленного оптимистического сообщения. */
  retryPending(clientId: string) {
    const item = this.pendingMessages.find((m) => m.__clientId === clientId);
    if (!item || !this.channel?._id) return;
    const resendId = item.clientMessageId ?? newClientMessageId();
    if (!item.clientMessageId) {
      runInAction(() => {
        item.clientMessageId = resendId;
      });
    }
    item.__status = 'sending';
    const text =
      typeof item.text === 'string' && item.text.length > MAX_MESSAGE_TEXT_LENGTH
        ? item.text.slice(0, MAX_MESSAGE_TEXT_LENGTH)
        : item.text;
    rpc
      .channelSendMessage({
        channel: this.channel._id,
        clientMessageId: resendId,
        text,
        reply: item.reply,
      })
      .then(() => this.loadMessages())
      .then(() => {
        runInAction(() => {
          if (this.pendingMessages.some((m) => m.__clientId === clientId)) {
            this.removePending(clientId);
          }
          this.pendingScrollToBottom = true;
        });
      })
      .catch(() => {
        runInAction(() => this.markPendingFailed(clientId));
      });
  }

  async sendMessage(params: ArgsChannelSendMessageInput) {
    if (!this.channel?._id) return;

    if (
      typeof params.text === 'string' &&
      params.text.length > MAX_MESSAGE_TEXT_LENGTH
    ) {
      params.text = params.text.slice(0, MAX_MESSAGE_TEXT_LENGTH);
    }

    params.channel = this.channel._id;
    if (this.replyMsg?._id) {
      params.reply = this.replyMsg._id;
    }

    const clientMessageId = newClientMessageId();
    const clientId = clientMessageId;
    const optimistic: LocalMessage = {
      _id: clientId,
      __clientId: clientId,
      clientMessageId,
      __status: 'sending',
      channel: this.channel._id,
      owner: store.userID ?? undefined,
      text: params.text,
      reply: params.reply,
      attachments: params.attachments as any,
      voice: params.voice as any,
      type: 'regular',
      created_at: Math.floor(Date.now() / 1000),
      viewed: false,
    };

    runInAction(() => {
      this.pendingMessages.push(optimistic);
      if (this.replyMsg) this.replyMsg = null;
      if (params.text) this.messageText = '';
      this.pendingScrollToBottom = true;
      this.sending = true;
    });

    try {
      params.clientMessageId = clientMessageId;
      await rpc.channelSendMessage(params);
      await this.loadMessages();
      runInAction(() => {
        if (this.pendingMessages.some((m) => m.__clientId === clientId)) {
          this.removePending(clientId);
        }
      });
    } catch (e) {
      runInAction(() => {
        this.markPendingFailed(clientId);
      });
    } finally {
      runInAction(() => {
        this.sending = false;
      });
    }
  }

  resetCompose() {
    this.replyMsg = null;
    this.messageText = '';
  }
}

export const channelStore = new ChannelStore();
