import { assertNoLoneSurrogates } from "./strict-json";
import type { JsonValue, Proof, SignerMaterial } from "./browser-types";

const encoder = new TextEncoder();
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const ED25519_MULTICODEC = new Uint8Array([0xed, 0x01]);

export function bufferSource(bytes: Uint8Array): ArrayBuffer {
  return Uint8Array.from(bytes).buffer as ArrayBuffer;
}

export function canonicalize(value: JsonValue): string {
  if (value === null || typeof value === "boolean") return JSON.stringify(value);
  if (typeof value === "string") {
    assertNoLoneSurrogates(value);
    return JSON.stringify(value);
  }
  if (typeof value === "number") {
    if (!Number.isSafeInteger(value)) throw new TypeError("canonical JSON permits safe integers only");
    return JSON.stringify(value);
  }
  if (Array.isArray(value)) return `[${value.map((item) => canonicalize(item)).join(",")}]`;
  const keys = Object.keys(value).sort();
  return `{${keys
    .map((key) => {
      assertNoLoneSurrogates(key);
      return `${JSON.stringify(key)}:${canonicalize(value[key] as JsonValue)}`;
    })
    .join(",")}}`;
}

export function utf8(value: string): Uint8Array {
  return encoder.encode(value);
}

export function bytesToHex(bytes: Uint8Array): string {
  return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
}

export function bytesToBase64(bytes: Uint8Array): string {
  let binary = "";
  for (const byte of bytes) binary += String.fromCharCode(byte);
  return btoa(binary);
}

export function base64ToBytes(value: string): Uint8Array {
  if (
    value.length === 0 ||
    value.length % 4 !== 0 ||
    !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
      value,
    )
  ) {
    throw new TypeError("base64 value is not canonical padded encoding");
  }
  const binary = atob(value);
  const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
  if (bytesToBase64(bytes) !== value) {
    throw new TypeError("base64 value is not canonical padded encoding");
  }
  return bytes;
}

export function base64Url(bytes: Uint8Array): string {
  return bytesToBase64(bytes).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
}

export async function sha256Bytes(bytes: Uint8Array): Promise<string> {
  const digest = await crypto.subtle.digest("SHA-256", bufferSource(bytes));
  return `sha256:${bytesToHex(new Uint8Array(digest))}`;
}

export async function sha256Json(value: JsonValue): Promise<string> {
  return sha256Bytes(utf8(canonicalize(value)));
}

export async function leafHash(value: string): Promise<string> {
  const body = utf8(value);
  const prefixed = new Uint8Array(body.byteLength + 1);
  prefixed[0] = 0;
  prefixed.set(body, 1);
  return sha256Bytes(prefixed);
}

function base58(bytes: Uint8Array): string {
  const digits = [0];
  for (const byte of bytes) {
    let carry = byte;
    for (let index = 0; index < digits.length; index += 1) {
      carry += (digits[index] as number) * 256;
      digits[index] = carry % 58;
      carry = Math.floor(carry / 58);
    }
    while (carry > 0) {
      digits.push(carry % 58);
      carry = Math.floor(carry / 58);
    }
  }
  let zeros = 0;
  while (zeros < bytes.length && bytes[zeros] === 0) zeros += 1;
  return `${"1".repeat(zeros)}${digits
    .reverse()
    .map((digit) => BASE58_ALPHABET[digit])
    .join("")}`;
}

export async function generateSigner(): Promise<SignerMaterial> {
  const keyPair = (await crypto.subtle.generateKey(
    { name: "Ed25519" },
    true,
    ["sign", "verify"],
  )) as CryptoKeyPair;
  const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
  const privateJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
  const raw = new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey));
  const didBytes = new Uint8Array(ED25519_MULTICODEC.length + raw.length);
  didBytes.set(ED25519_MULTICODEC);
  didBytes.set(raw, ED25519_MULTICODEC.length);
  return {
    issuer: `did:key:z${base58(didBytes)}`,
    publicJwk,
    privateJwk,
  };
}

export function signerFromPrivateJwk(privateJwk: JsonWebKey): SignerMaterial {
  if (
    privateJwk.kty !== "OKP" ||
    privateJwk.crv !== "Ed25519" ||
    typeof privateJwk.x !== "string" ||
    typeof privateJwk.d !== "string"
  ) {
    throw new TypeError("signing JWK must be an Ed25519 OKP private key");
  }
  const normalized = privateJwk.x.replaceAll("-", "+").replaceAll("_", "/");
  const raw = base64ToBytes(`${normalized}${"=".repeat((4 - (normalized.length % 4)) % 4)}`);
  const didBytes = new Uint8Array(ED25519_MULTICODEC.length + raw.length);
  didBytes.set(ED25519_MULTICODEC);
  didBytes.set(raw, ED25519_MULTICODEC.length);
  const publicJwk: JsonWebKey = {
    kty: "OKP",
    crv: "Ed25519",
    x: privateJwk.x,
    ext: true,
  };
  return {
    issuer: `did:key:z${base58(didBytes)}`,
    publicJwk,
    privateJwk,
  };
}

export function proofPreimage(
  purpose: Proof["purpose"],
  digest: string,
  schema = "0.4",
): Uint8Array {
  return utf8(
    canonicalize({
      context: "bulla-proof",
      schema,
      purpose,
      digest,
    }),
  );
}

export async function signDomain(
  signer: SignerMaterial,
  purpose: Proof["purpose"],
  digest: string,
  schema = "0.4",
): Promise<Proof> {
  const key = await crypto.subtle.importKey(
    "jwk",
    signer.privateJwk,
    { name: "Ed25519" },
    false,
    ["sign"],
  );
  const signature = await crypto.subtle.sign(
    { name: "Ed25519" },
    key,
    bufferSource(proofPreimage(purpose, digest, schema)),
  );
  return {
    type: "bulla/ed25519-2026",
    purpose,
    issuer: signer.issuer,
    verificationMethod: signer.issuer,
    proofValue: bytesToBase64(new Uint8Array(signature)),
  };
}

export async function verifyDomain(
  publicJwk: JsonWebKey,
  proof: Proof,
  expectedPurpose: Proof["purpose"],
  digest: string,
  schema = "0.4",
): Promise<boolean> {
  if (proof.purpose !== expectedPurpose) return false;
  const key = await crypto.subtle.importKey(
    "jwk",
    publicJwk,
    { name: "Ed25519" },
    false,
    ["verify"],
  );
  return crypto.subtle.verify(
    { name: "Ed25519" },
    key,
    bufferSource(base64ToBytes(proof.proofValue)),
    bufferSource(proofPreimage(expectedPurpose, digest, schema)),
  );
}

export function randomToken(bytes = 32): string {
  const value = new Uint8Array(bytes);
  crypto.getRandomValues(value);
  return base64Url(value);
}

export function randomUuid(): string {
  return crypto.randomUUID().toLowerCase();
}

export async function constantTimeDigestEqual(left: string, right: string): Promise<boolean> {
  const a = utf8(left);
  const b = utf8(right);
  const length = Math.max(a.length, b.length);
  let difference = a.length ^ b.length;
  for (let index = 0; index < length; index += 1) {
    difference |= (a[index] ?? 0) ^ (b[index] ?? 0);
  }
  await Promise.resolve();
  return difference === 0;
}
