import { gcm } from '@noble/ciphers/aes.js'
import { sha256 } from '@noble/hashes/sha2.js'

const ZW_VERSION = 1
const KEY_SEED = 'JGJS2026::ZW::ARCHIVE::V1::DO_NOT_TAMPER'
const MAGIC_BYTES = new Uint8Array([0x4a, 0x47, 0x4a, 0x53, 0x5a, 0x57]) // JGJSZW

export const ZW_FILE_EXTENSION = '.zw'

const encoder = new TextEncoder()
const decoder = new TextDecoder()
let cachedKeyBytes: Uint8Array | null = null

const toArrayBuffer = (bytes: Uint8Array): ArrayBuffer =>
  bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer

const getCryptoApi = () => {
  const cryptoApi = globalThis.crypto
  if (!cryptoApi || typeof cryptoApi.getRandomValues !== 'function') {
    throw new Error('ZW_CRYPTO_UNAVAILABLE')
  }
  return cryptoApi
}

const hasSubtleCrypto = () => {
  const subtle = globalThis.crypto?.subtle
  return Boolean(
    subtle &&
    typeof subtle.importKey === 'function' &&
    typeof subtle.encrypt === 'function' &&
    typeof subtle.decrypt === 'function'
  )
}

const getArchiveKeyBytes = () => {
  if (!cachedKeyBytes) {
    cachedKeyBytes = sha256(encoder.encode(KEY_SEED))
  }
  return cachedKeyBytes
}

const getArchiveWebCryptoKey = async () => {
  if (!hasSubtleCrypto()) throw new Error('ZW_SUBTLE_UNAVAILABLE')
  return getCryptoApi().subtle.importKey(
    'raw',
    toArrayBuffer(getArchiveKeyBytes()),
    { name: 'AES-GCM' },
    false,
    ['encrypt', 'decrypt']
  )
}

export const encodeZwArchive = async (payload: unknown): Promise<Uint8Array> => {
  const cryptoApi = getCryptoApi()
  const iv = cryptoApi.getRandomValues(new Uint8Array(12))
  const ivBuffer = toArrayBuffer(iv)
  const plainText = encoder.encode(JSON.stringify(payload))
  const cipher = hasSubtleCrypto()
    ? new Uint8Array(await cryptoApi.subtle.encrypt(
        { name: 'AES-GCM', iv: ivBuffer },
        await getArchiveWebCryptoKey(),
        toArrayBuffer(plainText)
      ))
    : gcm(getArchiveKeyBytes(), iv).encrypt(plainText)
  const result = new Uint8Array(MAGIC_BYTES.length + 1 + 1 + iv.length + cipher.length)
  let offset = 0
  result.set(MAGIC_BYTES, offset)
  offset += MAGIC_BYTES.length
  result[offset] = ZW_VERSION
  offset += 1
  result[offset] = iv.length
  offset += 1
  result.set(iv, offset)
  offset += iv.length
  result.set(cipher, offset)
  return result
}

export const decodeZwArchive = async <T>(raw: ArrayBuffer | Uint8Array): Promise<T> => {
  const bytes = raw instanceof Uint8Array ? raw : new Uint8Array(raw)
  const minLength = MAGIC_BYTES.length + 1 + 1 + 1 + 16
  if (bytes.length < minLength) {
    throw new Error('INVALID_ZW_PAYLOAD')
  }

  let offset = 0
  for (let i = 0; i < MAGIC_BYTES.length; i += 1) {
    if (bytes[offset + i] !== MAGIC_BYTES[i]) {
      throw new Error('INVALID_ZW_HEADER')
    }
  }
  offset += MAGIC_BYTES.length

  const version = bytes[offset]
  offset += 1
  if (version !== ZW_VERSION) {
    throw new Error('INVALID_ZW_VERSION')
  }

  const ivLength = bytes[offset]
  offset += 1
  if (ivLength <= 0 || bytes.length <= offset + ivLength) {
    throw new Error('INVALID_ZW_PAYLOAD')
  }

  const iv = bytes.slice(offset, offset + ivLength)
  offset += ivLength
  const cipher = bytes.slice(offset)
  if (cipher.length < 16) {
    throw new Error('INVALID_ZW_PAYLOAD')
  }

  let plainBuffer: ArrayBuffer
  try {
    if (hasSubtleCrypto()) {
      plainBuffer = await getCryptoApi().subtle.decrypt(
        { name: 'AES-GCM', iv: toArrayBuffer(iv) },
        await getArchiveWebCryptoKey(),
        toArrayBuffer(cipher)
      )
    } else {
      plainBuffer = toArrayBuffer(gcm(getArchiveKeyBytes(), iv).decrypt(cipher))
    }
  } catch {
    throw new Error('INVALID_ZW_TAMPERED')
  }

  try {
    return JSON.parse(decoder.decode(plainBuffer)) as T
  } catch {
    throw new Error('INVALID_ZW_CONTENT')
  }
}
