import {
  createContext,
  useMemo,
  useEffect,
  useCallback,
  useState,
  useContext,
  type ReactNode,
  useRef,
} from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useRouterState } from "@tanstack/react-router";
import type { InfiniteData } from "@tanstack/react-query";
import type { LiveMessage, RawMessage, RawMessages } from "./values";
import {
  deriveChatId,
  deriveChatSecretId,
  decryptChatText,
  kemPublicKeyFromPublicKeyBundle,
  ownKemPublicKeyFromKeyring,
  randomChatSecret,
  unwrapChatSecret,
  wrapChatSecret,
} from "@tensamin/crypto/chatSecret";
import { useStorage } from "@tensamin/storage/context";
import { requireRelaySuccess, useMTP } from "@tensamin/mtp";
import { log, toast } from "@tensamin/shared/log";
import { useSession } from "@tensamin/identity/session";
import { useUser } from "@tensamin/identity/context";
import { createCache, type ChatDraft } from "@tensamin/cache";
import { secureValueCodec } from "@tensamin/storage/secure";

export const context = createContext<contextType | undefined>(undefined);

const queryClient = new QueryClient();
const CHAT_SECRET_VERSION = 1;

function bytesFromProtocol(value: unknown): Uint8Array {
  if (value instanceof Uint8Array) return value;
  if (value instanceof ArrayBuffer) return new Uint8Array(value);
  if (ArrayBuffer.isView(value)) {
    return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
  }
  if (Array.isArray(value)) return new Uint8Array(value);
  if (typeof value === "string") {
    const bin = atob(value);
    const out = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
    return out;
  }
  if (typeof value === "object" && value !== null) {
    const entries = Object.entries(value);
    if (
      entries.every(
        ([key, item]) => /^\d+$/.test(key) && typeof item === "number",
      )
    ) {
      return new Uint8Array(entries.map(([, item]) => item as number));
    }
  }
  throw new Error("expected protocol bytes");
}

type EditableMessage = RawMessage & { failed?: boolean };
type MessageEdit = Partial<
  Pick<
    EditableMessage,
    "Content" | "Edited" | "MessageState" | "Reactions" | "failed"
  >
>;

const messageStateRank: Record<RawMessage["MessageState"], number> = {
  awaiting: 0,
  sending: 0,
  sent: 1,
  received: 2,
  read: 3,
};

function updateMessagesBySendTime<T extends EditableMessage>(
  messages: T[],
  sendTime: number,
  edit: MessageEdit,
): { next: T[]; updated: boolean } {
  let updated = false;

  const next = messages.map((item) => {
    if (item.SendTime !== sendTime) {
      return item;
    }

    if (
      edit.MessageState !== undefined &&
      messageStateRank[edit.MessageState] < messageStateRank[item.MessageState]
    ) {
      return item;
    }

    const entries = Object.entries(edit) as Array<
      [keyof MessageEdit, MessageEdit[keyof MessageEdit]]
    >;

    if (entries.every(([key, value]) => item[key] === value)) {
      return item;
    }

    updated = true;
    return {
      ...item,
      ...edit,
    };
  });

  return {
    next,
    updated,
  };
}

function updateMessageReaction<T extends EditableMessage>(
  message: T,
  reaction: string,
  senderId: number,
  accepted: boolean,
): T {
  const current = message.Reactions ?? [];
  const withoutReaction = current.filter(
    (item) => item.Reaction !== reaction || item.SenderId !== senderId,
  );
  const next = accepted
    ? [...withoutReaction, { Reaction: reaction, SenderId: senderId }]
    : withoutReaction;

  if (
    next.length === current.length &&
    next.every(
      (item, index) =>
        item.Reaction === current[index]?.Reaction &&
        item.SenderId === current[index]?.SenderId,
    )
  ) {
    return message;
  }

  return { ...message, Reactions: next };
}

function assertProtocolSuccess(type: string, response: { type: string }) {
  if (response.type.startsWith("Error")) {
    throw new Error(`${type} failed: ${response.type}`);
  }
}

function protocolBytes(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
  return new Uint8Array(bytes);
}

type SendMessageGet = (
  type: "MessageGet",
  data: { SendTime: number },
) => Promise<{ data: RawMessage }>;

type StoredDraftState = ChatDraft & {
  accountId: number;
  userId: number;
  loaded: boolean;
  revision: number;
};

