import { ID, STR, UID } from "./UniqueIds";
import { HexKey, NostrEvent } from "..";

export default class SocialGraph {
  root: UID;
  followDistanceByUser = new Map<UID, number>();
  usersByFollowDistance = new Map<number, Set<UID>>();
  followedByUser = new Map<UID, Set<UID>>();
  followersByUser = new Map<UID, Set<UID>>();
  latestFollowEventTimestamps = new Map<UID, number>();

  constructor(root: HexKey) {
    this.root = ID(root);
    this.followDistanceByUser.set(this.root, 0);
    this.usersByFollowDistance.set(0, new Set([this.root]));
  }

  setRoot(root: HexKey) {
    const rootId = ID(root);
    if (rootId === this.root) {
      return;
    }
    this.root = rootId;
    this.followDistanceByUser.clear();
    this.usersByFollowDistance.clear();
    this.followDistanceByUser.set(this.root, 0);
    this.usersByFollowDistance.set(0, new Set([this.root]));

    const queue = [this.root];

    while (queue.length > 0) {
      const user = queue.shift()!;
      const distance = this.followDistanceByUser.get(user)!;

      const followers = this.followersByUser.get(user) || new Set<UID>();
      for (const follower of followers) {
        if (!this.followDistanceByUser.has(follower)) {
          const newFollowDistance = distance + 1;
          this.followDistanceByUser.set(follower, newFollowDistance);
          if (!this.usersByFollowDistance.has(newFollowDistance)) {
            this.usersByFollowDistance.set(newFollowDistance, new Set());
          }
          this.usersByFollowDistance.get(newFollowDistance)!.add(follower);
          queue.push(follower);
        }
      }
    }
  }

  handleEvent(evs: NostrEvent | Array<NostrEvent>) {
    const filtered = (Array.isArray(evs) ? evs : [evs]).filter(a => a.kind === 3);
    if (filtered.length === 0) {
      return;
    }
    queueMicrotask(() => {
      try {
        for (const event of filtered) {
          const author = ID(event.pubkey);
          const timestamp = event.created_at;
          const existingTimestamp = this.latestFollowEventTimestamps.get(author);
          if (existingTimestamp && timestamp <= existingTimestamp) {
            return;
          }
          this.latestFollowEventTimestamps.set(author, timestamp);

          // Collect all users followed in the new event.
          const followedInEvent = new Set<UID>();
          for (const tag of event.tags) {
            if (tag[0] === "p") {
              const followedUser = ID(tag[1]);
              if (followedUser !== author) {
                followedInEvent.add(followedUser);
              }
            }
          }

          // Get the set of users currently followed by the author.
          const currentlyFollowed = this.followedByUser.get(author) || new Set<UID>();

          // Find users that need to be removed.
          for (const user of currentlyFollowed) {
            if (!followedInEvent.has(user)) {
              this.removeFollower(user, author);
            }
          }

          // Add or update the followers based on the new event.
          for (const user of followedInEvent) {
            this.addFollower(user, author);
          }
        }
      } catch (e) {
        // might not be logged in or sth
      }
    });
  }

  isFollowing(follower: HexKey, followedUser: HexKey): boolean {
    const followedUserId = ID(followedUser);
    const followerId = ID(follower);
    return !!this.followedByUser.get(followerId)?.has(followedUserId);
  }

  getFollowDistance(user: HexKey): number {
    try {
      const userId = ID(user);
      if (userId === this.root) {
        return 0;
      }
      const distance = this.followDistanceByUser.get(userId);
      return distance === undefined ? 1000 : distance;
    } catch (e) {
      // might not be logged in or sth
      return 1000;
    }
  }

  addUserByFollowDistance(distance: number, user: UID) {
    if (!this.usersByFollowDistance.has(distance)) {
      this.usersByFollowDistance.set(distance, new Set());
    }
    this.usersByFollowDistance.get(distance)?.add(user);
    // remove from higher distances
    for (const d of this.usersByFollowDistance.keys()) {
      if (d > distance) {
        this.usersByFollowDistance.get(d)?.delete(user);
      }
    }
  }

  addFollower(followedUser: UID, follower: UID) {
    if (typeof followedUser !== "number" || typeof follower !== "number") {
      throw new Error("Invalid user id");
    }
    if (!this.followersByUser.has(followedUser)) {
      this.followersByUser.set(followedUser, new Set<UID>());
    }
    this.followersByUser.get(followedUser)?.add(follower);

    if (!this.followedByUser.has(follower)) {
      this.followedByUser.set(follower, new Set<UID>());
    }

    if (followedUser !== this.root) {
      let newFollowDistance;
      if (follower === this.root) {
        // basically same as the next "else" block, but faster
        newFollowDistance = 1;
        this.addUserByFollowDistance(newFollowDistance, followedUser);
        this.followDistanceByUser.set(followedUser, newFollowDistance);
      } else {
        const existingFollowDistance = this.followDistanceByUser.get(followedUser);
        const followerDistance = this.followDistanceByUser.get(follower);
        newFollowDistance = followerDistance && followerDistance + 1;
        if (existingFollowDistance === undefined || (newFollowDistance && newFollowDistance < existingFollowDistance)) {
          this.followDistanceByUser.set(followedUser, newFollowDistance!);
          this.addUserByFollowDistance(newFollowDistance!, followedUser);
        }
      }
    }

    this.followedByUser.get(follower)?.add(followedUser);
  }

  removeFollower(unfollowedUser: UID, follower: UID) {
    this.followersByUser.get(unfollowedUser)?.delete(follower);
    this.followedByUser.get(follower)?.delete(unfollowedUser);

    if (unfollowedUser === this.root) {
      return;
    }

    // iterate over remaining followers and set the smallest follow distance
    let smallest = Infinity;
    for (const follower of this.followersByUser.get(unfollowedUser) || []) {
      const followerDistance = this.followDistanceByUser.get(follower);
      if (followerDistance !== undefined && followerDistance + 1 < smallest) {
        smallest = followerDistance + 1;
      }
    }

    if (smallest === Infinity) {
      this.followDistanceByUser.delete(unfollowedUser);
    } else {
      this.followDistanceByUser.set(unfollowedUser, smallest);
    }
  }

  // TODO subscription methods for followersByUser and followedByUser. and maybe messagesByTime. and replies
  followerCount(address: HexKey) {
    const id = ID(address);
    return this.followersByUser.get(id)?.size ?? 0;
  }

  followedByFriendsCount(address: HexKey) {
    let count = 0;
    const id = ID(address);
    for (const follower of this.followersByUser.get(id) ?? []) {
      if (this.followedByUser.get(this.root)?.has(follower)) {
        count++; // should we stop at 10?
      }
    }
    return count;
  }

  followedByFriends(address: HexKey) {
    const id = ID(address);
    const set = new Set<HexKey>();
    for (const follower of this.followersByUser.get(id) ?? []) {
      if (this.followedByUser.get(this.root)?.has(follower)) {
        set.add(STR(follower));
      }
    }
    return set;
  }

  getFollowedByUser(user: HexKey, includeSelf = false): Set<HexKey> {
    const userId = ID(user);
    const set = new Set<HexKey>();
    for (const id of this.followedByUser.get(userId) || []) {
      set.add(STR(id));
    }
    if (includeSelf) {
      set.add(user);
    }
    return set;
  }

  getFollowersByUser(address: HexKey): Set<HexKey> {
    const userId = ID(address);
    const set = new Set<HexKey>();
    for (const id of this.followersByUser.get(userId) || []) {
      set.add(STR(id));
    }
    return set;
  }
}

export const socialGraphInstance = new SocialGraph("");
