function rotr(x, n) {
  return (x >>> n) | (x << (32 - n));
}

function ch(x, y, z) {
  return (x & y) ^ (~x & z);
}

function maj(x, y, z) {
  return (x & y) ^ (x & z) ^ (y & z);
}

function bigSigma0(x) {
  return rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22);
}

function bigSigma1(x) {
  return rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25);
}

function smallSigma0(x) {
  return rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3);
}

function smallSigma1(x) {
  return rotr(x, 17) ^ rotr(x, 19) ^ (x >>> 10);
}

const K = new Uint32Array([
  0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
  0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
  0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
  0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
  0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
  0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
  0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
  0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]);

function bytesToHex(bytes) {
  let out = "";
  for (let i = 0; i < bytes.length; i++) {
    out += bytes[i].toString(16).padStart(2, "0");
  }
  return out;
}

class Sha256 {
  constructor() {
    this._h0 = 0x6a09e667;
    this._h1 = 0xbb67ae85;
    this._h2 = 0x3c6ef372;
    this._h3 = 0xa54ff53a;
    this._h4 = 0x510e527f;
    this._h5 = 0x9b05688c;
    this._h6 = 0x1f83d9ab;
    this._h7 = 0x5be0cd19;

    this._buf = new Uint8Array(64);
    this._bufLen = 0;
    this._bytesHashed = 0;
    this._finished = false;
  }

  update(bytes) {
    if (this._finished) throw new Error("SHA256: cannot update because digest() was already called");
    if (!(bytes instanceof Uint8Array)) bytes = new Uint8Array(bytes);

    let pos = 0;
    this._bytesHashed += bytes.length;

    if (this._bufLen > 0) {
      const take = Math.min(64 - this._bufLen, bytes.length);
      this._buf.set(bytes.subarray(0, take), this._bufLen);
      this._bufLen += take;
      pos += take;
      if (this._bufLen === 64) {
        this._process(this._buf);
        this._bufLen = 0;
      }
    }

    while (pos + 64 <= bytes.length) {
      this._process(bytes.subarray(pos, pos + 64));
      pos += 64;
    }

    if (pos < bytes.length) {
      this._buf.set(bytes.subarray(pos), 0);
      this._bufLen = bytes.length - pos;
    }

    return this;
  }

  digest() {
    if (this._finished) throw new Error("SHA256: digest() already called");
    this._finished = true;

    const bytesHashed = this._bytesHashed;
    const left = this._bufLen;

    // Pad with a 1 bit then zeros until length ≡ 56 mod 64.
    this._buf[left] = 0x80;
    for (let i = left + 1; i < 64; i++) this._buf[i] = 0;

    if (left >= 56) {
      this._process(this._buf);
      for (let i = 0; i < 64; i++) this._buf[i] = 0;
    }

    // Append total length (in bits), big-endian.
    const bitLenLo = (bytesHashed * 8) >>> 0;
    const bitLenHi = Math.floor((bytesHashed * 8) / 0x100000000) >>> 0;
    this._buf[56] = (bitLenHi >>> 24) & 0xff;
    this._buf[57] = (bitLenHi >>> 16) & 0xff;
    this._buf[58] = (bitLenHi >>> 8) & 0xff;
    this._buf[59] = bitLenHi & 0xff;
    this._buf[60] = (bitLenLo >>> 24) & 0xff;
    this._buf[61] = (bitLenLo >>> 16) & 0xff;
    this._buf[62] = (bitLenLo >>> 8) & 0xff;
    this._buf[63] = bitLenLo & 0xff;

    this._process(this._buf);

    const out = new Uint8Array(32);
    const words = [this._h0, this._h1, this._h2, this._h3, this._h4, this._h5, this._h6, this._h7];
    for (let i = 0; i < words.length; i++) {
      out[i * 4 + 0] = (words[i] >>> 24) & 0xff;
      out[i * 4 + 1] = (words[i] >>> 16) & 0xff;
      out[i * 4 + 2] = (words[i] >>> 8) & 0xff;
      out[i * 4 + 3] = words[i] & 0xff;
    }
    return out;
  }

  hex() {
    return bytesToHex(this.digest());
  }

  _process(chunk) {
    const w = new Uint32Array(64);
    for (let i = 0; i < 16; i++) {
      const j = i * 4;
      w[i] = ((chunk[j] << 24) | (chunk[j + 1] << 16) | (chunk[j + 2] << 8) | chunk[j + 3]) >>> 0;
    }
    for (let i = 16; i < 64; i++) {
      w[i] = (smallSigma1(w[i - 2]) + w[i - 7] + smallSigma0(w[i - 15]) + w[i - 16]) >>> 0;
    }

    let a = this._h0;
    let b = this._h1;
    let c = this._h2;
    let d = this._h3;
    let e = this._h4;
    let f = this._h5;
    let g = this._h6;
    let h = this._h7;

    for (let i = 0; i < 64; i++) {
      const t1 = (h + bigSigma1(e) + ch(e, f, g) + K[i] + w[i]) >>> 0;
      const t2 = (bigSigma0(a) + maj(a, b, c)) >>> 0;
      h = g;
      g = f;
      f = e;
      e = (d + t1) >>> 0;
      d = c;
      c = b;
      b = a;
      a = (t1 + t2) >>> 0;
    }

    this._h0 = (this._h0 + a) >>> 0;
    this._h1 = (this._h1 + b) >>> 0;
    this._h2 = (this._h2 + c) >>> 0;
    this._h3 = (this._h3 + d) >>> 0;
    this._h4 = (this._h4 + e) >>> 0;
    this._h5 = (this._h5 + f) >>> 0;
    this._h6 = (this._h6 + g) >>> 0;
    this._h7 = (this._h7 + h) >>> 0;
  }
}

export function sha256Hex(bytes) {
  return new Sha256().update(bytes).hex();
}

export async function sha256HexOfBlob(blob, { chunkSize = 1024 * 1024, onProgress = null } = {}) {
  const hasher = new Sha256();
  let offset = 0;
  while (offset < blob.size) {
    const slice = blob.slice(offset, offset + chunkSize);
    const buf = new Uint8Array(await slice.arrayBuffer());
    hasher.update(buf);
    offset += buf.length;
    if (typeof onProgress === "function") {
      onProgress({ processedBytes: offset, totalBytes: blob.size });
    }
  }
  return hasher.hex();
}

export function formatBytes(bytes) {
  const n = Number(bytes || 0);
  if (!Number.isFinite(n) || n <= 0) return "0 B";
  const units = ["B", "KB", "MB", "GB", "TB"];
  let u = 0;
  let v = n;
  while (v >= 1024 && u < units.length - 1) {
    v /= 1024;
    u++;
  }
  return `${v.toFixed(v >= 10 || u === 0 ? 0 : 1)} ${units[u]}`;
}

