"use server";

import { readFile, readdir, writeFile } from "fs/promises";
import { PLATFORM_MOD_ID, RESTRICT_ACCESS_LIST } from "./constants";
import mongo, {
  Account,
  ChannelInvite,
  EmailClassification,
  createDM,
  fetchAccountById,
  findDM,
} from "./db";
import { publishMessage, sendChatMessage } from "./redis";
import { ulid } from "ulid";
import {
  AccountInfo,
  AccountStrike,
  Bot,
  Channel,
  File,
  Invite,
  Member,
  Message,
  Report,
  Server,
  SessionInfo,
  User,
} from "revolt-api";
import { checkPermission } from "./accessPermissions";
import { Long } from "mongodb";
import { nanoid } from "nanoid";

export async function sendAlert(userId: string, content: string) {
  await checkPermission("users/create/alert", userId, { content });

  const messageId = ulid();

  let dm = await findDM(PLATFORM_MOD_ID, userId);
  if (!dm) dm = await createDM(PLATFORM_MOD_ID, userId, messageId);

  await sendChatMessage({
    _id: messageId,
    author: PLATFORM_MOD_ID,
    channel: dm._id,
    content,
  });
}

export async function createStrike(
  userId: string,
  givenReason: string,
  additionalContext: string
) {
  if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";

  await checkPermission("users/create/strike", userId, {
    givenReason,
    additionalContext,
  });

  const strike: AccountStrike & { moderator_id: string } = {
    _id: ulid(),
    user_id: userId,
    moderator_id: "01EX2NCWQ0CHS3QJF0FEQS1GR4", // TODO
    reason: additionalContext
      ? givenReason + " - " + additionalContext
      : givenReason,
  };

  await mongo()
    .db("revolt")
    .collection<{ _id: string }>("safety_strikes")
    .insertOne(strike);
  await sendAlert(
    userId,
    `You have received an account strike, for one or more reasons:
- ${givenReason}
 
Further violations will result in suspension or a permanent ban depending on severity, please abide by the [Acceptable Usage Policy](https://revolt.chat/aup).`
  );

  return strike;
}

export async function updateReportNotes(reportId: string, notes: string) {
  await checkPermission("reports/update/notes", reportId, { notes });

  return await mongo()
    .db("revolt")
    .collection<Report>("safety_reports")
    .updateOne(
      { _id: reportId },
      {
        $set: {
          notes,
        },
      }
    );
}

export async function resolveReport(reportId: string) {
  await checkPermission("reports/update/resolve", reportId);

  const $set = {
    status: "Resolved",
    closed_at: new Date().toISOString(),
  } as Report;

  await mongo().db("revolt").collection<Report>("safety_reports").updateOne(
    { _id: reportId },
    {
      $set,
    }
  );

  return $set;
}

export async function rejectReport(reportId: string, reason: string) {
  await checkPermission("reports/update/reject", reportId, { reason });

  const $set = {
    status: "Rejected",
    rejection_reason: reason,
    closed_at: new Date().toISOString(),
  } as Report;

  await mongo().db("revolt").collection<Report>("safety_reports").updateOne(
    { _id: reportId },
    {
      $set,
    }
  );

  return $set;
}

export async function reopenReport(reportId: string) {
  await checkPermission("reports/update/reopen", reportId);

  const $set = {
    status: "Created",
  } as Report;

  await mongo()
    .db("revolt")
    .collection<Report>("safety_reports")
    .updateOne(
      { _id: reportId },
      {
        $set,
        $unset: {
          closed_at: 1,
        } as never,
      }
    );

  return $set;
}

export async function closeReportsByUser(userId: string) {
  await checkPermission("reports/update/bulk-close/by-user", userId);

  return await mongo()
    .db("revolt")
    .collection<Report>("safety_reports")
    .updateMany(
      {
        status: "Created",
        author_id: userId,
      },
      {
        $set: {
          status: "Rejected",
          rejection_reason: "bulk close",
          closed_at: new Date().toISOString(),
        },
      }
    )
    .then((res) => res.modifiedCount);
}

