const encoder = new TextEncoder();
const decoder = new TextDecoder();
export const utf8 = value => encoder.encode(value);
export const text = bytes => decoder.decode(bytes);
export function randomSecret() { return toBase64(crypto.getRandomValues(new Uint8Array(32))); }
export function toBase64(bytes) { let s = ''; for (const b of bytes) s += String.fromCharCode(b); return btoa(s).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, ''); }
export function fromBase64(value) { if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error('Clé invalide'); return Uint8Array.from(atob(value.replaceAll('-', '+').replaceAll('_', '/')), c => c.charCodeAt(0)); }
export async function deriveKey(secret, context) {
  const raw = fromBase64(secret); if (raw.length !== 32) throw new Error('Clé de partage invalide');
  const material = await crypto.subtle.importKey('raw', raw, 'HKDF', false, ['deriveKey']);
  return crypto.subtle.deriveKey({name:'HKDF', hash:'SHA-256', salt:utf8('P2P prototype v1'), info:utf8(context)}, material, {name:'AES-GCM',length:256}, false, ['encrypt','decrypt']);
}
export async function deriveJoinToken(secret,shareId) {
  const raw=fromBase64(secret);if(raw.length!==32)throw new Error('Clé de partage invalide');
  if(typeof shareId!=='string'||!shareId||shareId.length>128)throw new Error('Identifiant de partage invalide');
  const material=await crypto.subtle.importKey('raw',raw,'HKDF',false,['deriveBits']);
  const bits=await crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-256',salt:utf8('P2P prototype v1'),info:utf8(JSON.stringify(['P2P broker access v1',shareId]))},material,256);
  return toBase64(new Uint8Array(bits));
}
export async function hashJoinToken(token) {
  if(typeof token!=='string'||!/^[A-Za-z0-9_-]{43}$/.test(token)||fromBase64(token).length!==32)throw new Error('Jeton de partage invalide');
  return [...new Uint8Array(await crypto.subtle.digest('SHA-256',utf8(token)))].map(value=>value.toString(16).padStart(2,'0')).join('');
}
export async function seal(key, data, context, nonce = crypto.getRandomValues(new Uint8Array(12))) {
  const cipher = new Uint8Array(await crypto.subtle.encrypt({name:'AES-GCM',iv:nonce,additionalData:utf8(context),tagLength:128},key,data));
  const out = new Uint8Array(12+cipher.length); out.set(nonce); out.set(cipher,12); return out;
}
export async function open(key, packet, context) {
  const bytes = new Uint8Array(packet); if(bytes.length < 28) throw new Error('Message tronqué');
  return new Uint8Array(await crypto.subtle.decrypt({name:'AES-GCM',iv:bytes.slice(0,12),additionalData:utf8(context),tagLength:128},key,bytes.slice(12)));
}
export function createCipher(key, context) {
  let sent = 0n; let received = 0n;
  function nonce(n) { const out=new Uint8Array(12);new DataView(out.buffer).setBigUint64(4,n);return out; }
  return {
    async encrypt(bytes) { if(sent >= 0xffffffffffffffffn) throw new Error('Compteur épuisé'); const n=sent++;return seal(key,bytes,context,nonce(n)); },
    async decrypt(packet) { const bytes=new Uint8Array(packet);const expected=nonce(received);if(bytes.length<28 || expected.some((b,i)=>bytes[i]!==b))throw new Error('Message rejoué ou désordonné');const plain=await open(key,bytes,context);received++;return plain; }
  };
}
export async function createDataCiphers({secret,sessionId,ownerNonce,receiverNonce,owner}) {
  for(const nonce of [ownerNonce,receiverNonce])if(typeof nonce!=='string'||fromBase64(nonce).length!==32)throw new Error('Aléa de négociation invalide');
  const context=direction=>JSON.stringify(['P2P data v2',sessionId,ownerNonce,receiverNonce,direction]);
  const txContext=context(owner?'owner':'receiver'),rxContext=context(owner?'receiver':'owner');
  return {tx:createCipher(await deriveKey(secret,txContext),txContext),rx:createCipher(await deriveKey(secret,rxContext),rxContext)};
}
export async function passwordDigest(password, salt) { return toBase64(new Uint8Array(await crypto.subtle.digest('SHA-256',utf8(JSON.stringify([salt,password]))))); }
export function equalDigest(a,b) { if(a.length!==b.length)return false;let d=0;for(let i=0;i<a.length;i++)d|=a.charCodeAt(i)^b.charCodeAt(i);return d===0; }
