import getPermissions from "./permissions.js";
import { jsonError } from "./common.js";
import { PrismaClient } from "../generated/prisma/client.js";
import { PrismaD1 } from "@prisma/adapter-d1";
import * as Sentry from "@sentry/cloudflare";

async function constructHTML(context: RequestContext) {
  const { pathname } = new URL(context.request.url);

  if (pathname.startsWith("/api/")) return await context.next();

  if (
    pathname.startsWith("/assets/") ||
    ["/app.webmanifest", "/favicon.ico", "/robots.txt"].includes(pathname) ||
    pathname.startsWith("/files/")
  )
    return await context.env.ASSETS.fetch(context.request);

  return await context.next();
}

async function generateTokenHash(token: string) {
  const hash = await crypto.subtle.digest(
    "SHA-512",
    new TextEncoder().encode(token),
  );
  return btoa(String.fromCharCode(...new Uint8Array(hash)))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=/g, "");
}

async function initializePrisma(context: RequestContext) {
  const adapter = new PrismaD1(context.env.D1);
  context.data.prisma = new PrismaClient({ adapter });

  return await context.next();
}

async function mxAndBypassCheck(context: RequestContext) {
  if (!(await context.env.DATA.get("mx"))) return await context.next();
  const cookies = context.request.headers.get("cookie")?.split(/; */);
  const isAPI = new URL(context.request.url).pathname.startsWith("/api");

  if (!cookies?.length || !cookies.find((c) => c.startsWith("mxb="))) {
    if (isAPI) return jsonError("API is undergoing maintenance", 503);

    context.data.mx = true;
  }

  return await context.next();
}