export async function disableAccount(userId: string) {
  if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";

  await checkPermission("accounts/disable", userId);

  await mongo()
    .db("revolt")
    .collection<AccountInfo>("accounts")
    .updateOne({ _id: userId }, { $set: { disabled: true } });

  await mongo().db("revolt").collection<SessionInfo>("sessions").deleteMany({
    user_id: userId,
  });
}

export async function deleteMFARecoveryCodes(userId: string) {
  if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
  await checkPermission("accounts/update/mfa", userId);

  await mongo()
    .db("revolt")
    .collection<Account>("accounts")
    .updateOne(
      { _id: userId },
      {
        $unset: {
          "mfa.recovery_codes": 1,
        },
      },
    )
}

export async function disableMFA(userId: string) {
  if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
  await checkPermission("accounts/update/mfa", userId);

  await mongo()
    .db("revolt")
    .collection<Account>("accounts")
    .updateOne(
      { _id: userId },
      {
        $unset: {
          "mfa.totp_token": 1,
        },
      },
    )
}

export async function changeAccountEmail(userId: string, email: string) {
  if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
  await checkPermission("accounts/update/email", userId);

  const SPLIT_RE = /([^@]+)(@.+)/;
  const SYMBOL_RE = /\+.+|\./g;

  const segments = SPLIT_RE.exec(email);
  if (!segments) throw "invalid email";

  await mongo()
    .db("revolt")
    .collection<Account>("accounts")
    .updateOne(
      { _id: userId },
      {
        $set: {
          email: email,
          email_normalised: segments[1].replace(SYMBOL_RE, "") + segments[2],
          verification: { status: "Verified" },
        },
      }
    );
}

export async function verifyAccountEmail(userId: string) {
  if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
  await checkPermission("accounts/update/email", userId);

  const account = await fetchAccountById(userId);

  if (!account) throw new Error("couldn't find account");
  if (account.verification.status == "Verified")
    throw new Error("already verified");

  let email = account.email;
  if (account.verification.status == "Moving") {
    email = account.verification.new_email;
  }

  await mongo()
    .db("revolt")
    .collection<Account>("accounts")
    .updateOne(
      { _id: userId },
      {
        $set: {
          email: email,
          email_normalised: email, // <-- should be fine but someone should fix this in the future
          verification: { status: "Verified" },
        },
      }
    );
}

export async function lookupEmail(email: string): Promise<string | false> {
  await checkPermission("accounts/fetch/by-email", email);

  const accounts = mongo()
    .db("revolt")
    .collection<Account>("accounts");

  let result = await accounts.findOne({ email: email });
  if (result) return result._id;

  result = await accounts.findOne({ email_normalised: email });
  if (result) return result._id;

  return false;
}

export async function suspendUser(userId: string) {
  if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";

  await checkPermission("users/action/suspend", userId);
  await disableAccount(userId);

  await mongo()
    .db("revolt")
    .collection<User>("users")
    .updateOne(
      {
        _id: userId,
      },
      {
        $set: {
          flags: 1,
        },
      }
    );

  const memberships = await mongo()
    .db("revolt")
    .collection<{ _id: { user: string; server: string } }>("server_members")
    .find({ "_id.user": userId })
    .toArray();

  for (const topic of memberships.map((x) => x._id.server)) {
    await publishMessage(topic, {
      type: "UserUpdate",
      id: userId,
      data: {
        flags: 1,
      },
      clear: [],
    });
  }
}

