"use server";

import { Filter, MongoClient, WithId } from "mongodb";
import type {
  AccountStrike,
  Bot,
  Channel,
  Invite,
  Message,
  Report,
  Server,
  SessionInfo,
  SnapshotContent,
  User,
} from "revolt-api";
import { ulid } from "ulid";
import { publishMessage } from "./redis";
import { checkPermission, hasPermissionFromSession } from "./accessPermissions";
import { PLATFORM_MOD_ID } from "./constants";
import { getServerSession } from "next-auth";

let client: MongoClient;

function mongo() {
  if (!client) {
    client = new MongoClient(process.env.MONGODB!);
  }

  return client;
}

export default mongo;

export async function insertAuditLog(
  permission: string,
  context: any,
  args?: any
) {
  const session = await getServerSession();
  if (!session!.user!.email!) throw "invalid procedure call";

  await mongo()
    .db("revolt")
    .collection<{
      _id: string;
      moderator: string;
      permission: string;
      context: any;
      args: any;
    }>("safety_audit")
    .insertOne({
      _id: ulid(),
      moderator: session!.user!.email!,
      permission,
      context,
      args,
    });
}

export async function fetchBotById(id: string) {
  await checkPermission("bots/fetch/by-id", id);

  return await mongo()
    .db("revolt")
    .collection<Bot>("bots")
    .findOne({ _id: id });
}

export type Account = {
  _id: string;
  email: string;
  email_normalised: string;
  verification:
    | {
        status: "Verified";
      }
    | {
        status: "Pending";
        token: string;
        expiry: string;
      }
    | {
        status: "Moving";
        new_email: string;
        token: string;
        expiry: string;
      };
  password_reset: null | {
    token: string;
    expiry: string;
  };
  deletion:
    | null
    | {
        status: "WaitingForVerification";
        token: string;
        expiry: string;
      }
    | {
        status: "Scheduled";
        after: string;
      };
  disabled: boolean;
  lockout: null | {
    attempts: number;
    expiry: string;
  };
  mfa?: {
    totp_token?: {
      status: "Pending" | "Enabled";
    };
    recovery_codes?: number;
  };
};

export async function fetchAccountById(id: string) {
  await checkPermission("accounts/fetch/by-id", id);

  return await mongo()
    .db("revolt")
    .collection<Account>("accounts")
    .aggregate(
      [
        {
          $match: { _id: id },
        },
        {
          $project: {
            password: 0,
            "mfa.totp_token.secret": 0,
          }
        },
        {
          $set: {
            // Replace recovery code array with amount of codes
            "mfa.recovery_codes": {
              $cond: {
                if: { $isArray: "$mfa.recovery_codes" },
                then: { $size: "$mfa.recovery_codes", },
                else: undefined,
              }
            }
          }
        }
      ]
    ).next() as WithId<Account>;
}

export async function fetchSessionsByAccount(accountId: string) {
  await checkPermission("sessions/fetch/by-account-id", accountId);

  return await mongo()
    .db("revolt")
    .collection<SessionInfo>("sessions")
    .find(
      { user_id: accountId },
      {
        projection: {
          token: 0,
        },
        sort: {
          _id: -1,
        },
      }
    )
    .toArray();
}

export async function fetchUserById(id: string) {
  await checkPermission("users/fetch/by-id", id);

  return await mongo()
    .db("revolt")
    .collection<User>("users")
    .findOne(
      { _id: id },
      {
        projection: (await hasPermissionFromSession("users/fetch/relations"))
          ? undefined
          : {
              relations: 0,
            },
      }
    );
}

export async function fetchUsersById(ids: string[]) {
  await checkPermission("users/fetch/by-id", ids);

  return await mongo()
    .db("revolt")
    .collection<User>("users")
    .find(
      { _id: { $in: ids } },
      {
        projection: (await hasPermissionFromSession("users/fetch/relations"))
          ? undefined
          : {
              relations: 0,
            },
      }
    )
    .toArray();
}

export async function fetchChannelById(id: string) {
  await checkPermission("channels/fetch/by-id", id);

  return await mongo()
    .db("revolt")
    .collection<Channel>("channels")
    .findOne({ _id: id });
}

export async function fetchChannels(query: Filter<Channel>) {
  await checkPermission("channels/fetch", query);

  return await mongo()
    .db("revolt")
    .collection<Channel>("channels")
    .find(query)
    .toArray();
}

