/**
 * Minimal pure-JS AES-256-GCM decryption.
 *
 * Powód: React Native (Hermes) nie ma Web Crypto (`crypto.subtle`), a projekt
 * nie linkuje natywnego crypto modułu. `filemoonHoster.ts` musi rozszyfrować
 * AES-256-GCM payload zwracany przez filemoon `/api/videos/<code>/embed/playback`.
 * Czysty JS = zmiana ships przez OTA update (bez rebuilda APK).
 *
 * Zakres: tylko DECRYPT, klucz 256-bit, IV 96-bit (GCM standard), 128-bit tag
 * doklejony na końcu ciphertextu (konwencja Web Crypto / Go `crypto/cipher`).
 * Tablica S-box liczona z GF(2^8) przy load module — zero hardkodowanych
 * 256-elementowych literałów (mniej miejsca na literówkę). GHASH weryfikuje tag.
 *
 * Referencje: NIST SP 800-38D (GCM), FIPS-197 (AES).
 */

// ---- GF(2^8) — xtime + tablice exp/log + S-box ----

function xtime(b: number): number {
  return ((b << 1) ^ (b & 0x80 ? 0x1b : 0)) & 0xff;
}

const SBOX = new Uint8Array(256);

(function initSbox() {
  // log/antilog w GF(2^8) z generatorem 3 (0x03).
  const exp = new Uint8Array(256);
  const log = new Uint8Array(256);
  let x = 1;
  for (let i = 0; i < 255; i++) {
    exp[i] = x;
    log[x] = i;
    x ^= xtime(x); // x = x*2 ^ x = x*3
  }
  // S-box: odwrotność multiplikatywna + transformacja afiniczna.
  for (let a = 0; a < 256; a++) {
    let inv = a === 0 ? 0 : exp[(255 - log[a]) % 255];
    let s = inv;
    let r = inv;
    for (let i = 0; i < 4; i++) {
      s = ((s << 1) | (s >>> 7)) & 0xff;
      r ^= s;
    }
    SBOX[a] = r ^ 0x63;
  }
})();

// ---- AES-256 key schedule (Nk=8, Nr=14 → 240 bajtów round keys) ----

const NR = 14;

function expandKey(key: Uint8Array): Uint8Array {
  if (key.length !== 32) throw new Error('aesGcm: key must be 32 bytes (AES-256)');
  const Nk = 8;
  const total = 4 * (NR + 1); // 60 słów
  const w = new Uint8Array(total * 4); // 240 bajtów
  w.set(key);
  let rc = 1;
  for (let i = Nk; i < total; i++) {
    let t0 = w[(i - 1) * 4];
    let t1 = w[(i - 1) * 4 + 1];
    let t2 = w[(i - 1) * 4 + 2];
    let t3 = w[(i - 1) * 4 + 3];
    if (i % Nk === 0) {
      // RotWord + SubWord + Rcon
      const tmp = t0;
      t0 = SBOX[t1] ^ rc;
      t1 = SBOX[t2];
      t2 = SBOX[t3];
      t3 = SBOX[tmp];
      rc = xtime(rc);
    } else if (i % Nk === 4) {
      // AES-256: dodatkowy SubWord co 4 słowa
      t0 = SBOX[t0];
      t1 = SBOX[t1];
      t2 = SBOX[t2];
      t3 = SBOX[t3];
    }
    w[i * 4] = w[(i - Nk) * 4] ^ t0;
    w[i * 4 + 1] = w[(i - Nk) * 4 + 1] ^ t1;
    w[i * 4 + 2] = w[(i - Nk) * 4 + 2] ^ t2;
    w[i * 4 + 3] = w[(i - Nk) * 4 + 3] ^ t3;
  }
  return w;
}

function m3(b: number): number {
  return xtime(b) ^ b;
}

// Szyfrowanie pojedynczego 16-bajtowego bloku. GCM używa tylko AES-encrypt
// (zarówno do keystreamu CTR jak i do hashowania) — AES-decrypt nie jest potrzebny.
function encryptBlock(rk: Uint8Array, input: Uint8Array): Uint8Array {
  const s = new Uint8Array(16);
  s.set(input);
  // AddRoundKey (runda 0)
  for (let i = 0; i < 16; i++) s[i] ^= rk[i];
  for (let round = 1; round <= NR; round++) {
    // SubBytes
    for (let i = 0; i < 16; i++) s[i] = SBOX[s[i]];
    // ShiftRows (state column-major: bajt (row r, col c) = s[r + 4c])
    let t = s[1];
    s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t;
    t = s[2]; s[2] = s[10]; s[10] = t;
    t = s[6]; s[6] = s[14]; s[14] = t;
    t = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = s[3]; s[3] = t;
    // MixColumns (pomijane w ostatniej rundzie)
    if (round < NR) {
      for (let c = 0; c < 4; c++) {
        const a0 = s[4 * c], a1 = s[4 * c + 1], a2 = s[4 * c + 2], a3 = s[4 * c + 3];
        s[4 * c] = xtime(a0) ^ m3(a1) ^ a2 ^ a3;
        s[4 * c + 1] = a0 ^ xtime(a1) ^ m3(a2) ^ a3;
        s[4 * c + 2] = a0 ^ a1 ^ xtime(a2) ^ m3(a3);
        s[4 * c + 3] = m3(a0) ^ a1 ^ a2 ^ xtime(a3);
      }
    }
    // AddRoundKey
    for (let i = 0; i < 16; i++) s[i] ^= rk[round * 16 + i];
  }
  return s;
}

// ---- GCM ----

