import getPermissions from "./permissions.js";
import { jsonError } from "./common.js";

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 refreshAuth(context: RequestContext) {
  const { current_user: currentUser } = context.data;

  if (!currentUser || currentUser.refresh_at > Date.now())
    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 userData: { [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();

  userData = {
    ...userData,
    ...(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,
  );

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

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

  delete context.data.sid;

  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 (
    context.request.method === "POST" &&
    !context.request.url.endsWith("/api/infractions/new")
  ) {
    if (context.request.headers.get("content-type") !== "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) =>
    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 = [
  setAuth,
  refreshAuth,
  setTheme,
  constructHTML,
  setBody,
  setHeaders,
];