export async function findDM(user_a: string, user_b: string) {
  await checkPermission("channels/fetch/dm", { user_a, user_b });

  return await mongo()
    .db("revolt")
    .collection<Channel & { channel_type: "DirectMessage" }>("channels")
    .findOne({
      channel_type: "DirectMessage",
      recipients: {
        $all: [user_a, user_b],
      },
    });
}

export async function createDM(
  userA: string,
  userB: string,
  lastMessageId?: string
) {
  await checkPermission("channels/create/dm", { userA, userB }, lastMessageId);

  const newChannel: Channel & { channel_type: "DirectMessage" } = {
    _id: ulid(),
    channel_type: "DirectMessage",
    active: typeof lastMessageId !== "undefined",
    recipients: [userA, userB],
  };

  await mongo()
    .db("revolt")
    .collection<Channel & { channel_type: "DirectMessage" }>("channels")
    .insertOne(newChannel);

  await publishMessage(userA + "!", {
    type: "ChannelCreate",
    ...newChannel,
  });

  await publishMessage(userB + "!", {
    type: "ChannelCreate",
    ...newChannel,
  });

  return newChannel;
}

export async function fetchServerById(id: string) {
  await checkPermission("servers/fetch/by-id", id);

  return await mongo()
    .db("revolt")
    .collection<Server>("servers")
    .findOne({ _id: id });
}

export async function fetchServers(query: Filter<Server>) {
  await checkPermission("servers/fetch", query);

  return await mongo()
    .db("revolt")
    .collection<Server>("servers")
    .find(query)
    .toArray();
}

// `vanity` should eventually be added to the backend as well
export type ChannelInvite = Invite & { vanity?: boolean }

export async function fetchInvites(query: Filter<ChannelInvite>) {
  await checkPermission("channels/fetch/invites", query);

  const invites = await mongo()
    .db("revolt")
    .collection<ChannelInvite>("channel_invites")
    .find(query)
    .toArray();

  const channels = await mongo()
    .db("revolt")
    .collection<Channel>("channels")
    .find({ _id: { $in: invites.map((invite) => invite.channel) } })
    .toArray();

  const users = await mongo()
  .db("revolt")
  .collection<User>("users")
  .find({ _id: { $in: invites.map((invite) => invite.creator) } })
  .toArray();

  return { invites, channels, users }
}

export async function fetchMessageById(id: string) {
  await checkPermission("messages/fetch/by-id", id);

  return await mongo()
    .db("revolt")
    .collection<Message>("messages")
    .findOne({ _id: id });
}

