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_PAGE_LIMIT = 50;
const MESSAGES_POLL_FOCUSED_MS = 2500;
const MESSAGES_POLL_HIDDEN_MS = 10000;
const newMessageAudio = new Audio(newMessageSound);

/**
 * Локальное расширение API-типа сообщения. Используется только в клиентском
 * стейте для оптимистического рендера: сразу показываем bubble с часами,
 * после ответа сервера подставляем сообщение из `channelSendMessage`.
 */
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;
  /** После отправки сообщения — скролл вниз после подстановки ответа send. */
  pendingScrollToBottom = false;

  constructor() {
    makeAutoObservable(this);
  }

  get pinnedMessage(): ShmChannelMessageType | null {
    const pinnedId = this.channel?.pinnedMessageId;
    if (pinnedId) {
      const byId = this.messagesList?.find((m) => m._id === pinnedId);
      if (byId?.pinned?.enabled) return byId;
    }
    return this.messagesList?.find((e) => e.pinned?.enabled) ?? 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;
        }
      });
    });
  }

  /** Сброс unread и mark viewed — отдельно от загрузки сообщений. */
  markChannelRead(lastReadMessageId?: string): Promise<void> {
    if (!this.channel?._id) return Promise.resolve();

    const channelId = this.channel._id;
    const params: { channel: string; lastReadMessageId?: string } = {
      channel: channelId,
    };
    if (lastReadMessageId) {
      params.lastReadMessageId = lastReadMessageId;
    }

    return rpc.channelMarkRead(params).then(() => {
      runInAction(() => {
        if (this.channel?._id !== channelId || !store.userID) return;
        this.channel = {
          ...this.channel,
          unreadCounts: {
            ...(this.channel.unreadCounts ?? {}),
            [store.userID]: 0,
          },
        };
      });
    });
  }

  private afterMessagesLoaded(requestedChannelId: string, messages: ShmChannelMessageType[]) {
    const lastIncoming = [...messages]
      .reverse()
      .find((m) => m.owner != store.userID && m._id);
    if (lastIncoming?._id) {
      return this.markChannelRead(lastIncoming._id);
    }
    return this.markChannelRead();
  }

  /**
   * Убрать из `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;
  }

  /** Подставить сообщение из ответа `channelSendMessage` без полного reload. */
  private mergeSentMessage(serverMessage: ShmChannelMessageType | null | undefined) {
    if (!serverMessage?._id || !this.channel?._id) return;
    if (serverMessage.channel && serverMessage.channel !== this.channel._id) return;

    let list = [...this.messagesList];
    const byId = list.findIndex((m) => m._id === serverMessage._id);
    const byClientId =
      serverMessage.clientMessageId != null && serverMessage.clientMessageId !== ''
        ? list.findIndex((m) => m.clientMessageId === serverMessage.clientMessageId)
        : -1;
    const idx = byId >= 0 ? byId : byClientId;

    if (idx >= 0) {
      list[idx] = serverMessage;
    } else {
      list.push(serverMessage);
      if (list.length > MESSAGES_PAGE_LIMIT) {
        list = list.slice(-MESSAGES_PAGE_LIMIT);
      }
    }

    this.messagesList = list;
    if (this.prunePendingSyncedWithServer()) {
      this.pendingScrollToBottom = true;
    }
  }

  /** Синхронизировать hash ленты после локального merge (лёгкий onlyHash). */
  private syncMessagesHash() {
    if (!this.channel?._id) return;
    const channelId = this.channel._id;
    rpc
      .channelMessages({ channel: channelId, onlyHash: true })
      .then(({ result }) => {
        runInAction(() => {
          if (this.channel?._id === channelId) {
            this.messagesLastHash = result?.hash ?? null;
          }
        });
      })
      .catch(() => {});
  }

  private confirmSentMessage(
    serverMessage: ShmChannelMessageType | null | undefined,
    pendingClientId?: string
  ) {
    this.mergeSentMessage(serverMessage);
    if (
      pendingClientId &&
      this.pendingMessages.some((m) => m.__clientId === pendingClientId)
    ) {
      this.removePending(pendingClientId);
    }
    this.pendingScrollToBottom = true;
    void this.markChannelRead();
    this.syncMessagesHash();
  }

  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;
          }
        });
        return this.afterMessagesLoaded(requestedChannelId, json?.data ?? []);
      })
      .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 }) => {
              const requestedChannelId = this.channel!._id!;
              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;
                }
              });
              return this.afterMessagesLoaded(requestedChannelId, json?.data ?? []);
            })
            .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,
        },
        ['shmChannelMessages']
      )
      .then(({ result }) => {
        runInAction(() => {
          this.confirmSentMessage(result, clientId);
        });
      })
      .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;
      const { result: serverMessage } = await rpc.channelSendMessage(
        params,
        ['shmChannelMessages']
      );
      runInAction(() => {
        this.confirmSentMessage(serverMessage, clientId);
      });
    } catch (e) {
      runInAction(() => {
        this.markPendingFailed(clientId);
      });
    } finally {
      runInAction(() => {
        this.sending = false;
      });
    }
  }

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

export const channelStore = new ChannelStore();
