// Thin wrappers around the gateway public auth REST surface used by
// the email-code login flow. The two exported functions correspond
// 1:1 to the OpenAPI operations defined in
// `backend/openapi.yaml`:
//
//   POST /api/v1/public/auth/send-email-code
//   POST /api/v1/public/auth/confirm-email-code
//
// Both endpoints are unauthenticated — the device session does not
// exist yet during send-code, and confirm-code is the call that
// creates one. Persisting the returned `device_session_id` is the
// caller's responsibility (see `lib/session-store.svelte.ts`).
//
// `Accept-Language` is set automatically by the browser; the gateway
// reads it for the auth-mail localisation. We do not duplicate the
// value into the optional `locale` body field.

const SEND_EMAIL_CODE_PATH = "/api/v1/public/auth/send-email-code";
const CONFIRM_EMAIL_CODE_PATH = "/api/v1/public/auth/confirm-email-code";

export interface SendEmailCodeResult {
	challengeId: string;
}

export interface SendEmailCodeOptions {
	/**
	 * locale is forwarded inside the JSON body and read by the
	 * gateway in preference to the request `Accept-Language` header.
	 * The body field is the canonical channel because Safari/WebKit
	 * silently drops JS-set `Accept-Language` headers, while the
	 * body round-trips correctly on every supported engine. When the
	 * caller omits this option the browser-default Accept-Language
	 * remains the gateway's only signal and the auth-mail uses the
	 * system locale.
	 */
	locale?: string;
}

export interface ConfirmEmailCodeInput {
	challengeId: string;
	code: string;
	publicKey: Uint8Array;
	timeZone: string;
}

export interface ConfirmEmailCodeResult {
	deviceSessionId: string;
}

/**
 * AuthError is thrown by `sendEmailCode` and `confirmEmailCode` for
 * every non-2xx gateway response. `code` mirrors the stable
 * machine-readable identifier from the gateway error envelope
 * (`invalid_request`, `service_unavailable`, `internal_error`, ...);
 * `status` is the HTTP status that produced the error.
 */
export class AuthError extends Error {
	readonly code: string;
	readonly status: number;

	constructor(code: string, message: string, status: number) {
		super(message);
		this.name = "AuthError";
		this.code = code;
		this.status = status;
	}
}

/**
 * sendEmailCode issues a login challenge for `email`. The gateway
 * returns the same opaque `challenge_id` shape regardless of whether
 * the address belongs to a new, existing, or throttled account, so
 * the caller cannot use the response to enumerate accounts.
 */
export async function sendEmailCode(
	baseUrl: string,
	email: string,
	options?: SendEmailCodeOptions,
): Promise<SendEmailCodeResult> {
	const requestBody: Record<string, string> = { email };
	if (options?.locale !== undefined && options.locale !== "") {
		requestBody.locale = options.locale;
	}
	const response = await fetch(joinUrl(baseUrl, SEND_EMAIL_CODE_PATH), {
		method: "POST",
		headers: { "content-type": "application/json" },
		body: JSON.stringify(requestBody),
	});
	if (!response.ok) {
		throw await readAuthError(response);
	}
	const responseBody = (await response.json()) as { challenge_id?: unknown };
	if (
		typeof responseBody.challenge_id !== "string" ||
		responseBody.challenge_id.length === 0
	) {
		throw new AuthError(
			"internal_error",
			"gateway returned a malformed send-email-code response",
			response.status,
		);
	}
	return { challengeId: responseBody.challenge_id };
}

/**
 * confirmEmailCode submits the verification code and the device's
 * Ed25519 public key. On success the gateway returns the new device
 * session identifier; persistence of that identifier is the caller's
 * responsibility.
 */
export async function confirmEmailCode(
	baseUrl: string,
	input: ConfirmEmailCodeInput,
): Promise<ConfirmEmailCodeResult> {
	const response = await fetch(joinUrl(baseUrl, CONFIRM_EMAIL_CODE_PATH), {
		method: "POST",
		headers: { "content-type": "application/json" },
		body: JSON.stringify({
			challenge_id: input.challengeId,
			code: input.code,
			client_public_key: encodeBase64(input.publicKey),
			time_zone: input.timeZone,
		}),
	});
	if (!response.ok) {
		throw await readAuthError(response);
	}
	const body = (await response.json()) as { device_session_id?: unknown };
	if (
		typeof body.device_session_id !== "string" ||
		body.device_session_id.length === 0
	) {
		throw new AuthError(
			"internal_error",
			"gateway returned a malformed confirm-email-code response",
			response.status,
		);
	}
	return { deviceSessionId: body.device_session_id };
}

async function readAuthError(response: Response): Promise<AuthError> {
	let code = "";
	let message = "";
	try {
		const body = (await response.json()) as {
			error?: { code?: unknown; message?: unknown };
		};
		const err = body.error;
		if (err && typeof err.code === "string") {
			code = err.code;
		}
		if (err && typeof err.message === "string") {
			message = err.message;
		}
	} catch {
		// Body was not JSON or could not be parsed; fall through to
		// generic defaults below.
	}
	if (code.length === 0) {
		code = response.status >= 500 ? "internal_error" : "invalid_request";
	}
	if (message.length === 0) {
		message =
			response.status >= 500
				? "service is temporarily unavailable"
				: `request rejected (${response.status})`;
	}
	return new AuthError(code, message, response.status);
}

function joinUrl(baseUrl: string, path: string): string {
	const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
	const trimmedPath = path.startsWith("/") ? path : `/${path}`;
	return `${trimmedBase}${trimmedPath}`;
}

function encodeBase64(bytes: Uint8Array): string {
	let binary = "";
	for (let i = 0; i < bytes.length; i++) {
		binary += String.fromCharCode(bytes[i]!);
	}
	return btoa(binary);
}
