import { trimPluginDescription, ZeppelinPlugin } from "./ZeppelinPlugin";
import * as t from "io-ts";
import { stripObjectToScalars, successMessage } from "../utils";
import { decorators as d, IPluginOptions, logger } from "knub";
import { GuildChannel, Member, Message } from "eris";
import { GuildLogs } from "../data/GuildLogs";
import { LogType } from "../data/LogType";

const ConfigSchema = t.type({
  can_assign: t.boolean,
  can_mass_assign: t.boolean,
  assignable_roles: t.array(t.string),
});
type TConfigSchema = t.TypeOf<typeof ConfigSchema>;

export class RolesPlugin extends ZeppelinPlugin<TConfigSchema> {
  public static pluginName = "roles";
  public static configSchema = ConfigSchema;

  public static pluginInfo = {
    prettyName: "Roles",
    description: trimPluginDescription(`
      Enables authorised users to add and remove whitelisted roles with a command.
    `),
  };

  protected logs: GuildLogs;

  onLoad() {
    this.logs = new GuildLogs(this.guildId);
  }

  public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
    return {
      config: {
        can_assign: false,
        can_mass_assign: false,
        assignable_roles: [],
      },
      overrides: [
        {
          level: ">=50",
          config: {
            can_assign: true,
          },
        },
        {
          level: ">=100",
          config: {
            can_mass_assign: true,
          },
        },
      ],
    };
  }

  @d.command("addrole", "<member:member> <role:string$>", {
    extra: {
      info: {
        description: "Add a role to the specified member",
      },
    },
  })
  @d.permission("can_assign")
  async addRoleCmd(msg: Message, args: { member: Member; role: string }) {
    if (!this.canActOn(msg.member, args.member, true)) {
      return this.sendErrorMessage(msg.channel, "Cannot add roles to this user: insufficient permissions");
    }

    const roleId = await this.resolveRoleId(args.role);
    if (!roleId) {
      return this.sendErrorMessage(msg.channel, "Invalid role id");
    }

    const config = this.getConfigForMsg(msg);
    if (!config.assignable_roles.includes(roleId)) {
      return this.sendErrorMessage(msg.channel, "You cannot assign that role");
    }

    // Sanity check: make sure the role is configured properly
    const role = (msg.channel as GuildChannel).guild.roles.get(roleId);
    if (!role) {
      this.logs.log(LogType.BOT_ALERT, {
        body: `Unknown role configured for 'roles' plugin: ${roleId}`,
      });
      return this.sendErrorMessage(msg.channel, "You cannot assign that role");
    }

    if (args.member.roles.includes(roleId)) {
      return this.sendErrorMessage(msg.channel, "Member already has that role");
    }

    this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, args.member.id);

    await args.member.addRole(roleId);

    this.logs.log(LogType.MEMBER_ROLE_ADD, {
      member: stripObjectToScalars(args.member, ["user", "roles"]),
      roles: role.name,
      mod: stripObjectToScalars(msg.author),
    });

    this.sendSuccessMessage(msg.channel, "Role added to user!");
  }

  @d.command("massaddrole", "<role:string> <members:member...>")
  @d.permission("can_mass_assign")
  async massAddRoleCmd(msg: Message, args: { role: string; members: Member[] }) {
    for (const member of args.members) {
      if (!this.canActOn(msg.member, member, true)) {
        return this.sendErrorMessage(
          msg.channel,
          "Cannot add roles to 1 or more specified members: insufficient permissions",
        );
      }
    }

    const roleId = await this.resolveRoleId(args.role);
    if (!roleId) {
      return this.sendErrorMessage(msg.channel, "Invalid role id");
    }
    const role = this.guild.roles.get(roleId);

    const config = this.getConfigForMsg(msg);
    if (!config.assignable_roles.includes(roleId)) {
      return this.sendErrorMessage(msg.channel, "You cannot assign that role");
    }

    const membersWithoutTheRole = args.members.filter(m => !m.roles.includes(roleId));
    let assigned = 0;
    let failed = 0;
    const alreadyHadRole = args.members.length - membersWithoutTheRole.length;

    msg.channel.createMessage(`Adding role to specified members...`);

    for (const member of membersWithoutTheRole) {
      try {
        this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, member.id);
        await member.addRole(roleId);
        this.logs.log(LogType.MEMBER_ROLE_ADD, {
          member: stripObjectToScalars(member, ["user", "roles"]),
          roles: role.name,
          mod: stripObjectToScalars(msg.author),
        });
        assigned++;
      } catch (e) {
        logger.warn(`Error when adding role via !massaddrole: ${e.message}`);
        failed++;
      }
    }

    let resultMessage = `Role added to ${assigned} ${assigned === 1 ? "member" : "members"}!`;
    if (failed > 0) {
      resultMessage += ` Failed to add the role to ${failed} ${failed === 1 ? "member" : "members"}.`;
    }
    if (alreadyHadRole) {
      resultMessage += ` ${alreadyHadRole} ${alreadyHadRole === 1 ? "member" : "members"} already had the role.`;
    }

    msg.channel.createMessage(successMessage(resultMessage));
  }

  @d.command("removerole", "<member:member> <role:string$>", {
    extra: {
      info: {
        description: "Remove a role from the specified member",
      },
    },
  })
  @d.permission("can_assign")
  async removeRoleCmd(msg: Message, args: { member: Member; role: string }) {
    if (!this.canActOn(msg.member, args.member, true)) {
      return this.sendErrorMessage(msg.channel, "Cannot remove roles from this user: insufficient permissions");
    }

    const roleId = await this.resolveRoleId(args.role);
    if (!roleId) {
      return this.sendErrorMessage(msg.channel, "Invalid role id");
    }

    const config = this.getConfigForMsg(msg);
    if (!config.assignable_roles.includes(roleId)) {
      return this.sendErrorMessage(msg.channel, "You cannot remove that role");
    }

    // Sanity check: make sure the role is configured properly
    const role = (msg.channel as GuildChannel).guild.roles.get(roleId);
    if (!role) {
      this.logs.log(LogType.BOT_ALERT, {
        body: `Unknown role configured for 'roles' plugin: ${roleId}`,
      });
      return this.sendErrorMessage(msg.channel, "You cannot remove that role");
    }

    if (!args.member.roles.includes(roleId)) {
      return this.sendErrorMessage(msg.channel, "Member doesn't have that role");
    }

    this.logs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, args.member.id);

    await args.member.removeRole(roleId);

    this.logs.log(LogType.MEMBER_ROLE_REMOVE, {
      member: stripObjectToScalars(args.member, ["user", "roles"]),
      roles: role.name,
      mod: stripObjectToScalars(msg.author),
    });

    this.sendSuccessMessage(msg.channel, "Role removed from user!");
  }

  @d.command("massremoverole", "<role:string> <members:member...>")
  @d.permission("can_mass_assign")
  async massRemoveRoleCmd(msg: Message, args: { role: string; members: Member[] }) {
    for (const member of args.members) {
      if (!this.canActOn(msg.member, member, true)) {
        return this.sendErrorMessage(
          msg.channel,
          "Cannot add roles to 1 or more specified members: insufficient permissions",
        );
      }
    }

    const roleId = await this.resolveRoleId(args.role);
    if (!roleId) {
      return this.sendErrorMessage(msg.channel, "Invalid role id");
    }
    const role = this.guild.roles.get(roleId);

    const config = this.getConfigForMsg(msg);
    if (!config.assignable_roles.includes(roleId)) {
      return this.sendErrorMessage(msg.channel, "You cannot remove that role");
    }

    const membersWithTheRole = args.members.filter(m => m.roles.includes(roleId));
    let assigned = 0;
    let failed = 0;
    const didNotHaveRole = args.members.length - membersWithTheRole.length;

    msg.channel.createMessage(`Removing role from specified members...`);

    for (const member of membersWithTheRole) {
      try {
        this.logs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, member.id);
        await member.removeRole(roleId);
        this.logs.log(LogType.MEMBER_ROLE_REMOVE, {
          member: stripObjectToScalars(member, ["user", "roles"]),
          roles: role.name,
          mod: stripObjectToScalars(msg.author),
        });
        assigned++;
      } catch (e) {
        logger.warn(`Error when removing role via !massremoverole: ${e.message}`);
        failed++;
      }
    }

    let resultMessage = `Role removed from  ${assigned} ${assigned === 1 ? "member" : "members"}!`;
    if (failed > 0) {
      resultMessage += ` Failed to remove the role from  ${failed} ${failed === 1 ? "member" : "members"}.`;
    }
    if (didNotHaveRole) {
      resultMessage += ` ${didNotHaveRole} ${didNotHaveRole === 1 ? "member" : "members"} didn't have the role.`;
    }

    msg.channel.createMessage(successMessage(resultMessage));
  }
}