const enc = new TextEncoder();
export function randomToken(bytes = 32) { return Array.from(crypto.getRandomValues(new Uint8Array(bytes)), b => b.toString(16).padStart(2, '0')).join(''); }
export async function sha(text: string) { return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', enc.encode(text))), b => b.toString(16).padStart(2, '0')).join(''); }
export function constantEqual(a: string, b: string) { let d = a.length ^ b.length; for (let i = 0; i < Math.max(a.length, b.length); i++)
    d |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0); return d === 0; }
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
export function base32(bytes: Uint8Array) { let bits = 0, value = 0, out = ''; for (const b of bytes) {
    value = (value << 8) | b;
    bits += 8;
    while (bits >= 5) {
        out += alphabet[(value >>> (bits - 5)) & 31];
        bits -= 5;
    }
} if (bits)
    out += alphabet[(value << (5 - bits)) & 31]; return out; }
function unbase32(input: string) { let bits = 0, v = 0, out = []; for (const c of input) {
    v = (v << 5) | alphabet.indexOf(c);
    bits += 5;
    if (bits >= 8) {
        out.push((v >>> (bits - 8)) & 255);
        bits -= 8;
    }
} return new Uint8Array(out); }
export async function totp(secret: string, step = Math.floor(Date.now() / 30000)) {
    const data = new Uint8Array(8);
    new DataView(data.buffer).setBigUint64(0, BigInt(step));
    const k = await crypto.subtle.importKey('raw', unbase32(secret), { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
    const h = new Uint8Array(await crypto.subtle.sign('HMAC', k, data));
    const o = h[h.length - 1] & 15;
    const n = ((h[o] & 127) << 24) | (h[o + 1] << 16) | (h[o + 2] << 8) | h[o + 3];
    return (n % 1000000).toString().padStart(6, '0');
}
export async function validateTotp(secret: string, code: string, last = 0) { if (!/^\d{6}$/.test(code))
    return null; const s = Math.floor(Date.now() / 30000); for (const step of [s, s - 1, s + 1])
    if (step > last && constantEqual(await totp(secret, step), code))
        return step; return null; }
async function key(secret: string) { if (!/^[a-f\d]{64}$/i.test(secret))
    throw new Error('Kunci enkripsi belum dikonfigurasi.'); return crypto.subtle.importKey('raw', new Uint8Array(secret.match(/../g)!.map(x => parseInt(x, 16))), 'AES-GCM', false, ['encrypt', 'decrypt']); }
export async function encrypt(data: string, secret: string) { const iv = crypto.getRandomValues(new Uint8Array(12)); const bytes = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, await key(secret), enc.encode(data))); return `${Array.from(iv, b => b.toString(16).padStart(2, '0')).join('')}.${Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')}`; }
export async function decrypt(data: string, secret: string) { const [i, c] = data.split('.'); const bytes = (s: string) => new Uint8Array(s.match(/../g)!.map(x => parseInt(x, 16))); return new TextDecoder().decode(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: bytes(i) }, await key(secret), bytes(c))); }
export async function validateFile(file: File, video = false) {
    if (file.size < 1 || file.size > (video && file.type === 'video/mp4' ? 25000000 : 5242880))
        throw new Error(video && file.type === 'video/mp4' ? 'Ukuran video maksimal 25 MB.' : 'Ukuran file maksimal 5 MB.');
    const b = new Uint8Array(await file.slice(0, 16).arrayBuffer());
    let mime = '';
    if (b[0] === 255 && b[1] === 216 && b[2] === 255)
        mime = 'image/jpeg';
    else if (b.slice(0, 8).join(',') === '137,80,78,71,13,10,26,10')
        mime = 'image/png';
    else if (new TextDecoder().decode(b.slice(0, 5)) === '%PDF-')
        mime = 'application/pdf';
    else if (video && new TextDecoder().decode(b.slice(4, 8)) === 'ftyp')
        mime = 'video/mp4';
    if (!mime || !['image/jpeg', 'image/png', 'application/pdf', ...(video ? ['video/mp4'] : [])].includes(file.type) || file.type !== mime)
        throw new Error('Jenis file tidak sesuai. Gunakan JPG, PNG, atau PDF yang valid.');
    return mime;
}