export async function updateUserBadges(userId: string, badges: number) {
  await checkPermission("users/update/badges", userId, { badges });
  await mongo().db("revolt").collection<User>("users").updateOne(
    {
      _id: userId,
    },
    {
      $set: {
        badges,
      },
    }
  );

  const memberships = await mongo()
    .db("revolt")
    .collection<{ _id: { user: string; server: string } }>("server_members")
    .find({ "_id.user": userId })
    .toArray();

  for (const topic of [userId, ...memberships.map((x) => x._id.server)]) {
    await publishMessage(topic, {
      type: "UserUpdate",
      id: userId,
      data: {
        badges,
      },
      clear: [],
    });
  }
}

export async function wipeUser(userId: string, flags = 4) {
  if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";

  await checkPermission("users/action/wipe", userId, { flags });

  const user = await mongo()
    .db("revolt")
    .collection<User>("users")
    .findOne({ _id: userId });

  const messages = await mongo()
    .db("revolt")
    .collection<Message>("messages")
    .find({ author: userId }, { sort: { _id: -1 } })
    .toArray();

  const dms = await mongo()
    .db("revolt")
    .collection<Channel>("channels")
    .find({
      channel_type: "DirectMessage",
      recipients: userId,
    })
    .toArray();

  const memberships = await mongo()
    .db("revolt")
    .collection<{ _id: { user: string; server: string } }>("server_members")
    .find({ "_id.user": userId })
    .toArray();

  // retrieve messages, dm channels, relationships, server memberships
  const backup = {
    _event: "wipe",
    user,
    messages,
    dms,
    memberships,
  };

  await writeFile(
    `./exports/${new Date().toISOString()} - ${userId}.json`,
    JSON.stringify(backup)
  );

  // mark all attachments as deleted + reported
  const attachmentIds = backup.messages
    .filter((message) => message.attachments)
    .map((message) => message.attachments)
    .flat()
    .filter((attachment) => attachment)
    .map((attachment) => attachment!._id);

  if (backup.user?.avatar) {
    attachmentIds.push(backup.user.avatar._id);
  }

  if (backup.user?.profile?.background) {
    attachmentIds.push(backup.user.profile.background._id);
  }

  if (attachmentIds.length) {
    await mongo()
      .db("revolt")
      .collection<File>("attachments")
      .updateMany(
        { _id: { $in: attachmentIds } },
        {
          $set: {
            reported: true,
            deleted: true,
          },
        }
      );
  }

  // delete messages
  await mongo().db("revolt").collection<Message>("messages").deleteMany({
    author: userId,
  });

  // delete server memberships
  await mongo().db("revolt").collection<Member>("server_members").deleteMany({
    "_id.user": userId,
  });

  // disable account
  await disableAccount(userId);

  // clear user profile
  await mongo()
    .db("revolt")
    .collection<User>("users")
    .updateOne(
      {
        _id: userId,
      },
      {
        $set: {
          flags,
        },
        $unset: {
          avatar: 1,
          profile: 1,
          status: 1,
        },
      }
    );

  // broadcast wipe event
  for (const topic of [
    ...backup.dms.map((x) => x._id),
    ...backup.memberships.map((x) => x._id.server),
  ]) {
    await publishMessage(topic, {
      type: "UserPlatformWipe",
      user_id: userId,
      flags,
    });
  }
}

export async function banUser(userId: string) {
  if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
  await checkPermission("users/action/ban", userId);
  return await wipeUser(userId, 4);
}

export async function unsuspendUser(userId: string) {
  if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
  await checkPermission("users/action/unsuspend", userId);
  await restoreAccount(userId);

  await mongo()
    .db("revolt")
    .collection<User>("users")
    .updateOne(
      {
        _id: userId,
      },
      {
        $unset: {
          flags: 1,
        },
      }
    );
}

