// SPDX-License-Identifier: AGPL-3.0-or-later

const {existsSync} = require('node:fs');
const {createHash} = require('node:crypto');
const {join, sep} = require('node:path');
const {createNativeLoadError, loadNativeBinding} = require('./loader-diagnostics.cjs');
const MODULE_NAME = '@fluxer/webauthn';
const SKIP_NATIVE_PROBE_ENV = 'FLUXER_WEBAUTHN_SKIP_NATIVE_PROBE';
const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY = 0;
const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM = 1;
const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM = 2;
const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED = 1;
const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED = 2;
const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3;
const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE = 1;
const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT = 2;
const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT = 3;
const WEBAUTHN_ENTERPRISE_ATTESTATION_NONE = 0;
const WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED = 1;
const WEBAUTHN_CTAP_TRANSPORT_USB = 0x00000001;
const WEBAUTHN_CTAP_TRANSPORT_NFC = 0x00000002;
const WEBAUTHN_CTAP_TRANSPORT_BLE = 0x00000004;
const WEBAUTHN_CTAP_TRANSPORT_INTERNAL = 0x00000010;
const WEBAUTHN_CTAP_TRANSPORT_HYBRID = 0x00000020;
const WEBAUTHN_CTAP_TRANSPORT_SMART_CARD = 0x00000040;

function resolveNativeRoot() {
	const asarSegment = `${sep}app.asar${sep}`;
	if (!__dirname.includes(asarSegment)) return __dirname;
	const unpackedDir = __dirname.replace(asarSegment, `${sep}app.asar.unpacked${sep}`);
	return existsSync(unpackedDir) ? unpackedDir : __dirname;
}

function nativeFileName(platform = process.platform, arch = process.arch) {
	if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) return `webauthn.darwin-${arch}.node`;
	if (platform === 'win32' && (arch === 'x64' || arch === 'arm64')) return `webauthn.win32-${arch}-msvc.node`;
	if (platform === 'linux' && (arch === 'x64' || arch === 'arm64')) return `webauthn.linux-${arch}-gnu.node`;
	return null;
}

let binding = null;
let loadError = null;

const fileName = nativeFileName();

if (fileName) {
	try {
		const nativeRoot = resolveNativeRoot();
		const nativePath = join(nativeRoot, fileName);
		const loaded = loadNativeBinding({
			moduleName: MODULE_NAME,
			nativePath,
			nativeRoot,
			packageDir: __dirname,
			skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
		});
		binding = loaded.binding;
		loadError = loaded.loadError;
		if (loadError) throw loadError;
	} catch (error) {
		loadError = createNativeLoadError({
			moduleName: MODULE_NAME,
			nativeRoot: resolveNativeRoot(),
			packageDir: __dirname,
			reason: 'native loader threw before binding load completed',
			cause: error,
			skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
		});
		throw loadError;
	}
} else {
	loadError = createNativeLoadError({
		moduleName: MODULE_NAME,
		nativeRoot: resolveNativeRoot(),
		packageDir: __dirname,
		reason: `no native binary mapping for ${process.platform}/${process.arch}`,
		skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
	});
	throw loadError;
}

function base64Url(buffer) {
	return Buffer.from(buffer).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function normalizedOrigin(origin) {
	if (typeof origin !== 'string' || origin.length === 0) {
		throw new TypeError('@fluxer/webauthn requires an origin string');
	}
	return new URL(origin).origin;
}

function relyingPartyId(options, origin) {
	if (typeof options.rpId === 'string' && options.rpId.length > 0) return options.rpId;
	if (options.rp && typeof options.rp.id === 'string' && options.rp.id.length > 0) return options.rp.id;
	return new URL(origin).hostname;
}

function clientDataJSON(type, challenge, origin) {
	return Buffer.from(
		JSON.stringify({
			type,
			challenge: base64Url(challenge),
			origin,
			crossOrigin: false,
		}),
		'utf8',
	);
}

function clientDataHash(data) {
	return createHash('sha256').update(data).digest();
}

function authenticatorAttachment(value) {
	if (value === 'platform') return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM;
	if (value === 'cross-platform') return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
	return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY;
}

function userVerification(value) {
	if (value === 'required') return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED;
	if (value === 'discouraged') return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
	return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED;
}

function attestation(value) {
	if (value === 'direct') return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT;
	if (value === 'indirect') return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT;
	return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE;
}

function enterpriseAttestation(value) {
	return value === 'enterprise'
		? WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED
		: WEBAUTHN_ENTERPRISE_ATTESTATION_NONE;
}

function transportBits(transports) {
	if (!Array.isArray(transports)) return 0;
	let bits = 0;
	for (const transport of transports) {
		if (transport === 'usb') bits |= WEBAUTHN_CTAP_TRANSPORT_USB;
		else if (transport === 'nfc') bits |= WEBAUTHN_CTAP_TRANSPORT_NFC;
		else if (transport === 'ble') bits |= WEBAUTHN_CTAP_TRANSPORT_BLE;
		else if (transport === 'internal') bits |= WEBAUTHN_CTAP_TRANSPORT_INTERNAL;
		else if (transport === 'hybrid') bits |= WEBAUTHN_CTAP_TRANSPORT_HYBRID;
		else if (transport === 'smart-card') bits |= WEBAUTHN_CTAP_TRANSPORT_SMART_CARD;
	}
	return bits;
}

function credentialDescriptors(descriptors) {
	if (!Array.isArray(descriptors)) return [];
	return descriptors.map((descriptor) => ({
		id: Buffer.from(descriptor.id),
		transports: transportBits(descriptor.transports),
	}));
}

function windowHandleBuffer(value) {
	if (Buffer.isBuffer(value)) return value;
	if (value instanceof Uint8Array) return Buffer.from(value);
	return undefined;
}

function pinString(value) {
	return typeof value === 'string' && value.length > 0 ? value : undefined;
}

function residentKeyFlags(selection) {
	const residentKey = selection?.residentKey;
	const requireResidentKey = Boolean(selection?.requireResidentKey) || residentKey === 'required';
	return {
		requireResidentKey,
		preferResidentKey: requireResidentKey || residentKey === 'preferred',
	};
}

function normalizeCreateOptions(options) {
	if (!options || typeof options !== 'object') throw new TypeError('registration options must be an object');
	const origin = normalizedOrigin(options.origin);
	const selection = options.authenticatorSelection || {};
	const residentKeys = residentKeyFlags(selection);
	return {
		rpId: relyingPartyId(options, origin),
		rpName: options.rp && typeof options.rp.name === 'string' ? options.rp.name : relyingPartyId(options, origin),
		challenge: Buffer.from(options.challenge),
		userId: Buffer.from(options.user.id),
		userName: options.user.name,
		userDisplayName: options.user.displayName,
		clientDataJSON: clientDataJSON('webauthn.create', options.challenge, origin),
		pubKeyCredParams: options.pubKeyCredParams,
		excludeCredentials: credentialDescriptors(options.excludeCredentials),
		timeout: Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout)) : 0,
		authenticatorAttachment: authenticatorAttachment(selection.authenticatorAttachment),
		userVerification: userVerification(selection.userVerification),
		attestation: attestation(options.attestation),
		enterpriseAttestation: enterpriseAttestation(options.attestation),
		windowHandle: windowHandleBuffer(options.windowHandle),
		pin: pinString(options.pin),
		...residentKeys,
	};
}

