import { Events, type Social as SocialTypes } from "../../types";
import { APITypes } from "../../utils";
import type { Client } from "../client";
import { Relationship } from "./relationship";

interface SocialInitData {
  online: number;
  notifications: SocialTypes.Notification[];
  friends: ReturnType<typeof processRelationship>[];
  other: ReturnType<typeof processRelationship>[];
  blocked: SocialTypes.Blocked[];
}

const processRelationship = (r: SocialTypes.Relationship, selfID: string) => ({
  ...r,
  user: {
    id: r.from._id === selfID ? r.to._id : r.from._id,
    username: r.from._id === selfID ? r.to.username : r.from.username,
    avatar:
      r.from._id === selfID ? r.to.avatar_revision : r.from.avatar_revision
  }
});

export class Social {
  private client: Client;

  /** The current number of people online */
  public online: number;
  /** List of the client's friends */
  public friends: Relationship[];
  /** List of "pending" relationships (shows in `other` tab on TETR.IO) */
  public other: Relationship[];
  /** people you block */
  public blocked: SocialTypes.Blocked[];
  /** Notifications */
  public notifications: SocialTypes.Notification[];

  /** @hideconstructor */
  private constructor(client: Client, init: SocialInitData) {
    this.client = client;

    this.init();
    this.online = init.online;
    this.friends = init.friends.map(
      (r) =>
        new Relationship(
          {
            id: r.user.id,
            relationshipID: r._id,
            username: r.user.username,
            avatar: r.user.avatar
          },
          this,
          this.client
        )
    );
    this.other = init.other.map(
      (r) =>
        new Relationship(
          {
            id: r.user.id,
            relationshipID: r._id,
            username: r.user.username,
            avatar: r.user.avatar
          },
          this,

          this.client
        )
    );
    this.blocked = init.blocked;
    this.notifications = init.notifications;
  }

  static async create(
    client: Client,
    initData: Events.in.Client["client.ready"]["social"]
  ) {
    const rel = initData.relationships.map((r) =>
      processRelationship(r, client.user.id)
    );
    const data: SocialInitData = {
      online: initData.total_online,
      notifications: initData.notifications,
      friends: rel.filter((r) => r.type === "friend"),
      other: rel.filter((r) => r.type === "pending"),

      blocked: rel
        .filter((r) => r.type === "block")
        .map((r) => ({
          id: r.user.id,
          username: r.user.username,
          avatar: r.user.avatar
        }))
    };

    return new Social(client, data);
  }

  private get api() {
    return this.client.api;
  }

  private init() {
    setTimeout(
      () =>
        this.notifications.forEach((n) => {
          if (n.type === "friend") {
            if (!n.seen) {
              const rel = processRelationship(
                n.data.relationship,
                this.client.user.id
              );
              this.client.emit("client.friended", {
                id: rel.user.id,
                name: rel.user.username,
                avatar: rel.user.avatar
              });
            }
          }
        }),
      0
    );

    this.client.on("social.online", (count) => {
      this.online = count;
    });

    this.client.on("social.notification", async (n) => {
      this.notifications.splice(0, 0, n);

      if (n.type === "friend") {
        const rel = processRelationship(
          n.data.relationship,
          this.client.user.id
        );
        this.client.emit("client.friended", {
          id: rel.user.id,
          name: rel.user.username,
          avatar: rel.user.avatar
        });

        const user = this.get({ id: rel.user.id });

        if (!user) {
          this.other.push(
            new Relationship(
              {
                id: rel.user.id,
                username: rel.user.username,
                avatar: rel.user.avatar,
                relationshipID: ""
              },
              this,
              this.client
            )
          );
        }
      }
    });

    this.client.on("social.dm", async (dm) => {
      let user = this.get({ id: dm.data.user });
      if (user) {
        if (!user.dmsLoaded) await user.loadDms();
        else user.dms.push(dm);
      } else {
        const u = await this.who(dm.data.user);
        this.other.push(
          new Relationship(
            {
              id: u._id,
              username: u.username,
              avatar: u.avatar_revision,
              relationshipID: ""
            },
            this,
            this.client
          )
        );

        user = this.get({ id: dm.data.user })!;
        await user.loadDms();
      }

      this.client.emit("client.dm", {
        user,
        content: dm.data.content,
        reply: async (content: string) => {
          await this.dm(dm.data.user, content);
        }
      });
    });
  }

  /**
   * Marks all notifications as read
   * @example
   * client.social.markNotificationsAsRead();
   */
  markNotificationsAsRead() {
    this.client.emit("social.notification.ack");
  }