export async function wipeUserProfile(
  userId: string,
  fields: {
    banner: boolean;
    avatar: boolean;
    bio: boolean;
    displayName: boolean;
    status: boolean;
  }
) {
  await checkPermission("users/action/wipe-profile", userId);

  await mongo()
    .db("revolt")
    .collection<User>("users")
    .updateOne(
      {
        _id: userId,
      },
      {
        $unset: {
          ...(fields.banner ? { "profile.background": 1 } : {}),
          ...(fields.bio ? { "profile.content": 1 } : {}),
          ...(fields.status ? { "status.text": 1 } : {}),
          ...(fields.avatar ? { avatar: 1 } : {}),
          ...(fields.displayName ? { display_name: 1 } : {}),
        },
      }
    );
}

export async function updateServerFlags(serverId: string, flags: number) {
  await checkPermission("servers/update/flags", serverId, { flags });
  await mongo().db("revolt").collection<Server>("servers").updateOne(
    {
      _id: serverId,
    },
    {
      $set: {
        flags,
      },
    }
  );

  await publishMessage(serverId, {
    type: "ServerUpdate",
    id: serverId,
    data: {
      flags,
    },
    clear: [],
  });
}

export async function updateServerDiscoverability(
  serverId: string,
  state: boolean
) {
  await checkPermission("servers/update/discoverability", serverId, { state });
  await mongo()
    .db("revolt")
    .collection<Server>("servers")
    .updateOne(
      {
        _id: serverId,
      },
      {
        $set: {
          analytics: state,
          discoverable: state,
        },
      }
    );
}

export async function updateServerOwner(serverId: string, userId: string) {
  await checkPermission("servers/update/owner", { serverId, userId });

  await mongo()
    .db("revolt")
    .collection<Server>("servers")
    .updateOne(
      { _id: serverId },
      { $set: { owner: userId } },
    );

  await publishMessage(serverId, {
    type: "ServerUpdate",
    id: serverId,
    data: {
      owner: userId,
    },
    clear: [],
  });
}

export async function addServerMember(serverId: string, userId: string, withEvent: boolean) {
  await checkPermission("servers/update/add-member", { serverId, userId, withEvent });

  const server = await mongo()
    .db("revolt")
    .collection<Server>("servers")
    .findOne({ _id: serverId });

  const channels = await mongo()
    .db("revolt")
    .collection<Channel>("channels")
    .find({ server: serverId })
    .toArray();

  const member = await mongo()
    .db("revolt")
    .collection<Member>("server_members")
    .findOne({ _id: { server: serverId, user: userId } });

  if (!server) throw new Error("server doesn't exist");
  if (member) throw new Error("already a member");

  await mongo()
    .db("revolt")
    .collection<Member>("server_members")
    .insertOne({
      _id: { server: serverId, user: userId },
      joined_at: Long.fromNumber(Date.now()) as unknown as string,
    });

  await publishMessage(userId + '!', {
    type: "ServerCreate",
    id: serverId,
    channels: channels,
    server: server,
  });

  if (withEvent) {
    await publishMessage(serverId, {
      type: "ServerMemberJoin",
      id: serverId,
      user: userId,
    });
  }
}

export async function quarantineServer(serverId: string, message: string) {
  await checkPermission("servers/update/quarantine", { serverId, message });

  const server = await mongo()
    .db("revolt")
    .collection<Server>("servers")
    .findOne({ _id: serverId });

  const members = await mongo()
    .db("revolt")
    .collection<Member>("server_members")
    .find({ "_id.server": serverId })
    .toArray();

  const invites = await mongo()
    .db("revolt")
    .collection<Invite>("channel_invites")
    .find({ type: "Server", server: serverId })
    .toArray();

  if (!server) throw new Error("server doesn't exist");

  const backup = {
    _event: "quarantine",
    server,
    members,
    invites,
  }

  await writeFile(
    `./exports/${new Date().toISOString()} - ${serverId}.json`,
    JSON.stringify(backup),
  );

  await mongo()
    .db("revolt")
    .collection<Server>("servers")
    .updateOne(
      { _id: serverId },
      {
        $set: {
          owner: "0".repeat(26),
          analytics: false,
          discoverable: false,
        }
      }
    );

  await mongo()
    .db("revolt")
    .collection<Member>("server_members")
    .deleteMany({ "_id.server": serverId });

  await mongo()
    .db("revolt")
    .collection<Invite>("channel_invites")
    .deleteMany({ type: "Server", server: serverId });

  await publishMessage(serverId, {
    type: "ServerDelete",
    id: serverId,
  });

  while (members.length) {
    const m = members.splice(0, 50);

    await Promise.allSettled(
      m.map(async (member) => {
        const messageId = ulid();
  
        let dm = await findDM(PLATFORM_MOD_ID, member._id.user);
        if (!dm) dm = await createDM(PLATFORM_MOD_ID, member._id.user, messageId);
      
        await sendChatMessage({
          _id: messageId,
          author: PLATFORM_MOD_ID,
          channel: dm._id,
          content: message,
        });
      })
    );
  }
}

