/**
 * SyncHttpClient adapter over the platform `fetch` API.
 *
 * Implements `SyncHttpClientPort` using `globalThis.fetch` for HTTP
 * dispatch and `JSON.stringify` / `Response.json()` for body
 * serialization. This is the default transport for
 * `SyncController.syncWith(remote)` when the remote is a URL rather
 * than an in-process peer.
 *
 * Keeping this adapter in `src/infrastructure/adapters/` preserves
 * the hexagonal wall: `fetch` and `JSON.stringify` stay out of
 * `src/domain/**`.
 */
import SyncHttpClientPort from "../../ports/SyncHttpClientPort.js";
import { signSyncRequest, canonicalizePath } from "../../domain/services/sync/SyncAuthService.js";
import OperationPolicyTimeoutError from "../../domain/errors/OperationPolicyTimeoutError.js";
import AlfredOperationPolicyAdapter from "./AlfredOperationPolicyAdapter.js";
/**
 * Implementation of SyncHttpClientPort using `fetch`.
 */
export default class FetchSyncHttpClientAdapter extends SyncHttpClientPort {
    _policy;
    constructor(options = {}) {
        super();
        this._policy = options.policy ?? new AlfredOperationPolicyAdapter();
    }
    async exchange(request, telemetry) {
        const bodyStr = JSON.stringify(request.body);
        const headers = await resolveHeaders(request, bodyStr);
        telemetry.onRequestSent();
        const res = await this._sendRequest(request, bodyStr, headers);
        if (res.kind !== 'http-response') {
            return res;
        }
        telemetry.onResponseReceived(res.response.status);
        return await this._interpretResponse(res.response);
    }
    /**
     * Performs the `fetch` call, translating transport-level failures
     * (timeout, abort, network) into typed result variants.
     */
    async _sendRequest(request, bodyStr, headers) {
        try {
            const response = await this._policy.execute((policySignal) => {
                const combinedSignal = combineAbortSignals(policySignal, request.signal);
                const init = {
                    method: 'POST',
                    headers,
                    body: bodyStr,
                    ...(combinedSignal !== undefined ? { signal: combinedSignal } : {}),
                };
                return fetch(request.targetUrl.toString(), init);
            }, { timeoutMs: request.timeoutMs });
            return { kind: 'http-response', response };
        }
        catch (err) {
            return classifyTransportError(err);
        }
    }
    /**
     * Inspects an HTTP response status and body, producing the final
     * SyncHttpClientResult. Body decoding uses `.json()` which itself
     * may throw — we translate that into a decode-failure variant.
     */
    async _interpretResponse(response) {
        if (response.status >= 400) {
            return { kind: 'status-failure', status: response.status };
        }
        try {
            const body = await response.json();
            return { kind: 'success', status: response.status, response: body };
        }
        catch {
            return { kind: 'decode-failure', status: response.status };
        }
    }
}
/**
 * Computes the final request headers — merges caller headers with
 * auth signatures computed from the serialized body.
 */
async function resolveHeaders(request, bodyStr) {
    if (request.auth === undefined) {
        return request.headers;
    }
    const authHeaders = await signForRequest(request.auth, request.targetUrl, bodyStr);
    return { ...request.headers, ...authHeaders };
}
/**
 * Signs a serialized body and returns the HMAC auth headers.
 */
async function signForRequest(auth, targetUrl, bodyStr) {
    const bodyBuf = new TextEncoder().encode(bodyStr);
    return await signSyncRequest({
        method: 'POST',
        path: canonicalizePath(targetUrl.pathname + (targetUrl.search || '')),
        contentType: 'application/json',
        body: bodyBuf,
        secret: auth.secret,
        keyId: auth.keyId !== undefined && auth.keyId !== '' ? auth.keyId : 'default',
        lamport: auth.lamport,
        authScheme: auth.scheme,
    }, { crypto: auth.crypto });
}
/**
 * Classifies an exception thrown from the fetch/timeout path into a
 * typed SyncHttpClientResult variant.
 */
function classifyTransportError(err) {
    if (err instanceof OperationPolicyTimeoutError) {
        return { kind: 'timeout' };
    }
    if (err instanceof Error && err.name === 'AbortError') {
        return { kind: 'aborted' };
    }
    const message = err instanceof Error ? err.message : String(err);
    return { kind: 'network-failure', message };
}
function combineAbortSignals(first, second) {
    if (first === undefined) {
        return second;
    }
    if (second === undefined) {
        return first;
    }
    return AbortSignal.any([first, second]);
}