async function refreshAuth(context: RequestContext) {
  const { current_user: currentUser } = context.data;

  if (!currentUser) return await context.next();

  if (currentUser.refresh_at > Date.now()) {
    if (
      [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(
        (p) => currentUser.permissions & (1 << p),
      )
    )
      Sentry.setUser({
        email: currentUser.email,
        id: currentUser.id,
        ip_address: context.request.headers.get("cf-connecting-ip"),
        username: currentUser.username,
      });
  }

  const oauthData = await context.env.DATA.get(
    `oauthcredentials_${currentUser.id}`,
    { type: "json" },
  );

  if (!oauthData) return await context.next();

  const refreshedTokenResponse = await fetch(
    "https://discord.com/api/v10/oauth2/token",
    {
      body: `grant_type=refresh_token&refresh_token=${currentUser.refresh_token}`,
      headers: {
        authorization: `Basic ${btoa(
          context.env.DISCORD_ID + ":" + context.env.DISCORD_SECRET,
        )}`,
        "content-type": "application/x-www-form-urlencoded",
      },
      method: "POST",
    },
  );

  if (!refreshedTokenResponse.ok) return await context.next();

  const accessData: { [k: string]: any } = await refreshedTokenResponse.json();

  let tokenData: { [k: string]: any } = {
    ...accessData,
    refresh_at: Date.now() + 3600000,
  };

  const newDiscordData = await fetch("https://discord.com/api/v10/users/@me", {
    headers: {
      authorization: `Bearer ${accessData.access_token}`,
    },
  });

  if (!newDiscordData.ok) return await context.next();

  const userData: { [k: string]: any } = await newDiscordData.json();

  const updatedServerMemberReq = await fetch(
    "https://discord.com/api/v10/users/@me/guilds/242263977986359297/member",
    {
      headers: {
        authorization: `Bearer ${accessData.access_token}`,
      },
    },
  );

  userData.permissions = await getPermissions(
    userData.id,
    context,
    updatedServerMemberReq.ok
      ? (
          (await updatedServerMemberReq.json()) as {
            [k: string]: any;
          }
        ).roles
      : undefined,
  );

  if (
    [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(
      (p) => currentUser.permissions & (1 << p),
    )
  )
    Sentry.setUser({
      email: currentUser.email,
      id: currentUser.id,
      ip_address: context.request.headers.get("cf-connecting-ip"),
      username: currentUser.username,
    });

  const tokenHash = await generateTokenHash(context.data.sid);

  await context.env.DATA.put(`auth_${tokenHash}`, JSON.stringify(userData), {
    expirationTtl: accessData.expires_in,
  });
  await context.env.DATA.put(
    `oauthcredentials_${userData.id}`,
    JSON.stringify(tokenData),
    {
      expirationTtl: 1209600000,
    },
  );

  delete context.data.sid;

  const jwtPayload = context.request.headers
    .get("authorization")
    ?.replace("Bearer ", "")
    .split(".")
    .at(1);

  if (jwtPayload) {
    let jwtData: { [k: string]: any };

    try {
      jwtData = JSON.parse(
        atob(jwtPayload.replaceAll("-", "+").replaceAll("_", "/")),
      );
    } catch {
      return jsonError("JWT is malformed", 400);
    }

    jwtData.email = userData.email;
    jwtData.exp = Math.floor(Date.now() / 1000) + userData.expires_in;
    jwtData.iat = Math.floor(Date.now() / 1000);
    jwtData.name = userData.username;
    jwtData.permissions = userData.permissions;
    jwtData.picture =
      userData.avatar ?? "https://carcrushers.cc/files/logo192.png";

    const key = await crypto.subtle.importKey(
      "raw",
      // @ts-expect-error
      Uint8Array.from(
        atob(
          context.env.JWT_SIGNING_KEY.replaceAll("-", "+").replaceAll("_", "/"),
        ),
        (m) => m.codePointAt(0),
      ),
      { hash: "SHA-256", name: "HMAC" },
      false,
      ["sign"],
    );

    const jwtBase = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.${btoa(
      JSON.stringify(jwtData),
    )
      .replaceAll("+", "-")
      .replaceAll("/", "_")
      .replaceAll("=", "")}`;

    const signature = btoa(
      String.fromCodePoint(
        ...new Uint8Array(
          await crypto.subtle.sign(
            "HMAC",
            key,
            new TextEncoder().encode(jwtBase),
          ),
        ),
      ),
    )
      .replaceAll("+", "-")
      .replace("/", "_")
      .replaceAll("=", "");

    const response = await context.next();

    response.headers.set("refreshed-token", `${jwtBase}.${signature}`);

    return response;
  }

  return await context.next();
}

async function setAuth(context: RequestContext) {
  const cookies = context.request.headers.get("cookie");
  const auth = context.request.headers.get("authorization");

  if (auth) {
    const jwtSegments = auth.replace("Bearer ", "").split(".");

    if (jwtSegments.length !== 3) return jsonError("Malformed token", 401);

    const { alg } = JSON.parse(atob(jwtSegments[0]));

    if (alg !== "HS256") return jsonError("Invalid token", 400);

    const key = await crypto.subtle.importKey(
      "raw",
      // @ts-expect-error
      Uint8Array.from(
        atob(
          context.env.JWT_SIGNING_KEY.replaceAll("-", "+").replaceAll("_", "/"),
        ),
        (m) => m.codePointAt(0),
      ),
      { hash: "SHA-256", name: "HMAC" },
      false,
      ["verify"],
    );

    if (
      !(await crypto.subtle.verify(
        "HMAC",
        key,
        // @ts-expect-error
        Uint8Array.from(
          atob(jwtSegments[2].replaceAll("-", "+").replaceAll("_", "/")),
          (m) => m.codePointAt(0),
        ),
        new TextEncoder().encode(`${jwtSegments[0]}.${jwtSegments[1]}`),
      ))
    )
      return jsonError("Token could not be verified", 401);

    const {
      jti: sessionToken,
    }: {
      jti: string;
    } = JSON.parse(
      atob(jwtSegments[1].replaceAll("-", "+").replaceAll("_", "/")),
    );

    const linkedSessionData = await context.env.DATA.get(
      `auth_${await generateTokenHash(sessionToken)}`,
    );

    if (linkedSessionData) {
      context.data.current_user = JSON.parse(linkedSessionData);
      context.data.sid = sessionToken;

      return await context.next();
    } else return jsonError("Session is invalid or expired", 401);
  }

  if (!cookies) return await context.next();

  const cookieList = cookies.split(/; /);

  for (const c of cookieList) {
    const [name, value] = c.split("=");

    if (name !== "_s") continue;

    const userData = await context.env.DATA.get(
      `auth_${await generateTokenHash(value)}`,
    );

    if (userData) {
      context.data.current_user = JSON.parse(userData);
      context.data.sid = value;
    } else
      context.request.headers.append(
        "set-cookie",
        "_s=; HttpOnly; Max-Age=0; Path=/; Secure;",
      );

    break;
  }

  return await context.next();
}

async function setBody(context: RequestContext) {
  if (
    ["PATCH", "POST", "PUT"].includes(context.request.method) &&
    !context.request.url.endsWith("/api/infractions/new") &&
    !context.request.url.endsWith("/api/st")
  ) {
    if (
      !context.request.headers
        .get("content-type")
        ?.startsWith("application/json")
    )
      return new Response('{"error":"Invalid content-type"}', {
        headers: {
          "content-type": "application/json",
        },
        status: 400,
      });

    let body: { [k: string]: any };

    try {
      body = await context.request.json();
    } catch {
      return new Response('{"error":"Invalid JSON"}', {
        headers: {
          "content-type": "application/json",
        },
        status: 400,
      });
    }

    context.data.body = body;
  }

  return await context.next();
}

async function setHeaders(context: RequestContext) {
  const response = await context.next();
  const rtvValues = [
    "Aldaria",
    "Altadena",
    "DEMA",
    "Dragonborn",
    "Heaven, Iowa",
    "Hollywood",
    "Parkway East",
    "Parkway North",
    "Parkway West",
    "Tokyo",
    "Wintervale",
  ];

  response.headers.set("Permissions-Policy", "clipboard-write=(self)");
  response.headers.set("Referrer-Policy", "same-origin");
  response.headers.set(
    "RTV",
    rtvValues[Math.round(Math.random() * (rtvValues.length - 1))],
  );
  response.headers.set("X-Frame-Options", "SAMEORIGIN");

  return response;
}

async function setTheme(context: RequestContext) {
  const cookies = context.request.headers.get("cookie");

  if (!cookies) {
    context.data.theme = "dark";
    return await context.next();
  }

  const cookieList = cookies.split("; ");

  const themeCookie = cookieList.find((c: string) =>
    c.startsWith("chakra-ui-color-mode"),
  );
  const theme = themeCookie?.split("=").at(1);

  if (!theme || !["dark", "light"].includes(theme)) context.data.theme = "dark";
  else context.data.theme = theme;

  return await context.next();
}

export const onRequest = [
  Sentry.sentryPagesPlugin((context: RequestContext) => ({
    beforeSend(event) {
      delete event.request?.cookies;
      return event;
    },
    dsn: context.env.FUNCTIONS_DSN,
    release: context.env.COMMIT_SHA,
    sendDefaultPii: true,
  })),
  mxAndBypassCheck,
  setAuth,
  refreshAuth,
  setTheme,
  constructHTML,
  setBody,
  initializePrisma,
  setHeaders,
];