export async function deleteInvite(invite: string) {
  await checkPermission("channels/update/invites", invite);

  if (!invite) throw new Error("invite is empty");

  await mongo()
    .db("revolt")
    .collection<ChannelInvite>("channel_invites")
    .deleteOne({ _id: invite });
}

export async function editInvite(invite: string, newInvite: string) {
  await checkPermission("channels/update/invites", { invite, newInvite });

  if (!invite) throw new Error("invite is empty");
  if (!newInvite) throw new Error("new invite is empty");

  const { value } = await mongo()
    .db("revolt")
    .collection<ChannelInvite>("channel_invites")
    .findOneAndDelete({ _id: invite });

  if (!value) throw new Error("invite doesn't exist");

  await mongo()
    .db("revolt")
    .collection<ChannelInvite>("channel_invites")
    .insertOne({ ...value, _id: newInvite });
}

export async function editInviteChannel(invite: string, newChannel: string) {
  await checkPermission("channels/update/invites", { invite, newChannel });

  if (!invite) throw new Error("invite is empty");

  await mongo()
    .db("revolt")
    .collection<ChannelInvite>("channel_invites")
    .updateOne({ _id: invite }, { $set: { channel: newChannel } });
}

export async function createInvite(invite: ChannelInvite) {
  await checkPermission("channels/update/invites", invite);

  await mongo()
    .db("revolt")
    .collection<ChannelInvite>("channel_invites")
    .insertOne(invite);
}

export async function bulkDeleteInvites(invites: string[]) {
  await checkPermission("channels/update/invites", invites);

  await mongo()
    .db("revolt")
    .collection<ChannelInvite>("channel_invites")
    .deleteMany({ _id: { $in: invites } });
}

export async function updateBotDiscoverability(botId: string, state: boolean) {
  await checkPermission("bots/update/discoverability", botId, { state });
  await mongo()
    .db("revolt")
    .collection<Bot>("bots")
    .updateOne(
      {
        _id: botId,
      },
      {
        $set: {
          analytics: state,
          discoverable: state,
        },
      }
    );
}

export async function resetBotToken(botId: string) {
  await checkPermission("bots/update/reset-token", { botId });

  // Should generate tokens the exact same as the backend generates them:
  // https://github.com/revoltchat/backend/blob/41f20c2239ed6307ad821b321d13240dc6ff3327/crates/core/database/src/models/bots/model.rs#L106

  await mongo()
    .db("revolt")
    .collection<Bot>("bots")
    .updateOne(
      {
        _id: botId,
      },
      {
        $set: {
          token: nanoid(64),
        },
      },
    );
}