export async function fetchMessagesByUser(userId: string) {
  await checkPermission("messages/fetch/by-user", userId);

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

export async function fetchMessagesByChannel(channelId: string) {
  await checkPermission("messages/fetch/by-user", channelId);

  return await mongo()
    .db("revolt")
    .collection<Message>("messages")
    .find({ channel: channelId }, { sort: { _id: -1 }, limit: 50 })
    .toArray();
}

export async function fetchMessages(query: Filter<Message>, limit = 50) {
  await checkPermission("messages/fetch", query, { limit });

  return await mongo()
    .db("revolt")
    .collection<Message>("messages")
    .find(query, { sort: { _id: -1 }, limit })
    .toArray();
}

export async function fetchOpenReports() {
  await checkPermission("reports/fetch/open", "all");

  return await mongo()
    .db("revolt")
    .collection<Report>("safety_reports")
    .find(
      { status: "Created" },
      {
        sort: {
          _id: -1,
        },
      }
    )
    .toArray();
}

export async function fetchRelatedReportsByContent(contentId: string) {
  await checkPermission("reports/fetch/related/by-content", contentId);

  return await mongo()
    .db("revolt")
    .collection<Report>("safety_reports")
    .find(
      { status: "Created", "content.id": contentId },
      {
        sort: {
          _id: -1,
        },
      }
    )
    .toArray();
}

export async function fetchReportsByUser(userId: string) {
  await checkPermission("reports/fetch/related/by-user", userId);

  return await mongo()
    .db("revolt")
    .collection<Report>("safety_reports")
    .find(
      { author_id: userId },
      {
        sort: {
          _id: -1,
        },
      }
    )
    .toArray();
}

export async function fetchReportsAgainstUser(userId: string) {
  await checkPermission("reports/fetch/related/against-user", userId);

  const reportIdsInvolvingUser = await mongo()
    .db("revolt")
    .collection<{ _id: string; report_id: string; content: SnapshotContent }>(
      "safety_snapshots"
    )
    .find({
      $or: [{ "content._id": userId }, { "content.author": userId }],
    })
    .toArray()
    .then((snapshots) => [
      ...new Set(snapshots.map((snapshot) => snapshot.report_id)),
    ]);

  return await mongo()
    .db("revolt")
    .collection<Report>("safety_reports")
    .find(
      {
        _id: {
          $in: reportIdsInvolvingUser,
        },
      },
      {
        sort: {
          _id: -1,
        },
      }
    )
    .toArray();
}

export async function fetchReports(
  query: Filter<Report> = { status: "Created" }
) {
  await checkPermission("reports/fetch", query);

  return await mongo()
    .db("revolt")
    .collection<Report>("safety_reports")
    .find(query as never, {
      sort: {
        _id: -1,
      },
    })
    .toArray();
}

export async function fetchReportById(id: string) {
  await checkPermission("reports/fetch/by-id", id);

  return await mongo()
    .db("revolt")
    .collection<Report>("safety_reports")
    .findOne({ _id: id });
}

export async function fetchMembershipsByUser(userId: string) {
  await checkPermission("users/fetch/memberships", userId);

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

export async function fetchSnapshots(
  query: Filter<{ _id: string; report_id: string; content: SnapshotContent }>
) {
  await checkPermission("reports/fetch/snapshots", query);

  return await mongo()
    .db("revolt")
    .collection<{ _id: string; report_id: string; content: SnapshotContent }>(
      "safety_snapshots"
    )
    .find(query)
    .toArray();
}

export async function fetchSnapshotsByReport(reportId: string) {
  await checkPermission("reports/fetch/snapshots/by-report", reportId);

  return await mongo()
    .db("revolt")
    .collection<{ content: SnapshotContent }>("safety_snapshots")
    .find({ report_id: reportId })
    .toArray()
    .then((snapshots) => snapshots.map((snapshot) => snapshot!.content));
}

export async function fetchStrikesByUser(userId: string) {
  await checkPermission("users/fetch/strikes", userId);

  return await mongo()
    .db("revolt")
    .collection<AccountStrike>("safety_strikes")
    .find(
      { user_id: userId },
      {
        sort: {
          _id: -1,
        },
      }
    )
    .toArray();
}

export async function fetchNoticesByUser(userId: string) {
  await checkPermission("users/fetch/notices", userId);

  const dm = await mongo()
    .db("revolt")
    .collection<Channel & { channel_type: "DirectMessage" }>("channels")
    .findOne({
      channel_type: "DirectMessage",
      recipients: {
        $all: [userId, PLATFORM_MOD_ID],
      },
    });

  if (!dm) return [];

  return await mongo()
    .db("revolt")
    .collection<Message>("messages")
    .find({ channel: dm!._id }, { sort: { _id: -1 } })
    .toArray();
}

export async function fetchBotsByUser(userId: string) {
  await checkPermission("bots/fetch/by-user", userId);

  return await mongo()
    .db("revolt")
    .collection<Bot>("bots")
    .find({ owner: userId })
    .toArray();
}

export type EmailClassification = {
  _id: string;
  classification: string;
};

export async function fetchAuthifierEmailClassification(provider: string) {
  await checkPermission("authifier", provider);

  return await mongo()
    .db("authifier")
    .collection<EmailClassification>("email_classification")
    .findOne({ _id: provider });
}

export type SafetyNotes = {
  _id: { id: string, type: "message" | "channel" | "server" | "user" | "account" | "global" };
  text: string;
  edited_by: string;
  edited_at: Date;
}

export async function fetchSafetyNote(objectId: string, type: SafetyNotes["_id"]["type"]) {
  await checkPermission(`safety_notes/fetch/${type}`, objectId);

  return mongo()
    .db("revolt")
    .collection<SafetyNotes>("safety_notes")
    .findOne({ _id: { id: objectId, type: type } });
}

export async function updateSafetyNote(objectId: string, type: SafetyNotes["_id"]["type"], note: string) {
  await checkPermission(`safety_notes/update/${type}`, objectId);

  const session = await getServerSession();

  return mongo()
    .db("revolt")
    .collection<SafetyNotes>("safety_notes")
    .updateOne(
      { _id: { id: objectId, type: type } },
      {
        $set: {
          text: note,
          edited_at: new Date(Date.now()),
          edited_by: session?.user?.email ?? "",
        },
      },
      { upsert: true },
    );
}