function draftKey(accountId: number, userId: number) {
  return `${accountId}:${userId}`;
}

export async function getMessage({
  sendTime,
  ownId,
  chatPartnerId,
  send,
}: {
  sendTime: number;
  ownId: number;
  chatPartnerId: number;
  send: SendMessageGet;
}): Promise<RawMessage> {
  const cache = createCache(String(ownId), {
    codec: secureValueCodec,
  });
  const window = await cache.conversations.get(chatPartnerId);
  const cachedMessage = window?.Messages.find(
    (message) => message.SendTime === sendTime,
  );
  const message =
    cachedMessage ??
    (
      await send("MessageGet", {
        SendTime: sendTime,
      })
    ).data;
  const messageChatPartnerId =
    message.SenderId === ownId ? chatPartnerId : message.SenderId;

  if (messageChatPartnerId !== chatPartnerId) {
    throw new Error("Reply message does not belong to this chat");
  }

  return message;
}

export async function fetchReplyMessage({
  replyTo,
  ownId,
  chatUserId,
  send,
  getChatSecret,
}: {
  replyTo: number;
  ownId: number;
  chatUserId: number;
  send: SendMessageGet;
  getChatSecret: (userId: number) => Promise<Uint8Array | null>;
}) {
  const message = await getMessage({
    sendTime: replyTo,
    ownId,
    chatPartnerId: chatUserId,
    send,
  });
  const chatSecret = await getChatSecret(chatUserId);

  if (!chatSecret) {
    throw new Error("Missing chat secret");
  }

  const decryptedContent = await decryptChatText(chatSecret, message.Content);

  return {
    ...message,
    Content: decryptedContent,
  };
}