function normalizeGetOptions(options) {
	if (!options || typeof options !== 'object') throw new TypeError('assertion options must be an object');
	const origin = normalizedOrigin(options.origin);
	return {
		rpId: relyingPartyId(options, origin),
		challenge: Buffer.from(options.challenge),
		clientDataJSON: clientDataJSON('webauthn.get', options.challenge, origin),
		allowCredentials: credentialDescriptors(options.allowCredentials),
		timeout: Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout)) : 0,
		authenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY,
		userVerification: userVerification(options.userVerification),
		windowHandle: windowHandleBuffer(options.windowHandle),
		pin: pinString(options.pin),
	};
}

function unavailableError() {
	return new Error(
		`@fluxer/webauthn native backend unavailable on ${process.platform}/${process.arch}: ${
			loadError instanceof Error ? loadError.message : 'unknown load error'
		}`,
	);
}

function requireBinding() {
	if (!binding) throw unavailableError();
	return binding;
}

async function isSupported() {
	if (!binding || typeof binding.isSupported !== 'function') return false;
	return Boolean(await binding.isSupported());
}

function rawIdCredential(rawId, response, authenticatorAttachment) {
	const id = base64Url(rawId);
	return {
		id,
		rawId,
		response: Buffer.from(JSON.stringify(response), 'utf8'),
		authenticatorAttachment,
		type: 'public-key',
	};
}

async function create(options) {
	const native = requireBinding();
	if (typeof native.create !== 'function') {
		throw new Error(
			`@fluxer/webauthn native backend did not export registration on ${process.platform}/${process.arch}`,
		);
	}
	const normalized = normalizeCreateOptions(options);
	normalized.clientDataHash = clientDataHash(normalized.clientDataJSON);
	const result = await native.create(normalized);
	return rawIdCredential(
		result.rawId,
		{
			clientDataJSON: base64Url(result.clientDataJSON),
			attestationObject: base64Url(result.attestationObject),
		},
		result.authenticatorAttachment,
	);
}

async function get(options) {
	const native = requireBinding();
	if (typeof native.get !== 'function') {
		throw new Error(
			`@fluxer/webauthn native backend did not export authentication on ${process.platform}/${process.arch}`,
		);
	}
	const normalized = normalizeGetOptions(options);
	normalized.clientDataHash = clientDataHash(normalized.clientDataJSON);
	const result = await native.get(normalized);
	const response = {
		clientDataJSON: base64Url(result.clientDataJSON),
		authenticatorData: base64Url(result.authenticatorData),
		signature: base64Url(result.signature),
	};
	if (result.userHandle) response.userHandle = base64Url(result.userHandle);
	return rawIdCredential(result.rawId, response, result.authenticatorAttachment);
}

function getBackendInfo() {
	if (!binding || typeof binding.getBackendInfo !== 'function') {
		return {
			target: `${process.platform}/${process.arch}`,
			backend: 'unavailable',
			nativeLoaded: false,
			ceremoniesImplemented: false,
			platformBrokerAvailable: false,
			platformAuthenticatorAvailable: false,
			supported: false,
			apiVersion: 0,
			reason: loadError instanceof Error ? loadError.message : 'native backend did not load',
		};
	}
	return binding.getBackendInfo();
}

module.exports = {
	create,
	get,
	getBackendInfo,
	isSupported,
	loadError,
	_private: {
		base64Url,
		clientDataJSON,
		nativeFileName,
		normalizeCreateOptions,
		normalizeGetOptions,
		resolveNativeRoot,
		transportBits,
	},
};
