export function getErrorMessage(error: unknown) {
  if (
    typeof error === "object" &&
    error !== null &&
    "response" in error &&
    typeof error.response === "object" &&
    error.response !== null &&
    "data" in error.response
  ) {
    const data = error.response.data;
    if (typeof data === "string") {
      return data;
    }
    if (
      typeof data === "object" &&
      data !== null &&
      "message" in data &&
      typeof data.message === "string"
    ) {
      return data.message;
    }
  }

  if (error instanceof Error) {
    return error.message;
  }

  return "Request failed. Please try again.";
}

export function fieldValue(form: FormData, name: string) {
  return String(form.get(name) ?? "").trim();
}

export function passwordValue(form: FormData, name: string) {
  return String(form.get(name) ?? "");
}

type DerReader = {
  bytes: Uint8Array;
  offset: number;
};

function readLength(reader: DerReader) {
  const first = reader.bytes[reader.offset++];
  if (first < 0x80) {
    return first;
  }

  const byteCount = first & 0x7f;
  let length = 0;
  for (let index = 0; index < byteCount; index += 1) {
    length = (length << 8) | reader.bytes[reader.offset++];
  }
  return length;
}

function readValue(reader: DerReader, tag: number) {
  if (reader.bytes[reader.offset++] !== tag) {
    throw new Error("Invalid RSA public key.");
  }

  const length = readLength(reader);
  const value = reader.bytes.slice(reader.offset, reader.offset + length);
  reader.offset += length;
  return value;
}

function readSequence(reader: DerReader) {
  const value = readValue(reader, 0x30);
  return { bytes: value, offset: 0 };
}

function readInteger(reader: DerReader) {
  let value = readValue(reader, 0x02);
  while (value.length > 1 && value[0] === 0) {
    value = value.slice(1);
  }
  return value;
}

function pemToDer(publicKey: string) {
  const base64 = publicKey
    .replace(/-----BEGIN [^-]+-----/g, "")
    .replace(/-----END [^-]+-----/g, "")
    .replace(/\s/g, "");
  const binary = atob(base64);
  return Uint8Array.from(binary, (char) => char.charCodeAt(0));
}

function parseRsaPublicKey(publicKey: string) {
  const topLevel = readSequence({ bytes: pemToDer(publicKey), offset: 0 });

  if (topLevel.bytes[topLevel.offset] === 0x02) {
    return {
      modulus: readInteger(topLevel),
      exponent: readInteger(topLevel),
    };
  }

  readSequence(topLevel);
  const bitString = readValue(topLevel, 0x03);
  const nestedKey = bitString.slice(1);
  const nested = readSequence({ bytes: nestedKey, offset: 0 });
  return {
    modulus: readInteger(nested),
    exponent: readInteger(nested),
  };
}

function bytesToBigInt(bytes: Uint8Array) {
  return BigInt(`0x${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`);
}

function bigIntToBytes(value: bigint, length: number) {
  const hex = value.toString(16).padStart(length * 2, "0");
  const bytes = new Uint8Array(length);
  for (let index = 0; index < length; index += 1) {
    bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
  }
  return bytes;
}

function modPow(base: bigint, exponent: bigint, modulus: bigint) {
  let result = 1n;
  let power = base % modulus;
  let exp = exponent;

  while (exp > 0n) {
    if (exp % 2n === 1n) {
      result = (result * power) % modulus;
    }
    exp /= 2n;
    power = (power * power) % modulus;
  }

  return result;
}

function randomNonZeroBytes(length: number) {
  const bytes = new Uint8Array(length);
  for (let index = 0; index < length; index += 1) {
    let value = 0;
    while (value === 0) {
      value = crypto.getRandomValues(new Uint8Array(1))[0];
    }
    bytes[index] = value;
  }
  return bytes;
}

function bytesToBase64(bytes: Uint8Array) {
  let binary = "";
  bytes.forEach((byte) => {
    binary += String.fromCharCode(byte);
  });
  return btoa(binary);
}

export function encryptPassword(publicKey: string, password: string) {
  const { modulus, exponent } = parseRsaPublicKey(publicKey);
  const keySize = modulus.length;
  const message = new TextEncoder().encode(password);

  if (message.length > keySize - 11) {
    throw new Error("Password is too long.");
  }

  const padded = new Uint8Array(keySize);
  padded[0] = 0;
  padded[1] = 2;
  padded.set(randomNonZeroBytes(keySize - message.length - 3), 2);
  padded[keySize - message.length - 1] = 0;
  padded.set(message, keySize - message.length);

  const encrypted = modPow(
    bytesToBigInt(padded),
    bytesToBigInt(exponent),
    bytesToBigInt(modulus),
  );

  return bytesToBase64(bigIntToBytes(encrypted, keySize));
}