export default function Provider({ children }: { children: ReactNode }) {
  const { load } = useStorage();
  const { send, sendSealedRelay, subscribe } = useMTP();
  const { get: getUser, getIota } = useUser();
  const { moveUserIdToTop } = useSession();

  const [error, setError] = useState("");
  const [errorDescription, setErrorDescription] = useState("");

  const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]);
  const [ownId, setOwnId] = useState(0);
  const [drafts, setDrafts] = useState<Record<string, StoredDraftState>>({});
  const draftsRef = useRef<Record<string, StoredDraftState>>({});
  const loadingDraftsRef = useRef(new Set<string>());
  const draftWriteQueuesRef = useRef(new Map<string, Promise<void>>());
  const [currentChatSecretState, setCurrentChatSecretState] = useState<{
    userId: number;
    value: Uint8Array | null;
  }>({
    userId: 0,
    value: null,
  });

  const inputBoxRef = useRef<HTMLDivElement>(null);

  const locationSearch = useRouterState({
    select: (state) => state.location.search,
  });

  // User ID compare to clear shared secret
  const userIdValue = useMemo(() => {
    const rawId = (locationSearch as unknown as { id?: unknown })?.id;
    return Number(rawId ?? 0);
  }, [locationSearch]);

  const currentChatSecret = useMemo(() => {
    if (currentChatSecretState.userId !== userIdValue) {
      return null;
    }

    return currentChatSecretState.value;
  }, [currentChatSecretState, userIdValue]);

  useEffect(() => {
    load("user_id").then(setOwnId);
  }, [load]);

  const persistDraft = useCallback(
    (key: string, accountId: number, userId: number, draft: ChatDraft) => {
      const previous =
        draftWriteQueuesRef.current.get(key) ?? Promise.resolve();
      const next = previous
        .catch(() => undefined)
        .then(async () => {
          const cache = createCache(String(accountId), {
            codec: secureValueCodec,
          });
          if (draft.Content === "" && draft.ReplyId === undefined) {
            await cache.drafts.delete(userId);
          } else {
            await cache.drafts.put(userId, draft);
          }
        })
        .catch((err) => {
          log(1, "chat", "red", "Failed to cache chat draft", err);
        });
      draftWriteQueuesRef.current.set(key, next);
    },
    [],
  );

  const updateDraft = useCallback(
    (
      accountId: number,
      userId: number,
      update: (current: ChatDraft) => ChatDraft,
    ) => {
      const key = draftKey(accountId, userId);
      const current = draftsRef.current[key] ?? {
        accountId,
        userId,
        Content: "",
        loaded: false,
        revision: 0,
      };
      const changed = update(current);
      const next: StoredDraftState = {
        ...current,
        ...changed,
        revision: current.revision + 1,
      };
      const nextDrafts = { ...draftsRef.current, [key]: next };
      draftsRef.current = nextDrafts;
      setDrafts(nextDrafts);

      if (next.loaded) {
        persistDraft(key, accountId, userId, {
          Content: next.Content,
          ReplyId: next.ReplyId,
        });
      }
    },
    [persistDraft],
  );

  useEffect(() => {
    if (
      !Number.isSafeInteger(ownId) ||
      ownId <= 0 ||
      !Number.isSafeInteger(userIdValue) ||
      userIdValue <= 0
    ) {
      return;
    }

    const key = draftKey(ownId, userIdValue);
    if (draftsRef.current[key]?.loaded || loadingDraftsRef.current.has(key)) {
      return;
    }
    loadingDraftsRef.current.add(key);

    void (async () => {
      let stored: ChatDraft | undefined;
      try {
        stored = await createCache(String(ownId), {
          codec: secureValueCodec,
        }).drafts.get(userIdValue);
      } catch (err) {
        log(1, "chat", "red", "Failed to restore chat draft", err);
      } finally {
        const current = draftsRef.current[key];
        const next: StoredDraftState =
          current && current.revision > 0
            ? { ...current, loaded: true }
            : {
                accountId: ownId,
                userId: userIdValue,
                Content: stored?.Content ?? "",
                ReplyId: stored?.ReplyId,
                loaded: true,
                revision: 0,
              };
        const nextDrafts = { ...draftsRef.current, [key]: next };
        draftsRef.current = nextDrafts;
        setDrafts(nextDrafts);
        loadingDraftsRef.current.delete(key);

        if (next.revision > 0) {
          persistDraft(key, ownId, userIdValue, {
            Content: next.Content,
            ReplyId: next.ReplyId,
          });
        }
      }
    })();
  }, [ownId, persistDraft, userIdValue]);

  const activeDraftKey =
    ownId > 0 && userIdValue > 0 ? draftKey(ownId, userIdValue) : undefined;
  const activeDraft = activeDraftKey ? drafts[activeDraftKey] : undefined;
  const composerValue = activeDraft?.Content ?? "";
  const replyTo = activeDraft?.ReplyId;
  const setComposerValue = useCallback(
    (value: string) => {
      if (ownId <= 0 || userIdValue <= 0) return;
      updateDraft(ownId, userIdValue, (current) => ({
        ...current,
        Content: value,
      }));
    },
    [ownId, updateDraft, userIdValue],
  );
  const setReplyTo = useCallback(
    (value: number | undefined) => {
      if (ownId <= 0 || userIdValue <= 0) return;
      updateDraft(ownId, userIdValue, (current) => ({
        ...current,
        ReplyId: value,
      }));
    },
    [ownId, updateDraft, userIdValue],
  );

  useEffect(() => {
    if (!userIdValue) return;

    let active = true;

    void (async () => {
      try {
        setError("");
        setErrorDescription("");

        const ownUserId = Number(await load("user_id"));
        const keyring = await load("mtp_keyring");
        const chatId = deriveChatId(ownUserId, userIdValue);
        const secretId = deriveChatSecretId(chatId);

        const existing = await send("GetChatSecret", {
          UserId: String(ownUserId),
          ChatId: chatId,
          SecretId: secretId,
        });

        if (!existing.type.startsWith("Error")) {
          const data = existing.data as Record<string, unknown>;
          const secret = await unwrapChatSecret({
            encryptedSecret: bytesFromProtocol(data.EncryptedSecret),
            kemCiphertext: bytesFromProtocol(data.KemCiphertext),
            keyring,
            chatId: String(data.ChatId),
            secretId: String(data.SecretId),
            version: Number(data.VersionNumber),
            wrappingScheme: String(data.WrappingScheme),
          });

          if (active) {
            setCurrentChatSecretState({ userId: userIdValue, value: secret });
          }
          return;
        }

        if (existing.type !== "ErrorNotSet") {
          throw new Error(`GetChatSecret failed: ${existing.type}`);
        }

        const rawSecret = randomChatSecret();
        const ownWrapped = await wrapChatSecret({
          chatSecret: rawSecret,
          recipientKemPublicKey: ownKemPublicKeyFromKeyring(keyring),
          chatId,
          secretId,
          version: CHAT_SECRET_VERSION,
        });
        const peerUser = await getUser(userIdValue, ["PublicKey"]);
        const peerWrapped = await wrapChatSecret({
          chatSecret: rawSecret,
          recipientKemPublicKey: kemPublicKeyFromPublicKeyBundle(
            peerUser.PublicKey,
          ),
          chatId,
          secretId,
          version: CHAT_SECRET_VERSION,
        });

        const ownIota = await getIota(ownUserId);
        const peerIota = await getIota(userIdValue);
        const response = await sendSealedRelay(
          "SetChatSecret",
          {
            ChatId: chatId,
            SecretId: secretId,
            VersionNumber: CHAT_SECRET_VERSION,
            WrappingScheme: ownWrapped.wrappingScheme,
            CreatedAt: Date.now(),
            Recipients: [
              {
                UserId: String(ownUserId),
                EncryptedSecret: protocolBytes(ownWrapped.encryptedSecret),
                KemCiphertext: protocolBytes(ownWrapped.kemCiphertext),
              },
              {
                UserId: String(userIdValue),
                EncryptedSecret: protocolBytes(peerWrapped.encryptedSecret),
                KemCiphertext: protocolBytes(peerWrapped.kemCiphertext),
              },
            ],
          },
          {
            nextHop: { kind: "iota", id: ownIota.IotaId },
            finalRecipientId: userIdValue,
            metadataRecipients: [
              { value: ownIota.PublicKey, encoding: "base64" },
              { value: peerIota.PublicKey, encoding: "base64" },
            ],
            contentRecipients: [
              { value: ownIota.PublicKey, encoding: "base64" },
              { value: peerIota.PublicKey, encoding: "base64" },
            ],
          },
        );
        requireRelaySuccess(response);

        if (active) {
          setCurrentChatSecretState({ userId: userIdValue, value: rawSecret });
        }
      } catch (err) {
        log(1, "chat", "red", "Failed to initialize chat secret", err);
        if (active) {
          setError(err instanceof Error ? err.name : "Unknown Error");
          setErrorDescription(err instanceof Error ? err.message : String(err));
          setCurrentChatSecretState({ userId: userIdValue, value: null });
        }
      }
    })();

    return () => {
      active = false;
    };
  }, [getIota, getUser, load, send, sendSealedRelay, userIdValue]);

  const getChatSecret = useCallback(
    async (userId: number): Promise<Uint8Array | null> => {
      if (!Number.isSafeInteger(userId) || userId <= 0) {
        throw new Error("Invalid chat user id");
      }

      const ownUserId = Number(await load("user_id"));
      const keyring = await load("mtp_keyring");
      const chatId = deriveChatId(ownUserId, userId);
      const secretId = deriveChatSecretId(chatId);

      const response = await send("GetChatSecret", {
        UserId: String(ownUserId),
        ChatId: chatId,
        SecretId: secretId,
      });

      if (response.type === "ErrorNotSet") {
        return null;
      }

      if (response.type.startsWith("Error")) {
        throw new Error(`GetChatSecret failed: ${response.type}`);
      }

      const data = response.data as Record<string, unknown>;
      return await unwrapChatSecret({
        encryptedSecret: bytesFromProtocol(data.EncryptedSecret),
        kemCiphertext: bytesFromProtocol(data.KemCiphertext),
        keyring,
        chatId: String(data.ChatId),
        secretId: String(data.SecretId),
        version: Number(data.VersionNumber),
        wrappingScheme: String(data.WrappingScheme),
      });
    },
    [load, send],
  );

  const decryptMessages = useCallback(
    async (messages: RawMessages) => {
      if (!currentChatSecret) return [];
      return Promise.all(
        messages.map(async (message) => {
          try {
            return {
              ...message,
              Content: await decryptChatText(
                currentChatSecret,
                message.Content,
              ),
            };
          } catch (err) {
            log(1, "chat", "red", "Failed to decrypt historical message", err, {
              SendTime: message.SendTime,
            });
            return {
              ...message,
              Content: "Failed to decrypt message",
              decryptionFailed: true,
            };
          }
        }),
      );
    },
    [currentChatSecret],
  );

  const getCachedMessages = useCallback(async () => {
    if (!ownId || !userIdValue || !currentChatSecret) return [];
    const cache = createCache(String(ownId), {
      codec: secureValueCodec,
    });
    const window = await cache.conversations.get(userIdValue);
    return decryptMessages(window?.Messages ?? []);
  }, [currentChatSecret, decryptMessages, ownId, userIdValue]);

  const getMessages = useCallback(
    async (amount: number, offset: number) => {
      if (!currentChatSecret) {
        return [];
      }

      const messages = await send("MessagesGet", {
        Amount: amount,
        Offset: offset,
        UserId: userIdValue,
      });

      if (messages.type.startsWith("error")) {
        throw new Error(messages.type);
      }

      const rawMessages = messages.data.Messages;
      const sorted = [...rawMessages].sort((a, b) => a.SendTime - b.SendTime);

      if (sorted.length > 0) {
        const fetchedSendTimes = new Set(sorted.map((item) => item.SendTime));

        setLiveMessagesState((prev) => {
          const filtered = prev.filter(
            (liveMessage) => !fetchedSendTimes.has(liveMessage.SendTime),
          );

          return filtered.length === prev.length ? prev : filtered;
        });
      }

      return decryptMessages(sorted);
    },
    [currentChatSecret, decryptMessages, send, userIdValue],
  );

  useEffect(() => {
    if (!currentChatSecret || !ownId || !userIdValue) return;
    const queryKey = ["chat-messages", String(userIdValue), true] as const;
    void getCachedMessages().then((messages) => {
      if (messages.length === 0 || queryClient.getQueryData(queryKey)) return;
      queryClient.setQueryData<InfiniteData<RawMessages>>(queryKey, {
        pages: [messages],
        pageParams: [0],
      });
    });
  }, [currentChatSecret, getCachedMessages, ownId, userIdValue]);

  const editMessage = useCallback(
    (sendTime: number, edit: MessageEdit) => {
      setLiveMessagesState((prev) => {
        const { next, updated } = updateMessagesBySendTime(
          prev,
          sendTime,
          edit,
        );
        return updated ? next : prev;
      });

      const queryKey = [
        "chat-messages",
        String(userIdValue),
        currentChatSecret !== null,
      ] as const;
      queryClient.setQueryData<InfiniteData<RawMessages>>(
        queryKey,
        (current) => {
          if (!current) {
            return current;
          }

          let updated = false;

          const pages = current.pages.map((page) => {
            const nextPage = updateMessagesBySendTime(page, sendTime, edit);

            if (nextPage.updated) {
              updated = true;
            }

            return nextPage.next;
          });

          if (!updated) {
            return current;
          }

          return {
            ...current,
            pages,
          };
        },
      );
    },
    [currentChatSecret, userIdValue],
  );

  const removeMessage = useCallback(
    (sendTime: number) => {
      setLiveMessagesState((prev) =>
        prev.filter((message) => message.SendTime !== sendTime),
      );
      const queryKey = [
        "chat-messages",
        String(userIdValue),
        currentChatSecret !== null,
      ] as const;

      queryClient.setQueryData<InfiniteData<RawMessages>>(
        queryKey,
        (current) => {
          if (!current) {
            return current;
          }

          let updated = false;

          const pages = current.pages.map((page) => {
            const nextPage = page.filter((message) => {
              const keep = message.SendTime !== sendTime;
              if (!keep) {
                updated = true;
              }
              return keep;
            });

            return nextPage;
          });

          if (!updated) {
            return current;
          }

          return {
            ...current,
            pages,
          };
        },
      );
    },
    [currentChatSecret, userIdValue],
  );

  const applyLiveReaction = useCallback(
    (
      sendTime: number,
      reaction: string,
      senderId: number,
      accepted: boolean,
    ) => {
      setLiveMessagesState((current) =>
        current.map((message) =>
          message.SendTime === sendTime
            ? updateMessageReaction(message, reaction, senderId, accepted)
            : message,
        ),
      );

      const queryKey = [
        "chat-messages",
        String(userIdValue),
        currentChatSecret !== null,
      ] as const;
      queryClient.setQueryData<InfiniteData<RawMessages>>(
        queryKey,
        (current) =>
          current
            ? {
                ...current,
                pages: current.pages.map((page) =>
                  page.map((message) =>
                    message.SendTime === sendTime
                      ? updateMessageReaction(
                          message,
                          reaction,
                          senderId,
                          accepted,
                        )
                      : message,
                  ),
                ),
              }
            : current,
      );
    },
    [currentChatSecret, userIdValue],
  );

  const deleteMessage = useCallback(
    async (sendTime: number) => {
      try {
        const response = await send("MessageDelete", {
          ChatPartnerId: userIdValue,
          SendTime: sendTime,
        });

        assertProtocolSuccess("MessageDelete", response);
        removeMessage(sendTime);
      } catch (err) {
        log(1, "chat", "red", "Failed to delete message", err);
        toast("error", "Failed to delete message", String(err));
      }
    },
    [removeMessage, send, userIdValue],
  );

  const setReaction = useCallback(
    async (sendTime: number, reaction: string, add: boolean) => {
      let previousReactions: RawMessage["Reactions"];
      let foundMessage = false;

      const applyOptimisticUpdate = (message: EditableMessage) => {
        const current = message.Reactions ?? [];
        previousReactions = current;
        foundMessage = true;

        return add
          ? [...current, { Reaction: reaction, SenderId: ownId }]
          : current.filter(
              (item) => item.Reaction !== reaction || item.SenderId !== ownId,
            );
      };

      setLiveMessagesState((current) =>
        current.map((message) =>
          message.SendTime === sendTime
            ? { ...message, Reactions: applyOptimisticUpdate(message) }
            : message,
        ),
      );

      const queryKey = [
        "chat-messages",
        String(userIdValue),
        currentChatSecret !== null,
      ] as const;
      queryClient.setQueryData<InfiniteData<RawMessages>>(
        queryKey,
        (current) =>
          current
            ? {
                ...current,
                pages: current.pages.map((page) =>
                  page.map((message) =>
                    message.SendTime === sendTime
                      ? {
                          ...message,
                          Reactions: applyOptimisticUpdate(message),
                        }
                      : message,
                  ),
                ),
              }
            : current,
      );

      try {
        const response = await send(
          add ? "MessageReactionAdd" : "MessageReactionRemove",
          {
            ChatPartnerId: userIdValue,
            Reaction: reaction,
            SendTime: sendTime,
          },
        );
        assertProtocolSuccess(
          add ? "MessageReactionAdd" : "MessageReactionRemove",
          response,
        );
      } catch (err) {
        if (foundMessage) {
          editMessage(sendTime, { Reactions: previousReactions });
        }
        log(1, "chat", "red", "Failed to update reaction", err);
        toast("error", "Failed to update reaction", String(err));
      }
    },
    [currentChatSecret, editMessage, ownId, send, userIdValue],
  );

  const addReaction = useCallback(
    (sendTime: number, reaction: string) =>
      setReaction(sendTime, reaction, true),
    [setReaction],
  );
  const removeReaction = useCallback(
    (sendTime: number, reaction: string) =>
      setReaction(sendTime, reaction, false),
    [setReaction],
  );

  const addLiveMessage = useCallback(
    (message: RawMessage) => {
      const localId =
        globalThis.crypto?.randomUUID?.() ??
        `${Date.now()}-${Math.random().toString(36).slice(2)}`;

      if (message.SenderId !== ownId) {
        moveUserIdToTop(userIdValue);
      }

      setLiveMessagesState((prev) => [
        ...prev,
        {
          ...message,
          localId,
          failed: false,
        },
      ]);

      return {
        setFailed: (failed: boolean) => {
          editMessage(message.SendTime, { failed });
        },
        setMessageState: (MessageState: RawMessage["MessageState"]) => {
          editMessage(message.SendTime, { MessageState });
        },
      };
    },
    [editMessage, userIdValue, moveUserIdToTop, ownId],
  );

  const clearLiveMessages = useCallback(() => {
    setLiveMessagesState([]);
  }, []);

  // Get live updates for message states
  useEffect(() => {
    const unsubscribeEdit = subscribe("MessageEditLive", ({ data }) => {
      if (!currentChatSecret) return;
      if (data.ChatPartnerId !== userIdValue) {
        log(
          3,
          "chat",
          "yellow",
          "Cancel message edit update due to user ID mismatch",
          {
            expected: userIdValue,
            received: data.ChatPartnerId,
          },
        );
        return;
      }

      void decryptChatText(currentChatSecret, data.Content)
        .then((content) => {
          editMessage(data.SendTime, { Content: content, Edited: true });
        })
        .catch((err) => {
          log(1, "chat", "red", "Failed to decrypt message edit", err, {
            SendTime: data.SendTime,
          });
        });
    });
    const unsubscribeReaction = subscribe("MessageReactionLive", ({ data }) => {
      if (data.ChatPartnerId !== userIdValue) return;
      applyLiveReaction(
        data.SendTime,
        data.Reaction,
        data.SenderId,
        data.Accepted,
      );
    });
    const unsubscribeDelete = subscribe("MessageDeleteLive", ({ data }) => {
      if (data.ChatPartnerId !== userIdValue) return;
      removeMessage(data.SendTime);
    });
    const unsubscribeState = subscribe("MessageState", ({ data }) => {
      if (data.ChatPartnerId !== userIdValue) {
        log(
          3,
          "chat",
          "yellow",
          "Cancel message state update due to user ID mismatch",
          {
            expected: userIdValue,
            received: data.ChatPartnerId,
          },
        );
        return;
      }
      editMessage(data.SendTime, { MessageState: data.MessageState });
    });
    return () => {
      unsubscribeEdit();
      unsubscribeReaction();
      unsubscribeDelete();
      unsubscribeState();
    };
  }, [
    currentChatSecret,
    applyLiveReaction,
    editMessage,
    removeMessage,
    subscribe,
    userIdValue,
  ]);

  return (
    <QueryClientProvider client={queryClient}>
      <context.Provider
        value={{
          getMessages,
          getChatSecret,
          liveMessages: () => liveMessagesState,
          addLiveMessage,
          editMessage,
          deleteMessage,
          addReaction,
          removeReaction,
          clearLiveMessages,
          chatSecret: currentChatSecret,
          ownId,
          userId: userIdValue,
          inputBoxRef,
          error,
          errorDescription,
          composerValue,
          setComposerValue,
          replyTo,
          setReplyTo,
        }}
      >
        {children}
      </context.Provider>
    </QueryClientProvider>
  );
}