// Mnożenie w GF(2^128) wg konwencji GCM (bit-reversed, wielomian R = 0xe1<<120).
function gmult(X: Uint8Array, Y: Uint8Array): Uint8Array {
  const Z = new Uint8Array(16);
  const V = Y.slice();
  for (let i = 0; i < 128; i++) {
    const bit = (X[i >>> 3] >>> (7 - (i & 7))) & 1;
    if (bit) for (let j = 0; j < 16; j++) Z[j] ^= V[j];
    const lsb = V[15] & 1;
    for (let j = 15; j > 0; j--) V[j] = ((V[j] >>> 1) | ((V[j - 1] & 1) << 7)) & 0xff;
    V[0] = V[0] >>> 1;
    if (lsb) V[0] ^= 0xe1;
  }
  return Z;
}

// GHASH: X ← (X ^ block) · H, blok po bloku (16 bajtów, padding zerami).
function ghashUpdate(X: Uint8Array, H: Uint8Array, block: Uint8Array): Uint8Array {
  const t = new Uint8Array(16);
  for (let i = 0; i < 16; i++) t[i] = X[i] ^ (block[i] ?? 0);
  return gmult(t, H);
}

function inc32(block: Uint8Array): void {
  // inkrementacja ostatnich 4 bajtów jako big-endian uint32
  for (let i = 15; i >= 12; i--) {
    block[i] = (block[i] + 1) & 0xff;
    if (block[i] !== 0) break;
  }
}

function ct16Eq(a: Uint8Array, b: Uint8Array): boolean {
  // porównanie w stałym czasie (16 bajtów)
  let d = 0;
  for (let i = 0; i < 16; i++) d |= a[i] ^ b[i];
  return d === 0;
}

/**
 * Rozszyfruj AES-256-GCM. `ciphertextWithTag` = ciphertext || 16-bajtowy tag.
 * Rzuca wyjątek przy niezgodności tagu albo złych długościach.
 */
export function aesGcmDecrypt(
  key: Uint8Array,
  iv: Uint8Array,
  ciphertextWithTag: Uint8Array,
): Uint8Array {
  if (iv.length !== 12) throw new Error('aesGcm: IV must be 12 bytes (96-bit)');
  if (ciphertextWithTag.length < 16) throw new Error('aesGcm: payload shorter than tag');

  const rk = expandKey(key);
  const ctLen = ciphertextWithTag.length - 16;
  const ct = ciphertextWithTag.subarray(0, ctLen);
  const tag = ciphertextWithTag.subarray(ctLen);

  // H = AES_K(0^128)
  const H = encryptBlock(rk, new Uint8Array(16));

  // J0 = IV || 0x00000001
  const J0 = new Uint8Array(16);
  J0.set(iv);
  J0[15] = 1;

  // GHASH nad ciphertextem (AAD puste).
  let X = new Uint8Array(16);
  for (let off = 0; off < ctLen; off += 16) {
    const block = ct.subarray(off, Math.min(off + 16, ctLen));
    X = ghashUpdate(X, H, block);
  }
  // Blok długości: lenAAD (64-bit) || lenC (64-bit), oba w bitach.
  const lenBlock = new Uint8Array(16);
  const ctBits = ctLen * 8;
  // lenAAD = 0 → bajty 0..7 zostają zerowe. lenC w bajtach 8..15 (big-endian).
  lenBlock[15] = ctBits & 0xff;
  lenBlock[14] = (ctBits >>> 8) & 0xff;
  lenBlock[13] = (ctBits >>> 16) & 0xff;
  lenBlock[12] = (ctBits >>> 24) & 0xff;
  // ctBits > 2^32 nie wystąpi dla payloadów filemoon (kilka KB) — wyższe bajty 0.
  X = ghashUpdate(X, H, lenBlock);

  // Tag oczekiwany T = GHASH ^ AES_K(J0)
  const ej0 = encryptBlock(rk, J0);
  const expTag = new Uint8Array(16);
  for (let i = 0; i < 16; i++) expTag[i] = X[i] ^ ej0[i];
  if (!ct16Eq(expTag, tag)) throw new Error('aesGcm: authentication tag mismatch');

  // Deszyfracja CTR — licznik startuje od inc32(J0).
  const plain = new Uint8Array(ctLen);
  const counter = J0.slice();
  for (let off = 0; off < ctLen; off += 16) {
    inc32(counter);
    const ks = encryptBlock(rk, counter);
    const n = Math.min(16, ctLen - off);
    for (let i = 0; i < n; i++) plain[off + i] = ct[off + i] ^ ks[i];
  }
  return plain;
}

/** Dekoduje base64url (bez paddingu OK) do Uint8Array. Self-contained — nie
 *  zależy od `atob`/`Buffer` które w RN/Hermes bywają niedostępne. */
export function b64urlDecode(input: string): Uint8Array {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
  const lookup = new Int16Array(128).fill(-1);
  for (let i = 0; i < alphabet.length; i++) lookup[alphabet.charCodeAt(i)] = i;
  const clean = input.replace(/=+$/, '');
  const out = new Uint8Array(Math.floor((clean.length * 6) / 8));
  let bits = 0;
  let acc = 0;
  let oi = 0;
  for (let i = 0; i < clean.length; i++) {
    const v = lookup[clean.charCodeAt(i) & 0x7f];
    if (v < 0) throw new Error('b64urlDecode: invalid character');
    acc = (acc << 6) | v;
    bits += 6;
    if (bits >= 8) {
      bits -= 8;
      out[oi++] = (acc >>> bits) & 0xff;
    }
  }
  return out;
}
