function arrBufToB64Url(buf: ArrayBuffer) {
  const b64data = btoa(String.fromCharCode(...new Uint8Array(buf)));
  return b64data.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}

function stringToBuffer(str: string) {
  const buffer = new ArrayBuffer(str.length);
  const ui8 = new Uint8Array(buffer);
  for (let i = 0, bufLen = str.length; i < bufLen; i++) {
    ui8[i] = str.charCodeAt(i);
  }
  return buffer;
}

export async function GenerateUploadURL(
  env: Env,
  path: string,
  size: number,
  fileExt: string
): Promise<string> {
  const accessToken = await GetAccessToken(env);
  const contentTypes: { [k: string]: string } = {
    gif: "image/gif",
    heic: "image/heic",
    heif: "image/heif",
    jfif: "image/jpeg",
    jpeg: "image/jpeg",
    jpg: "image/jpeg",
    m4v: "video/x-m4v",
    mkv: "video/x-matroska",
    mov: "video/quicktime",
    mp4: "video/mp4",
    png: "image/png",
    webp: "image/webp",
    webm: "video/webm",
    wmv: "video/x-ms-wmv",
  };

  const resumableUploadReq = await fetch(
    `https://storage.googleapis.com/upload/storage/v1/b/portal-carcrushers-cc/o?uploadType=resumable&name=${encodeURIComponent(
      path
    )}`,
    {
      headers: {
        authorization: `Bearer ${accessToken}`,
        "x-upload-content-type": contentTypes[fileExt],
        "x-upload-content-length": size.toString(),
      },
      method: "POST",
    }
  );

  if (!resumableUploadReq.ok)
    throw new Error(
      `Failed to create resumable upload: ${await resumableUploadReq.text()}`
    );

  const url = resumableUploadReq.headers.get("location");

  if (!url) throw new Error("No location header returned");

  return url;
}

async function GetAccessToken(env: Env): Promise<string> {
  const claimSet = btoa(
    JSON.stringify({
      aud: "https://oauth2.googleapis.com/token",
      exp: Math.floor(Date.now() / 1000) + 120,
      iat: Math.floor(Date.now() / 1000),
      iss: env.WORKER_GSERVICEACCOUNT,
      scope:
        "https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/devstorage.read_write",
    })
  )
    .replaceAll("+", "-")
    .replaceAll("/", "_")
    .replaceAll("=", "");

  const signingKey = await crypto.subtle.importKey(
    "pkcs8",
    stringToBuffer(atob(env.STORAGE_ACCOUNT_KEY.replace(/(\r\n|\n|\r)/gm, ""))),
    { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
    false,
    ["sign"]
  );
  const signature = await crypto.subtle.sign(
    { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
    signingKey,
    stringToBuffer(`eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.${claimSet}`)
  );
  const assertion = `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.${claimSet}.${arrBufToB64Url(
    signature
  )}`;
  const tokenRequest = await fetch("https://oauth2.googleapis.com/token", {
    body: `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=${assertion}`,
    headers: {
      "content-type": "application/x-www-form-urlencoded",
    },
    method: "POST",
  });

  if (!tokenRequest.ok)
    throw new Error(`Failed to get access token: ${await tokenRequest.text()}`);

  return ((await tokenRequest.json()) as { [k: string]: any }).access_token;
}

async function getKeyIDs(
  access_token: string,
  projectId: string,
  keys: { partitionId: { projectId: string }; path: { kind: string }[] }[]
) {
  const keyRequest = await fetch(
    `https://datastore.googleapis.com/v1/projects/${projectId}:allocateIds`,
    {
      body: JSON.stringify({ keys }),
      headers: {
        authorization: `Bearer ${access_token}`,
        "content-type": "application/json",
      },
      method: "POST",
    }
  );

  if (!keyRequest.ok) {
    console.log(await keyRequest.json());
    throw new Error("Failed to allocate key IDs");
  }

  return ((await keyRequest.json()) as { keys: { [k: string]: any }[] }).keys;
}

export async function insertLogs(
  userActionMap: { [k: string]: number },
  reportId: string,
  context: RequestContext
) {
  const accessToken = await GetAccessToken(context.env);
  const actionBaseURLs: { [k: number]: string } = {
    1: "https://portal.carcrushers.cc/view-closed-report/",
    2: "https://portal.carcrushers.cc/view-closed-report/",
    3: "",
    4: "https://portal.carcrushers.cc/game-appeals/",
  };
  const actionIntegers: { [k: number]: string } = {
    1: "blacklist",
    2: "ban",
    3: "revoke",
    4: "accept_appeal",
  };
  const incompleteLogKey = {
    partitionId: {
      projectId: context.env.DATASTORE_PROJECT,
    },
    path: [
      {
        kind: "log",
      },
    ],
  };
  const payload: { mode: string; mutations: { [k: string]: any }[] } = {
    mode: "NON_TRANSACTIONAL",
    mutations: [],
  };
  const preAllocatedLogKeys = [];

  while (preAllocatedLogKeys.length < Object.keys(userActionMap).length)
    preAllocatedLogKeys.push(incompleteLogKey);

  const keys = await getKeyIDs(
    accessToken,
    context.env.DATASTORE_PROJECT,
    preAllocatedLogKeys
  );

  for (const [user, action] of Object.entries(userActionMap)) {
    payload.mutations.push({
      insert: {
        key: keys.pop(),
        properties: {
          action: {
            stringValue: actionIntegers[action],
          },
          evidence: {
            stringValue: actionBaseURLs[action] + reportId,
          },
          executed_at: {
            integerValue: Date.now(),
          },
          executor: {
            stringValue: context.data.current_user.id,
          },
          target: {
            integerValue: user,
          },
        },
      },
    });
  }

  const mutationRequest = await fetch(
    `https://datastore.googleapis.com/v1/projects/${context.env.DATASTORE_PROJECT}:commit`,
    {
      body: JSON.stringify(payload),
      headers: {
        authorization: `Bearer ${accessToken}`,
        "content-type": "application/json",
      },
      method: "POST",
    }
  );

  if (!mutationRequest.ok) {
    console.log(await mutationRequest.json());
    throw new Error("Failed to commit mutation");
  }

  return await mutationRequest.json();
}

export async function queryLogs(user: number, context: RequestContext) {
  const accessToken = await GetAccessToken(context.env);

  const queryRequest = await fetch(
    `https://datastore.googleapis.com/v1/projects/${context.env.DATASTORE_PROJECT}:runQuery`,
    {
      body: JSON.stringify({
        partitionId: {
          projectId: context.env.DATASTORE_PROJECT,
        },
        query: {
          filter: {
            propertyFilter: {
              op: "EQUAL",
              property: {
                name: "target",
              },
              value: {
                integerValue: user.toString(),
              },
            },
          },
          kind: [
            {
              name: "log",
            },
          ],
        },
      }),
      headers: {
        authorization: `Bearer ${accessToken}`,
        "content-type": "application/json",
      },
      method: "POST",
    }
  );

  if (!queryRequest.ok) {
    console.log(await queryRequest.json());
    throw new Error("Failed to query logs");
  }

  return (
    (
      (await queryRequest.json()) as {
        batch: {
          entityResults: {
            cursor: string;
            entity: { [k: string]: any };
            version: string;
          }[];
        };
      }
    ).batch?.entityResults ?? []
  );
}