export async function transferBot(botId: string, ownerId: string, resetToken: boolean) {
  await checkPermission("bots/update/owner", { botId, ownerId, resetToken });

  if (resetToken) {
    await checkPermission("bots/update/reset-token", { botId });
  }

  await mongo()
    .db("revolt")
    .collection<Bot>("bots")
    .updateOne(
      {
        _id: botId,
      },
      {
        $set: {
          owner: ownerId,
          ...(
            resetToken
              ? {
                token: nanoid(64),
              }
              : {}
          ),
        },
      },
    );

  await mongo()
    .db("revolt")
    .collection<User>("users")
    .updateOne(
      {
        _id: botId,
      },
      {
        $set: {
          "bot.owner": ownerId,
        },
      },
    );

  // This doesn't appear to work, maybe Revite can't handle it. I'll leave it in regardless.
  await publishMessage(
    botId,
    {
      type: "UserUpdate",
      id: botId,
      data: {
        bot: {
          owner: ownerId,
        },
      },
    },
  );
}

export async function restoreAccount(accountId: string) {
  if (RESTRICT_ACCESS_LIST.includes(accountId)) throw "restricted access";
  await checkPermission("accounts/restore", accountId);
  await mongo()
    .db("revolt")
    .collection<Account>("accounts")
    .updateOne(
      {
        _id: accountId,
      },
      {
        $unset: {
          disabled: 1,
        },
      }
    );
}

export async function queueAccountDeletion(accountId: string) {
  if (RESTRICT_ACCESS_LIST.includes(accountId)) throw "restricted access";
  await checkPermission("accounts/deletion/queue", accountId);
  const twoWeeksFuture = new Date();
  twoWeeksFuture.setDate(twoWeeksFuture.getDate() + 14);

  const $set: Partial<Account> = {
    disabled: true,
    deletion: {
      status: "Scheduled",
      after: twoWeeksFuture.toISOString(),
    },
  };

  await disableAccount(accountId);
  await mongo().db("revolt").collection<Account>("accounts").updateOne(
    {
      _id: accountId,
    },
    {
      $set,
    }
  );

  return $set;
}

export async function cancelAccountDeletion(accountId: string) {
  if (RESTRICT_ACCESS_LIST.includes(accountId)) throw "restricted access";
  await checkPermission("accounts/deletion/cancel", accountId);
  await mongo()
    .db("revolt")
    .collection<Account>("accounts")
    .updateOne(
      {
        _id: accountId,
      },
      {
        $unset: {
          deletion: 1,
        },
      }
    );
}

export async function fetchBackups() {
  await checkPermission("backup/fetch", null);

  return await Promise.all(
    (await readdir("./exports", { withFileTypes: true }))
      .filter((file) => file.isFile() && file.name.endsWith(".json"))
      .map(async (file) => {
        let type: string | null = null;
        try {
          type = JSON.parse((await readFile(`./exports/${file.name}`)).toString("utf-8"))._event;
        } catch(e) {}

        return { name: file.name, type: type }
      })
  );
}

export async function fetchBackup(name: string) {
  await checkPermission("backup/fetch/by-name", null);

  return JSON.parse((await readFile(`./exports/${name}`)).toString("utf-8"));
}

export async function fetchEmailClassifications(): Promise<EmailClassification[]> {
  await checkPermission("authifier/classification/fetch", null);

  return await mongo()
    .db("authifier")
    .collection<EmailClassification>("email_classification")
    .find({})
    .toArray();
}

export async function createEmailClassification(domain: string, classification: string) {
  await checkPermission("authifier/classification/create", { domain, classification });

  await mongo()
    .db("authifier")
    .collection<EmailClassification>("email_classification")
    .insertOne(
      { _id: domain, classification },
    );
}

export async function updateEmailClassification(domain: string, classification: string) {
  await checkPermission("authifier/classification/update", { domain, classification });

  await mongo()
    .db("authifier")
    .collection<EmailClassification>("email_classification")
    .updateOne(
      { _id: domain },
      { $set: { classification } },
    );
}

export async function deleteEmailClassification(domain: string) {
  await checkPermission("authifier/classification/delete", domain);

  await mongo()
    .db("authifier")
    .collection<EmailClassification>("email_classification")
    .deleteOne(
      { _id: domain },
    );
}