type contextType = {
  getMessages: (amount: number, offset: number) => Promise<RawMessages>;
  getChatSecret: (userId: number) => Promise<Uint8Array | null>;
  liveMessages: () => LiveMessage[];
  addLiveMessage: (message: RawMessage) => {
    setFailed: (failed: boolean) => void;
    setMessageState: (messageState: RawMessage["MessageState"]) => void;
  };
  editMessage: (sendTime: number, edit: MessageEdit) => void;
  deleteMessage: (sendTime: number) => void;
  addReaction: (sendTime: number, reaction: string) => Promise<void>;
  removeReaction: (sendTime: number, reaction: string) => Promise<void>;
  clearLiveMessages: () => void;
  chatSecret: Uint8Array | null;
  ownId: number;
  userId: number;
  inputBoxRef: React.RefObject<HTMLDivElement | null>;
  error: string;
  errorDescription: string;
  composerValue: string;
  setComposerValue: (value: string) => void;
  replyTo: number | undefined;
  setReplyTo: (value: number | undefined) => void;
};

export function useChat(): contextType {
  const ctx = useContext(context);
  if (!ctx) {
    throw new Error("useChat must be used within a ChatProvider");
  }
  return ctx;
}

export function useReplyMessage() {
  const { send } = useMTP();
  const { load } = useStorage();
  const { replyTo, setReplyTo, getChatSecret, userId } = useChat();
  const [ownId, setOwnId] = useState(0);
  const [replyMessage, setReplyMessage] = useState<RawMessage | undefined>();

  const handleReplyError = useCallback(
    (err: unknown) => {
      log(1, "chat", "red", "Failed to get reply message", err);
      toast("error", "Failed to get reply message", String(err));
      setReplyTo(undefined);
      setReplyMessage(undefined);
    },
    [setReplyTo],
  );

  useEffect(() => {
    void load("user_id").then((value) => {
      setOwnId(Number(value));
    });
  }, [load]);

  useEffect(() => {
    if (!replyTo || !ownId) {
      setReplyMessage(undefined);
      return;
    }

    let active = true;
    setReplyMessage(undefined);

    void fetchReplyMessage({
      replyTo,
      ownId,
      chatUserId: userId,
      send,
      getChatSecret,
    })
      .then((message) => {
        if (active) {
          setReplyMessage(message);
        }
      })
      .catch((err) => {
        if (active) {
          handleReplyError(err);
        }
      });

    return () => {
      active = false;
    };
  }, [replyTo, ownId, userId, send, getChatSecret, handleReplyError]);

  return {
    ownId,
    replyTo,
    replyMessage,
    replyUserId: replyMessage?.SenderId,
  };
}