  /**
   * Get a user + social data from the list of users and pending friends (OTHER tab in TETR.IO)
   * @example
   * const user = await client.social.get('halp')
   * @example
   * const user = await client.social.get('646f633d276f42a80ba44304');
   * @example
   * const user = await client.social.get({ username: 'halp' });
   * @example
   * const user = await client.social.get({ id: '646f633d276f42a80ba44304 });
   * @example
   * // You can then use the object to read user data and interact with the user
   * await user.dm('wanna play?');
   * await user.invite();
   */
  get(target: string): Relationship | null;
  get(target: { id: string }): Relationship | null;
  get(target: { username: string }): Relationship | null;
  get(
    target: string | { id: string } | { username: string }
  ): Relationship | null {
    if (typeof target === "string")
      return (
        this.friends.find((r) => r.id === target || r.username === target) ||
        this.other.find((r) => r.id === target || r.username === target) ||
        null
      );
    else if ("id" in target)
      return (
        this.friends.find((r) => r.id === target.id) ||
        this.other.find((r) => r.id === target.id) ||
        null
      );
    else
      return (
        this.friends.find((r) => r.username === target.username) ||
        this.other.find((r) => r.username === target.username) ||
        null
      );
  }

  /**
   * Get the user id given a username
   */
  async resolve(username: string) {
    return await this.api.users.resolve(username);
  }

  /** Get a users' information based on their userid or username
   * @example
   * const user = await client.social.who(await client.social.resolve('halp'));
   */
  who(id: string): Promise<APITypes.Users.User>;
  async who(id: string): Promise<APITypes.Users.User> {
    return this.api.users.get({ id });
  }

  /**
   * Send a message to a specified user (based on id)
   * @example
   * await client.social.dm(await client.social.resolve('halp'), 'what\'s up?');
   */
  async dm(userID: string, message: string) {
    return await this.client.wrap(
      "social.dm",
      {
        recipient: userID,
        msg: message
      },
      "social.dm",
      ["social.dm.fail", "client.error"]
    );
  }

  /**
   * Send a user a friend request
   * @example
   * await client.social.friend(await client.social.resolve('halp'));
   * @returns false if the user is already friended, true otherwise
   * @throws {Error} If an error occurs (such as the user has blocked the client, etc)
   */
  async friend(userID: string) {
    if (this.friends.find((r) => r.id === userID)) return false;
    await this.api.social.friend(userID);
    const userData = await this.who(userID);
    this.friends.push(
      new Relationship(
        {
          id: userData._id,
          relationshipID: "",
          username: userData.username,
          avatar: userData.avatar_revision
        },
        this,
        this.client
      )
    );

    return true;
  }

  /**
   * Unfriend a user. Note: unfriending a user will unblock them if they are blocked.
   * @example
   * await client.social.unfriend(await client.social.resolve('halp'));
   * @returns false if the user is not unfriended, true otherwise
   */
  async unfriend(userID: string) {
    if (!this.friends.find((r) => r.id === userID)) return false;
    await this.api.social.unfriend(userID);
    this.friends = this.friends.filter((r) => r.id !== userID);

    return true;
  }

  /**
   * Block a user
   * @example
   * await client.social.block(await client.social.resolve('halp'));
   * @returns false if the user is already blocked, true otherwise
   */
  async block(userID: string) {
    if (this.blocked.find((r) => r.id === userID)) return false;
    await this.api.social.block(userID);
  }

  /**
   * Unblock a user. Note: unblocking a user will unfriend them if they are friended.
   * @example
   * await client.social.unblock(await client.social.resolve('halp'));
   * @returns false if the user is not unblocked, true otherwise
   */
  async unblock(userID: string) {
    await this.api.social.unblock(userID);
  }

  /**
   * Invite a user to your room
   * @example
   * await client.social.invite(await client.social.resolve('halp'));
   */
  invite(userID: string) {
    return new Promise<void>(async (resolve, reject) => {
      let r = false;
      this.client.emit("social.invite", userID);
      const l = (e: string) => {
        if (r) return;
        r = true;
        reject(e);
      };
      this.client.once("client.error", l);
      await new Promise((r) => setTimeout(r, 100));
      this.client.off("client.error", l);
      r = true;
      resolve();
    });
  }

  /**
   * Set the client's status
   * @example
   * client.social.status('online', 'lobby:X-QP');
   */
  status(status: SocialTypes.Status, detail: SocialTypes.Detail | String = "") {
    this.client.emit("social.presence", { status, detail });
  }
}

export * from "./relationship";
