function bufferToBase64url(buffer: ArrayBuffer): string {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    bytes.forEach((byte) => {
        binary += String.fromCharCode(byte);
    });

    return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function base64urlToBuffer(value: string): ArrayBuffer {
    const padded = value + '='.repeat((4 - (value.length % 4)) % 4);
    const base64 = padded.replace(/-/g, '+').replace(/_/g, '/');
    const raw = atob(base64);
    const bytes = new Uint8Array(raw.length);

    for (let i = 0; i < raw.length; i++) {
        bytes[i] = raw.charCodeAt(i);
    }

    return bytes.buffer;
}

function csrfHeaders(): Record<string, string> {
    const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);

    return {
        Accept: 'application/json',
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
        ...(match ? { 'X-XSRF-TOKEN': decodeURIComponent(match[1]) } : {}),
    };
}

export function publicKeyFromServer(options: Record<string, unknown>): PublicKeyCredentialCreationOptions {
    const next = { ...options } as Record<string, unknown>;

    if (typeof next.challenge === 'string') {
        next.challenge = base64urlToBuffer(next.challenge);
    }

    const user = next.user as { id?: string } | undefined;

    if (user && typeof user.id === 'string') {
        next.user = { ...user, id: base64urlToBuffer(user.id) };
    }

    for (const key of ['excludeCredentials', 'allowCredentials'] as const) {
        const list = next[key];

        if (Array.isArray(list)) {
            next[key] = list.map((item) => {
                const credential = item as { id?: string };

                return {
                    ...credential,
                    id: typeof credential.id === 'string' ? base64urlToBuffer(credential.id) : credential.id,
                };
            });
        }
    }

    return next as PublicKeyCredentialCreationOptions;
}

export function credentialToJson(credential: PublicKeyCredential): Record<string, unknown> {
    const response = credential.response;
    const payload: Record<string, unknown> = {
        id: credential.id,
        rawId: bufferToBase64url(credential.rawId),
        type: credential.type,
        response: {
            clientDataJSON: bufferToBase64url(response.clientDataJSON),
        },
    };

    const body = payload.response as Record<string, unknown>;

    if ('attestationObject' in response) {
        body.attestationObject = bufferToBase64url(
            (response as AuthenticatorAttestationResponse).attestationObject,
        );
        body.transports = (response as AuthenticatorAttestationResponse).getTransports?.() ?? [];
    }

    if ('authenticatorData' in response) {
        const assertion = response as AuthenticatorAssertionResponse;
        body.authenticatorData = bufferToBase64url(assertion.authenticatorData);
        body.signature = bufferToBase64url(assertion.signature);
        body.userHandle = assertion.userHandle ? bufferToBase64url(assertion.userHandle) : null;
    }

    return payload;
}

export async function fetchJson(url: string, init: RequestInit = {}): Promise<unknown> {
    const response = await fetch(url, {
        credentials: 'same-origin',
        ...init,
        headers: {
            ...csrfHeaders(),
            ...(init.headers as Record<string, string> | undefined),
        },
    });

    const body = await response.json().catch(() => ({}));

    if (!response.ok) {
        const message =
            (body as { message?: string })?.message
            ?? (body as { errors?: Record<string, string[]> })?.errors?.email?.[0]
            ?? (body as { errors?: Record<string, string[]> })?.errors?.credential?.[0]
            ?? 'La requête a échoué.';

        throw new Error(message);
    }

    return body;
}
