import {
  parseUri,
  createNotFoundPost,
  addFeedItemToFeed,
  pinPostInFeed,
  unpinPostInFeed,
  valueForPinnedItem,
} from "/js/dataHelpers.js";
import { getCurrentTimestamp } from "/js/utils.js";
import { PostCreator } from "/js/postCreator.js";
import { untrack } from "/js/signals.js";

// Handles mutations to the data, making optimistic updates if needed.
export class Mutations {
  constructor(
    api,
    dataStore,
    patchStore,
    preferencesProvider,
    identityResolver,
    draftMediaStore,
  ) {
    this.api = api;
    this.dataStore = dataStore;
    this.patchStore = patchStore;
    this.preferencesProvider = preferencesProvider;
    this.draftMediaStore = draftMediaStore;
    this.postCreator = new PostCreator(api, identityResolver);
  }

  async addLike(post) {
    // Optimistic update
    const patchId = this.patchStore.addPostPatch(post.uri, {
      type: "addLike",
    });
    try {
      const like = await this.api.createLikeRecord(post);
      // update post in store
      this.dataStore.$posts.set(post.uri, {
        ...post,
        viewer: { ...post.viewer, like: like.uri },
        likeCount: post.likeCount + 1,
      });
      // If the "likes" feed is loaded, add the post to it.
      const currentUser = this.dataStore.$currentUser.get();
      if (currentUser) {
        const feedURI = `${currentUser.did}-likes`;
        const likedFeed = this.dataStore.$authorFeeds.get(feedURI);
        if (likedFeed) {
          this.dataStore.$authorFeeds.set(feedURI, {
            feed: [{ post: post }, ...likedFeed.feed],
            cursor: likedFeed.cursor,
          });
        }
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      // clear patch
      this.patchStore.removePostPatch(post.uri, patchId);
    }
  }

  async removeLike(post) {
    // Optimistic update
    const patchId = this.patchStore.addPostPatch(post.uri, {
      type: "removeLike",
    });
    try {
      await this.api.deleteLikeRecord(post);
      // update post in store
      this.dataStore.$posts.set(post.uri, {
        ...post,
        viewer: { ...post.viewer, like: null },
        likeCount: post.likeCount - 1,
      });
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      // clear patch
      this.patchStore.removePostPatch(post.uri, patchId);
    }
  }

  async createRepost(post) {
    const patchId = this.patchStore.addPostPatch(post.uri, {
      type: "createRepost",
    });
    try {
      const repost = await this.api.createRepostRecord(post);
      this.dataStore.$posts.set(post.uri, {
        ...post,
        viewer: { ...post.viewer, repost: repost.uri },
        repostCount: post.repostCount + 1,
      });
      // If the current user's author feed is loaded, add the repost to it.
      const currentUser = this.dataStore.$currentUser.get();
      if (currentUser) {
        const authorFeedURI = `${currentUser.did}-posts`;
        const authorFeed = this.dataStore.$authorFeeds.get(authorFeedURI);
        if (authorFeed) {
          const newFeedItem = {
            post: post,
            reason: {
              $type: "app.bsky.feed.defs#reasonRepost",
              by: currentUser,
              uri: repost.uri,
              cid: repost.cid,
              indexedAt: new Date().toISOString(),
            },
          };
          this.dataStore.$authorFeeds.set(authorFeedURI, {
            feed: addFeedItemToFeed(newFeedItem, authorFeed.feed),
            cursor: authorFeed.cursor,
          });
        }
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      // clear patch
      this.patchStore.removePostPatch(post.uri, patchId);
    }
  }

  async deleteRepost(post) {
    const patchId = this.patchStore.addPostPatch(post.uri, {
      type: "deleteRepost",
    });
    try {
      await this.api.deleteRepostRecord(post);
      this.dataStore.$posts.set(post.uri, {
        ...post,
        viewer: { ...post.viewer, repost: null },
        repostCount: post.repostCount - 1,
      });
      // If the current user's author feed is loaded, remove the repost from it.
      const currentUser = this.dataStore.$currentUser.get();
      if (currentUser) {
        const authorFeedURI = `${currentUser.did}-posts`;
        const authorFeed = this.dataStore.$authorFeeds.get(authorFeedURI);
        if (authorFeed) {
          this.dataStore.$authorFeeds.set(authorFeedURI, {
            feed: authorFeed.feed.filter((feedItem) => {
              if (
                feedItem.reason?.$type === "app.bsky.feed.defs#reasonRepost" &&
                feedItem.reason?.uri === post.viewer.repost
              ) {
                return false;
              }
              return true;
            }),
            cursor: authorFeed.cursor,
          });
        }
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      // clear patch
      this.patchStore.removePostPatch(post.uri, patchId);
    }
  }

  async addBookmark(post) {
    // Optimistic update
    const patchId = this.patchStore.addPostPatch(post.uri, {
      type: "addBookmark",
    });
    try {
      await this.api.createBookmark(post);
      // update post in store
      this.dataStore.$posts.set(post.uri, {
        ...post,
        viewer: { ...post.viewer, bookmarked: true },
        bookmarkCount: post.bookmarkCount + 1,
      });
      // If the bookmarks feed is loaded, add the post to it.
      const bookmarks = this.dataStore.$bookmarks.get();
      if (bookmarks) {
        this.dataStore.$bookmarks.set({
          bookmarks: [{ item: { ...post } }, ...bookmarks.bookmarks],
          cursor: bookmarks.cursor,
        });
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      // clear patch
      this.patchStore.removePostPatch(post.uri, patchId);
    }
  }

  async removeBookmark(post) {
    // Optimistic update
    const patchId = this.patchStore.addPostPatch(post.uri, {
      type: "removeBookmark",
    });
    try {
      await this.api.deleteBookmark(post);
      // update post in store
      this.dataStore.$posts.set(post.uri, {
        ...post,
        viewer: { ...post.viewer, bookmarked: false },
        bookmarkCount: post.bookmarkCount - 1,
      });
      // If the bookmarks feed is loaded, remove the post from it.
      const bookmarks = this.dataStore.$bookmarks.get();
      if (bookmarks) {
        this.dataStore.$bookmarks.set({
          bookmarks: bookmarks.bookmarks.filter(
            (bookmark) => bookmark.item?.uri !== post.uri,
          ),
          cursor: bookmarks.cursor,
        });
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      // clear patch
      this.patchStore.removePostPatch(post.uri, patchId);
    }
  }

  async followProfile(profile) {
    const patchId = this.patchStore.addProfilePatch(profile.did, {
      type: "followProfile",
    });
    try {
      const follow = await this.api.createFollowRecord(profile);
      this.dataStore.$profiles.set(profile.did, {
        ...profile,
        viewer: { ...profile.viewer, following: follow.uri },
      });
      const detailed = this.dataStore.$detailedProfiles.get(profile.did);
      if (detailed) {
        this.dataStore.$detailedProfiles.set(profile.did, {
          ...detailed,
          followersCount: detailed.followersCount + 1,
          viewer: { ...detailed.viewer, following: follow.uri },
        });
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      // clear patch
      this.patchStore.removeProfilePatch(profile.did, patchId);
    }
  }

  async addProfileToList(profile, list) {
    const result = await this.api.createListItemRecord(list.uri, profile.did);
    this._patchListMembershipForActor(profile.did, list.uri, {
      uri: result.uri,
      subject: profile.did,
    });
    // Add to cached list members
    const cachedMembers = this.dataStore.$listMembers.get(list.uri);
    if (
      cachedMembers &&
      !cachedMembers.items.some((item) => item.subject.did === profile.did)
    ) {
      this.dataStore.$listMembers.set(list.uri, {
        ...cachedMembers,
        items: [{ uri: result.uri, subject: profile }, ...cachedMembers.items],
      });
    }
  }

  async removeProfileFromList(profile, list, membershipUri) {
    await this.api.deleteListItemRecord(membershipUri);
    this._patchListMembershipForActor(profile.did, list.uri, null);
    // Remove from cached list members
    const cachedMembers = this.dataStore.$listMembers.get(list.uri);
    if (cachedMembers) {
      this.dataStore.$listMembers.set(list.uri, {
        ...cachedMembers,
        items: cachedMembers.items.filter(
          (item) => item.subject.did !== profile.did,
        ),
      });
    }
  }

  _patchListMembershipForActor(actorDid, listUri, listItem) {
    const existing = this.dataStore.$listsWithMembershipByActor.get(actorDid);
    if (!existing) return;
    this.dataStore.$listsWithMembershipByActor.set(actorDid, {
      ...existing,
      listsWithMembership: existing.listsWithMembership.map((entry) =>
        entry.list.uri === listUri
          ? { ...entry, listItem: listItem || undefined }
          : entry,
      ),
    });
  }

  async unfollowProfile(profile) {
    const patchId = this.patchStore.addProfilePatch(profile.did, {
      type: "unfollowProfile",
    });
    try {
      await this.api.deleteFollowRecord(profile);
      this.dataStore.$profiles.set(profile.did, {
        ...profile,
        viewer: { ...profile.viewer, following: null },
      });
      const detailed = this.dataStore.$detailedProfiles.get(profile.did);
      if (detailed) {
        this.dataStore.$detailedProfiles.set(profile.did, {
          ...detailed,
          followersCount: detailed.followersCount - 1,
          viewer: { ...detailed.viewer, following: null },
        });
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      // clear patch
      this.patchStore.removeProfilePatch(profile.did, patchId);
    }
  }

  async sendShowLessInteraction(postURI, feedContext, feedProxyUrl) {
    const showLessInteraction = {
      item: postURI,
      event: "app.bsky.feed.defs#requestLess",
      feedContext,
    };
    this.dataStore.$showLessInteractions.set([
      ...this.dataStore.$showLessInteractions.get(),
      showLessInteraction,
    ]);
    try {
      await this.api.sendInteractions([showLessInteraction], feedProxyUrl);
    } catch (error) {
      console.error(error);
      throw error;
    }
  }

  async sendShowMoreInteraction(postURI, feedContext, feedProxyUrl) {
    const showMoreInteraction = {
      item: postURI,
      event: "app.bsky.feed.defs#requestMore",
      feedContext,
    };
    // Note, we don't really need to store this interaction because we don't use it in the UI (yet).
    // But, let's do it anyway for consistency.
    this.dataStore.$showMoreInteractions.set([
      ...this.dataStore.$showMoreInteractions.get(),
      showMoreInteraction,
    ]);
    try {
      await this.api.sendInteractions([showMoreInteraction], feedProxyUrl);
    } catch (error) {
      console.error(error);
      throw error;
    }
  }

  async pinFeed(feedUri) {
    const patchId = this.patchStore.addPreferencePatch({
      type: "pinFeed",
      feedUri,
      entryType: "feed",
    });
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.pinFeed(feedUri, "feed");
    try {
      await this.preferencesProvider.updatePreferences(newPreferences);
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removePreferencePatch(patchId);
    }
  }

  async pinList(listUri) {
    const patchId = this.patchStore.addPreferencePatch({
      type: "pinFeed",
      feedUri: listUri,
      entryType: "list",
    });
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.pinFeed(listUri, "list");
    try {
      await this.preferencesProvider.updatePreferences(newPreferences);
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removePreferencePatch(patchId);
    }
  }

  async unpinFeed(feedUri) {
    const patchId = this.patchStore.addPreferencePatch({
      type: "unpinFeed",
      feedUri,
    });
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.unpinFeed(feedUri);
    try {
      await this.preferencesProvider.updatePreferences(newPreferences);
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removePreferencePatch(patchId);
    }
  }

  async unpinList(listUri) {
    const patchId = this.patchStore.addPreferencePatch({
      type: "unpinFeed",
      feedUri: listUri,
    });
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.unpinFeed(listUri);
    try {
      await this.preferencesProvider.updatePreferences(newPreferences);
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removePreferencePatch(patchId);
    }
  }

  async setPinnedItems(values) {
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.setPinnedItems(values);
    try {
      await this.preferencesProvider.updatePreferences(newPreferences);
    } catch (error) {
      console.error(error);
      throw error;
    }

    // Update pinned items in memory
    const pinnedItems = untrack(() => this.dataStore.$pinnedItems.get());
    if (pinnedItems) {
      const byValue = new Map(
        pinnedItems.map((item) => [valueForPinnedItem(item), item]),
      );
      const next = values.map((value) => byValue.get(value)).filter(Boolean);
      this.dataStore.$pinnedItems.set(next);
    }
  }

  async hidePost(post) {
    const patchId = this.patchStore.addPostPatch(post.uri, {
      type: "hidePost",
    });
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.hidePost(post.uri);
    try {
      await this.preferencesProvider.updatePreferences(newPreferences);
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      // clear patch
      this.patchStore.removePostPatch(post.uri, patchId);
    }
  }

  async addMutedWord({ value, targets, actorTarget, expiresAt }) {
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.addMutedWord({
      value,
      targets,
      actorTarget,
      expiresAt,
    });
    await this.preferencesProvider.updatePreferences(newPreferences);
  }

  async removeMutedWord(wordId) {
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.removeMutedWord(wordId);
    await this.preferencesProvider.updatePreferences(newPreferences);
  }

  async updateMutedWord(wordId, updatedFields) {
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.updateMutedWord(wordId, updatedFields);
    await this.preferencesProvider.updatePreferences(newPreferences);
  }

  async subscribeLabeler(profile, labelerInfo) {
    const patchId = this.patchStore.addPreferencePatch({
      type: "subscribeLabeler",
      did: profile.did,
      labelerInfo,
    });
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.subscribeLabeler(
      profile.did,
      labelerInfo,
    );

    try {
      await this.preferencesProvider.updatePreferences(newPreferences);
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removePreferencePatch(patchId);
    }
  }

  async unsubscribeLabeler(profile) {
    const patchId = this.patchStore.addPreferencePatch({
      type: "unsubscribeLabeler",
      did: profile.did,
    });
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.unsubscribeLabeler(profile.did);
    try {
      await this.preferencesProvider.updatePreferences(newPreferences);
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removePreferencePatch(patchId);
    }
  }

  async updateLabelerSetting({ labelerDid, label, visibility }) {
    const patchId = this.patchStore.addPreferencePatch({
      type: "setContentLabelPref",
      label,
      visibility,
      labelerDid,
    });
    const preferences = this.preferencesProvider.requirePreferences();
    const newPreferences = preferences.setContentLabelPref({
      label,
      visibility,
      labelerDid,
    });
    try {
      await this.preferencesProvider.updatePreferences(newPreferences);
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removePreferencePatch(patchId);
    }
  }

  async muteProfile(profile) {
    const patchId = this.patchStore.addProfilePatch(profile.did, {
      type: "muteProfile",
    });
    try {
      await this.api.muteActor(profile.did);
      this.dataStore.$profiles.set(profile.did, {
        ...profile,
        viewer: { ...profile.viewer, muted: true },
      });
      const detailed = this.dataStore.$detailedProfiles.get(profile.did);
      if (detailed) {
        this.dataStore.$detailedProfiles.set(profile.did, {
          ...detailed,
          viewer: { ...detailed.viewer, muted: true },
        });
      }
      this._updatePostsByAuthor(profile.did, (post) => {
        return {
          ...post,
          author: {
            ...post.author,
            viewer: { ...post.author.viewer, muted: true },
          },
        };
      });
      const mutedProfiles = this.dataStore.$mutedProfiles.get();
      if (mutedProfiles) {
        const alreadyListed = mutedProfiles.mutes.some(
          (muted) => muted.did === profile.did,
        );
        if (!alreadyListed) {
          this.dataStore.$mutedProfiles.set({
            ...mutedProfiles,
            mutes: [
              {
                ...profile,
                viewer: { ...profile.viewer, muted: true },
              },
              ...mutedProfiles.mutes,
            ],
          });
        }
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removeProfilePatch(profile.did, patchId);
    }
  }

  async unmuteProfile(profile) {
    const patchId = this.patchStore.addProfilePatch(profile.did, {
      type: "unmuteProfile",
    });
    try {
      await this.api.unmuteActor(profile.did);
      this.dataStore.$profiles.set(profile.did, {
        ...profile,
        viewer: { ...profile.viewer, muted: false },
      });
      const detailed = this.dataStore.$detailedProfiles.get(profile.did);
      if (detailed) {
        this.dataStore.$detailedProfiles.set(profile.did, {
          ...detailed,
          viewer: { ...detailed.viewer, muted: false },
        });
      }
      this._updatePostsByAuthor(profile.did, (post) => {
        return {
          ...post,
          author: {
            ...post.author,
            viewer: { ...post.author.viewer, muted: false },
          },
        };
      });
      const mutedProfiles = this.dataStore.$mutedProfiles.get();
      if (mutedProfiles) {
        this.dataStore.$mutedProfiles.set({
          ...mutedProfiles,
          mutes: mutedProfiles.mutes.filter(
            (muted) => muted.did !== profile.did,
          ),
        });
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removeProfilePatch(profile.did, patchId);
    }
  }

  async blockProfile(profile) {
    const patchId = this.patchStore.addProfilePatch(profile.did, {
      type: "blockProfile",
    });
    try {
      const block = await this.api.blockActor(profile);
      this.dataStore.$profiles.set(profile.did, {
        ...profile,
        viewer: { ...profile.viewer, blocking: block.uri },
      });
      const detailed = this.dataStore.$detailedProfiles.get(profile.did);
      if (detailed) {
        this.dataStore.$detailedProfiles.set(profile.did, {
          ...detailed,
          viewer: { ...detailed.viewer, blocking: block.uri },
        });
      }
      this._updatePostsByAuthor(profile.did, (post) => {
        return {
          ...post,
          author: {
            ...post.author,
            viewer: { ...post.author.viewer, blocking: block.uri },
          },
        };
      });
      const blockedProfiles = this.dataStore.$blockedProfiles.get();
      if (blockedProfiles) {
        const alreadyListed = blockedProfiles.blocks.some(
          (blocked) => blocked.did === profile.did,
        );
        if (!alreadyListed) {
          this.dataStore.$blockedProfiles.set({
            ...blockedProfiles,
            blocks: [
              {
                ...profile,
                viewer: { ...profile.viewer, blocking: block.uri },
              },
              ...blockedProfiles.blocks,
            ],
          });
        }
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removeProfilePatch(profile.did, patchId);
    }
  }

  async updatePostNotificationSubscription(profile, activitySubscription) {
    const patchId = this.patchStore.addProfilePatch(profile.did, {
      type: "updatePostNotificationSubscription",
      activitySubscription,
    });
    try {
      await this.api.putActivitySubscription(profile.did, activitySubscription);
      this.dataStore.$profiles.set(profile.did, {
        ...profile,
        viewer: { ...profile.viewer, activitySubscription },
      });
      const detailed = this.dataStore.$detailedProfiles.get(profile.did);
      if (detailed) {
        this.dataStore.$detailedProfiles.set(profile.did, {
          ...detailed,
          viewer: { ...detailed.viewer, activitySubscription },
        });
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removeProfilePatch(profile.did, patchId);
    }
  }

  async unblockProfile(profile) {
    const patchId = this.patchStore.addProfilePatch(profile.did, {
      type: "unblockProfile",
    });
    try {
      await this.api.unblockActor(profile);
      this.dataStore.$profiles.set(profile.did, {
        ...profile,
        viewer: { ...profile.viewer, blocking: null },
      });
      const detailed = this.dataStore.$detailedProfiles.get(profile.did);
      if (detailed) {
        this.dataStore.$detailedProfiles.set(profile.did, {
          ...detailed,
          viewer: { ...detailed.viewer, blocking: null },
        });
      }
      this._updatePostsByAuthor(profile.did, (post) => {
        return {
          ...post,
          author: {
            ...post.author,
            viewer: { ...post.author.viewer, blocking: null },
          },
        };
      });
      const blockedProfiles = this.dataStore.$blockedProfiles.get();
      if (blockedProfiles) {
        this.dataStore.$blockedProfiles.set({
          ...blockedProfiles,
          blocks: blockedProfiles.blocks.filter(
            (blocked) => blocked.did !== profile.did,
          ),
        });
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removeProfilePatch(profile.did, patchId);
    }
  }

  async muteModList(list) {
    try {
      await this.api.muteModList(list.uri);
      this.dataStore.$lists.set(list.uri, {
        ...list,
        viewer: { ...list.viewer, muted: true },
      });
    } catch (error) {
      console.error(error);
      throw error;
    }
  }

  async unmuteModList(list) {
    try {
      await this.api.unmuteModList(list.uri);
      this.dataStore.$lists.set(list.uri, {
        ...list,
        viewer: { ...list.viewer, muted: false },
      });
    } catch (error) {
      console.error(error);
      throw error;
    }
  }

  async blockModList(list) {
    try {
      const block = await this.api.blockModList(list.uri);
      this.dataStore.$lists.set(list.uri, {
        ...list,
        viewer: { ...list.viewer, blocked: block.uri },
      });
    } catch (error) {
      console.error(error);
      throw error;
    }
  }

  async unblockModList(list) {
    const blockUri = list.viewer?.blocked;
    if (!blockUri) return;
    try {
      await this.api.unblockModList(blockUri);
      this.dataStore.$lists.set(list.uri, {
        ...list,
        viewer: { ...list.viewer, blocked: null },
      });
    } catch (error) {
      console.error(error);
      throw error;
    }
  }

  async updateProfile(
    profile,
    {
      displayName,
      description,
      avatarBlob,
      bannerBlob,
      removeAvatar,
      removeBanner,
    },
  ) {
    const [avatarRef, bannerRef] = await Promise.all([
      avatarBlob ? this.api.uploadBlob(avatarBlob) : null,
      bannerBlob ? this.api.uploadBlob(bannerBlob) : null,
    ]);

    let existingRecord = {};
    let swapCid = null;
    try {
      const recordData = await this.api.getProfileRecord();
      existingRecord = recordData.value || {};
      swapCid = recordData.cid;
    } catch (error) {
      if (error.status === 400) {
        // No existing record is ok
      } else {
        throw error;
      }
    }

    const updatedRecord = { ...existingRecord };
    if (displayName !== undefined) {
      updatedRecord.displayName = displayName;
    }
    if (description !== undefined) {
      updatedRecord.description = description;
    }
    if (avatarRef) {
      updatedRecord.avatar = avatarRef;
    } else if (removeAvatar) {
      delete updatedRecord.avatar;
    }
    if (bannerRef) {
      updatedRecord.banner = bannerRef;
    } else if (removeBanner) {
      delete updatedRecord.banner;
    }

    await this.api.putProfileRecord(updatedRecord, swapCid);

    const preferences = this.preferencesProvider.requirePreferences();
    const labelers = preferences.getLabelerDids();
    // Fetch full profile to get updated image urls
    const updatedProfile = await this.api.getProfile(profile.did, { labelers });
    this.dataStore.$profiles.set(updatedProfile.did, updatedProfile);
    this.dataStore.$detailedProfiles.set(updatedProfile.did, updatedProfile);
    const currentUser = this.dataStore.$currentUser.get();
    if (currentUser && currentUser.did === updatedProfile.did) {
      this.dataStore.$currentUser.set({
        ...currentUser,
        ...updatedProfile,
      });
    }
  }

  async pinPost(post) {
    const currentUser = this.dataStore.$currentUser.get();
    if (!currentUser) throw new Error("No current user");
    const authorFeedURI = `${currentUser.did}-posts`;
    const pinnedRef = { uri: post.uri, cid: post.cid };

    // Optimistic update via patches on currentUser and author feed
    const userPatchId = this.patchStore.addCurrentUserPatch({
      type: "setPinnedPost",
      pinnedPost: pinnedRef,
    });
    const feedPatchId = this.patchStore.addAuthorFeedPatch(authorFeedURI, {
      type: "pinPost",
      post,
    });

    try {
      const recordData = await this.api.getProfileRecord();
      const existingRecord = recordData.value || {};
      const swapCid = recordData.cid;
      await this.api.putProfileRecord(
        { ...existingRecord, pinnedPost: pinnedRef },
        swapCid,
      );
      // Commit to dataStore
      const latestUser = this.dataStore.$currentUser.get();
      if (latestUser) {
        this.dataStore.$currentUser.set({
          ...latestUser,
          pinnedPost: pinnedRef,
        });
      }
      const existingFeed = this.dataStore.$authorFeeds.get(authorFeedURI);
      if (existingFeed) {
        this.dataStore.$authorFeeds.set(authorFeedURI, {
          feed: pinPostInFeed(existingFeed.feed, post),
          cursor: existingFeed.cursor,
        });
      }
    } finally {
      this.patchStore.removeCurrentUserPatch(userPatchId);
      this.patchStore.removeAuthorFeedPatch(authorFeedURI, feedPatchId);
    }
  }

  async unpinPost(post) {
    const currentUser = this.dataStore.$currentUser.get();
    if (!currentUser) throw new Error("No current user");
    if (currentUser.pinnedPost?.uri !== post.uri) {
      // Already unpinned (or a different post is pinned); nothing to do.
      return;
    }
    const authorFeedURI = `${currentUser.did}-posts`;

    const userPatchId = this.patchStore.addCurrentUserPatch({
      type: "clearPinnedPost",
    });
    const feedPatchId = this.patchStore.addAuthorFeedPatch(authorFeedURI, {
      type: "unpinPost",
      post,
    });

    try {
      const recordData = await this.api.getProfileRecord();
      const existingRecord = recordData.value || {};
      const swapCid = recordData.cid;
      const { pinnedPost: _, ...updatedRecord } = existingRecord;
      await this.api.putProfileRecord(updatedRecord, swapCid);
      // Commit to dataStore
      const latestUser = this.dataStore.$currentUser.get();
      if (latestUser) {
        const { pinnedPost: _, ...rest } = latestUser;
        this.dataStore.$currentUser.set(rest);
      }
      const existingFeed = this.dataStore.$authorFeeds.get(authorFeedURI);
      if (existingFeed) {
        this.dataStore.$authorFeeds.set(authorFeedURI, {
          feed: unpinPostInFeed(existingFeed.feed, post),
          cursor: existingFeed.cursor,
        });
      }
    } finally {
      this.patchStore.removeCurrentUserPatch(userPatchId);
      this.patchStore.removeAuthorFeedPatch(authorFeedURI, feedPatchId);
    }
  }

  async createPost({
    postText,
    external,
    replyTo,
    replyRoot,
    quotedRecord,
    images,
    video,
    labels,
    threadgateAllow,
    postgateEmbeddingRules,
  }) {
    const { uri, cid, post } = await this.postCreator.createPost({
      postText,
      external,
      replyTo,
      replyRoot,
      quotedRecord,
      images,
      video,
      labels,
      threadgateAllow,
      postgateEmbeddingRules,
    });
    // Update local data with new post, if present
    if (post) {
      // NOTE: LEXICON DEVIATION
      post.viewer.priorityReply = true;
      this.dataStore.$posts.set(post.uri, post);
      // If it's a reply, update the reply post thread in the store
      if (replyTo) {
        const replyPostThread = this.dataStore.$postThreads.get(replyTo.uri);
        if (replyPostThread) {
          this.dataStore.$postThreads.set(replyTo.uri, {
            ...replyPostThread,
            replies: [
              {
                $type: "app.bsky.feed.defs#threadViewPost",
                post: post,
                replies: [],
              },
              ...replyPostThread.replies,
            ],
          });
        }
      }
      // If the author feed is loaded, add the new post to it
      const { repo: did } = parseUri(post.uri);
      const authorFeedURI = replyTo ? `${did}-replies` : `${did}-posts`; // TODO - handle media tab too?
      const authorFeed = this.dataStore.$authorFeeds.get(authorFeedURI);
      if (authorFeed) {
        this.dataStore.$authorFeeds.set(authorFeedURI, {
          feed: addFeedItemToFeed({ post }, authorFeed.feed),
          cursor: authorFeed.cursor,
        });
      }
    }
    return { uri, cid, post };
  }

  async deletePost(post) {
    // no optimistic update
    await this.api.deletePost(post);
    // Replace the post with a not found post.
    // This *should* remove the post from all relevant places in the UI.
    this.dataStore.$posts.set(post.uri, createNotFoundPost(post.uri));
  }

  async createMessage(convoId, { text, facets, replyTo, embed }) {
    // no optimistic update
    const res = await this.api.sendMessage(convoId, {
      text,
      facets,
      replyTo,
      embed,
    });
    this.dataStore.$messages.set(res.id, res);
    // Add the new message to the chat messages array in the dataStore
    const convoMessages = this.dataStore.$convoMessages.get(convoId);
    if (convoMessages) {
      this.dataStore.$convoMessages.set(convoId, {
        messages: [res, ...convoMessages.messages],
        cursor: convoMessages.cursor,
      });
    }
    // Update the last message in the convo
    const convo = this.dataStore.$convos.get(convoId);
    if (convo) {
      this.dataStore.$convos.set(convoId, {
        ...convo,
        lastMessage: {
          $type: "chat.bsky.convo.defs#messageView",
          ...res,
        },
      });
    }
    return res;
  }

  async requestJoinGroupChat(code) {
    const res = await this.api.requestJoinGroupChat(code);
    const preview = this.dataStore.$joinLinkPreviewsByCode.get(code);
    if (
      preview?.$type === "chat.bsky.group.defs#joinLinkPreviewView" &&
      preview.code === code
    ) {
      const updatedPreview = { ...preview };
      if (res.status === "joined" && res.convo) {
        updatedPreview.convo = res.convo;
      } else {
        updatedPreview.viewer = {
          ...(preview.viewer ?? {}),
          requestedAt: getCurrentTimestamp(),
        };
      }
      this.dataStore.$joinLinkPreviewsByCode.set(code, updatedPreview);
    }
    if (res.status === "joined" && res.convo) {
      this.dataStore.$convos.set(res.convo.id, res.convo);
    }
    return res;
  }

  async acceptConvo(convo) {
    await this.api.acceptConvo(convo.id);

    // Create updated convo with accepted status
    const updatedConvo = {
      ...convo,
      status: "accepted",
    };

    this.dataStore.$convos.set(convo.id, updatedConvo);

    // Update the convo in the convo list, adding it if not present
    const convoList = this.dataStore.$convoList.get();
    if (convoList) {
      const inList = convoList.convos.some(
        (listConvo) => listConvo.id === convo.id,
      );
      this.dataStore.$convoList.set({
        convos: inList
          ? convoList.convos.map((listConvo) =>
              listConvo.id === convo.id ? updatedConvo : listConvo,
            )
          : [updatedConvo, ...convoList.convos],
        cursor: convoList.cursor,
      });
    }

    // Remove from request list
    const convoRequestList = this.dataStore.$convoRequestList.get();
    if (convoRequestList) {
      this.dataStore.$convoRequestList.set({
        convos: convoRequestList.convos.filter(
          (listConvo) => listConvo.id !== convo.id,
        ),
        cursor: convoRequestList.cursor,
      });
    }

    return updatedConvo;
  }

  async rejectConvo(convo) {
    await this.api.leaveConvo(convo.id);
    this.dataStore.$convos.set(convo.id, null);
    const convoList = this.dataStore.$convoList.get();
    if (convoList) {
      this.dataStore.$convoList.set({
        convos: convoList.convos.filter(
          (listConvo) => listConvo.id !== convo.id,
        ),
        cursor: convoList.cursor,
      });
    }
    const convoRequestList = this.dataStore.$convoRequestList.get();
    if (convoRequestList) {
      this.dataStore.$convoRequestList.set({
        convos: convoRequestList.convos.filter(
          (listConvo) => listConvo.id !== convo.id,
        ),
        cursor: convoRequestList.cursor,
      });
    }
  }

  async markConvoAsRead(convoId) {
    const convo = untrack(() => this.dataStore.$convos.get(convoId));
    if (!convo?.unreadCount) return;
    await this.api.markConvoAsRead(convoId);
    const latest = this.dataStore.$convos.get(convoId);
    if (latest) {
      this.dataStore.$convos.set(convoId, {
        ...latest,
        unreadCount: 0,
      });
    }
  }

  async addMessageReaction(convoId, messageId, emoji, currentUserDid) {
    const patchId = this.patchStore.addMessagePatch(messageId, {
      type: "addReaction",
      reaction: {
        createdAt: getCurrentTimestamp(),
        sender: { did: currentUserDid },
        value: emoji,
      },
    });
    try {
      const message = await this.api.addMessageReaction(
        convoId,
        messageId,
        emoji,
      );
      this.dataStore.$messages.set(messageId, message);
      // Update the last reaction in the convo
      const convo = this.dataStore.$convos.get(convoId);
      if (convo) {
        this.dataStore.$convos.set(convoId, {
          ...convo,
          lastReaction: {
            $type: "chat.bsky.convo.defs#messageAndReactionView",
            message: message,
            reaction: message.reactions[0],
          },
        });
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removeMessagePatch(messageId, patchId);
    }
  }

  async removeMessageReaction(convoId, messageId, emoji, currentUserDid) {
    const patchId = this.patchStore.addMessagePatch(messageId, {
      type: "removeReaction",
      currentUserDid,
      value: emoji,
    });
    try {
      const message = await this.api.removeMessageReaction(
        convoId,
        messageId,
        emoji,
      );
      this.dataStore.$messages.set(messageId, message);
      // Update the last reaction in the convo
      const convo = this.dataStore.$convos.get(convoId);
      if (convo) {
        this.dataStore.$convos.set(convoId, {
          ...convo,
          lastReaction: null,
        });
      }
    } catch (error) {
      console.error(error);
      throw error;
    } finally {
      this.patchStore.removeMessagePatch(messageId, patchId);
    }
  }

  async createDraft({ draft, media }) {
    const res = await this.api.createDraft(draft);
    await this._saveDraftMedia(media);
    this._invalidateCachedDrafts();
    return res.id;
  }

  async updateDraft({ draftId, draft, media, pruneLocalRefs }) {
    await this.api.updateDraft(draftId, draft);
    await this._saveDraftMedia(media);
    await this._deleteDraftMedia(pruneLocalRefs);
    this._invalidateCachedDrafts();
  }

  async _saveDraftMedia(media) {
    const storedMedia = this.draftMediaStore.$media.get();
    for (const { path, source } of media) {
      if (storedMedia[path]) continue;
      try {
        await this.draftMediaStore.save(path, source);
      } catch (error) {
        console.error("Failed to save draft media locally", error);
      }
    }
  }

  async _deleteDraftMedia(localRefs) {
    for (const key of localRefs) {
      try {
        await this.draftMediaStore.delete(key);
      } catch (error) {
        console.error("Failed to delete draft media", error);
      }
    }
  }

  // Delete the cached drafts list so the next dialog open refetches it
  _invalidateCachedDrafts() {
    if (untrack(() => this.dataStore.$drafts.get()) !== null) {
      this.dataStore.$drafts.set(null);
    }
  }

  async deleteDraft({ draftId, localRefs }) {
    await this.api.deleteDraft(draftId);
    await this._deleteDraftMedia(localRefs);
    const data = untrack(() => this.dataStore.$drafts.get());
    if (data) {
      this.dataStore.$drafts.set({
        ...data,
        drafts: data.drafts.filter((draftView) => draftView.id !== draftId),
      });
    }
  }

  _updatePostsByAuthor(profileDid, updateFunc) {
    for (const post of this.dataStore.$posts.values()) {
      if (post?.author?.did === profileDid) {
        this.dataStore.$posts.set(post.uri, updateFunc(post));
      }
    }
  }
}
