/**
 * Typed RPC client — creates a Proxy-based API from action specs.
 *
 * Two tiers of usage:
 * - **Tier 1** (simple, for tx): transport send/receive, Result return. No `environment`.
 * - **Tier 2** (full, for zzz): ActionEvent lifecycle with `environment`.
 *
 * Pass the consumer's generated `ActionsApi` interface as `<TApi>` to flow
 * full type safety through without an explicit cast at the call site.
 *
 * @module
 */
import { create_action_event } from './action_event.js';
import { is_send_request, is_notification_send, extract_action_result, } from './action_event_helpers.js';
import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
/**
 * Creates a Proxy-based API from action specs.
 *
 * Method calls are dynamically dispatched based on the action spec's kind:
 * - `request_response` → send request, await response, return Result
 * - `remote_notification` → send notification, return Result
 * - `local_call` → execute locally (sync or async), return Result or throw
 *
 * Generic `TApi` is the consumer's typed Proxy interface (typically a
 * codegen-derived `ActionsApi`). Required — no default, so forgetting it
 * is a type error rather than a silent slide into `any`. The `as unknown
 * as TApi` coercion lives inside this function so call sites get a typed
 * return without a cast at the seam. `TApi` is a type-layer promise about
 * what the Proxy responds to; the runtime walks `specs` (kept in sync by
 * the consumer, codegen recommended).
 *
 * ```ts
 * const api_result = create_rpc_client<MyActionsApi>({peer, environment});
 * ```
 *
 * @returns a Proxy typed as `TApi` that responds to any method name found in the environment's specs
 */
export const create_rpc_client = (options) => {
    const { peer, environment, on_action_event, transport_for_method } = options;
    // Internal factories construct broadly-typed `ActionEvent` instances; the
    // public callback narrows `event.spec.method` to `keyof TApi & string`.
    // Cast once here — function parameters are contravariant, so the narrow
    // callback isn't directly assignable to the broad slot the helpers take.
    const broad_on_action_event = on_action_event;
    return new Proxy({}, {
        get(_target, method) {
            const spec = environment.lookup_action_spec(method);
            if (!spec) {
                return undefined;
            }
            return create_action_method(peer, environment, spec, broad_on_action_event, transport_for_method);
        },
        has(_target, method) {
            return environment.lookup_action_spec(method) !== undefined;
        },
    });
};
const create_action_method = (peer, environment, spec, on_action_event, transport_for_method) => {
    switch (spec.kind) {
        case 'local_call':
            return spec.async
                ? create_async_local_call_method(environment, spec, on_action_event)
                : create_sync_local_call_method(environment, spec, on_action_event);
        case 'request_response':
            return create_request_response_method(peer, environment, spec, on_action_event, transport_for_method);
        case 'remote_notification':
            return create_remote_notification_method(peer, environment, spec, on_action_event, transport_for_method);
    }
};
/** Sync local-call dispatch — returns the value directly; throws on error (no Result wrapping). */
const create_sync_local_call_method = (environment, spec, on_action_event) => {
    return (input) => {
        const event = create_action_event(environment, spec, input);
        on_action_event?.(event);
        event.parse().handle_sync();
        const result = extract_action_result(event);
        if (result.ok) {
            return result.value;
        }
        else {
            // Sync methods must throw on error (cannot return Result synchronously)
            throw new Error(`${spec.method} failed: ${result.error.message}`);
        }
    };
};
/**
 * Async local-call dispatch — returns Result.
 *
 * Local calls don't traverse a transport, so `transport_name` is ignored and
 * `signal` can only short-circuit before the synchronous handler runs (no
 * cooperative interrupt mid-handler).
 */
const create_async_local_call_method = (environment, spec, on_action_event) => {
    return async (input, options) => {
        if (options?.signal?.aborted) {
            return {
                ok: false,
                error: jsonrpc_error_messages.internal_error(`${spec.method} aborted before execution`),
            };
        }
        const event = create_action_event(environment, spec, input);
        on_action_event?.(event);
        await event.parse().handle_async();
        return extract_action_result(event);
    };
};
const create_request_response_method = (peer, environment, spec, on_action_event, transport_for_method) => {
    return async (input, options) => {
        const event = create_action_event(environment, spec, input);
        on_action_event?.(event);
        await event.parse().handle_async();
        // Check if we're in send_error phase before type narrowing
        if (event.data.kind === 'request_response' && event.data.phase === 'send_error') {
            await event.handle_async(); // Call send_error handler
            return extract_action_result(event);
        }
        if (!is_send_request(event.data))
            throw Error(); // TODO @many maybe make this an assertion helper?
        if (event.data.step !== 'handled') {
            return extract_action_result(event);
        }
        const response = await peer.send(event.data.request, {
            transport_name: options?.transport_name ?? transport_for_method?.(spec.method),
            signal: options?.signal,
            queue: options?.queue,
        });
        event.transition('receive_response');
        // TODO @api shouldn't this happen in the peer like the other method calls?
        event.set_response(response);
        event.parse(); // May transition to receive_error
        await event.handle_async();
        return extract_action_result(event);
    };
};
/** Fire-and-forget remote notification — returns `Result<{value: void}>` for consistency with `request_response`. */
const create_remote_notification_method = (peer, environment, spec, on_action_event, transport_for_method) => {
    return async (input, options) => {
        const event = create_action_event(environment, spec, input);
        on_action_event?.(event);
        await event.parse().handle_async();
        if (!is_notification_send(event.data))
            throw Error(); // TODO @many maybe make this an assertion helper?
        if (event.data.step === 'handled') {
            const send_result = await peer.send(event.data.notification, {
                transport_name: options?.transport_name ?? transport_for_method?.(spec.method),
                signal: options?.signal,
                queue: options?.queue,
            });
            // Check if notification failed to send
            if (send_result !== null) {
                environment.log?.error('notification send failed:', send_result.error);
                return { ok: false, error: send_result.error };
            }
            return { ok: true, value: undefined };
        }
        return extract_action_result(event);
    };
};
/**
 * Wrap a typed RPC client so every call resolves to its unwrapped value or
 * throws an `Error` carrying the JSON-RPC `{code, message, data?}` shape.
 *
 * Implementation is a Proxy because the underlying `create_rpc_client`
 * return is itself a Proxy with no concrete keys — a key-by-key wrap would
 * need to enumerate the typed surface, which only the consumer's generated
 * `ActionsApi` interface knows.
 *
 * Pass-through on non-Result returns is deliberate: sync `local_call`
 * Proxy methods return values directly (see `create_sync_local_call_method`
 * above). The Proxy can't distinguish those at get-time, so the wrapper
 * inspects `result` shape at call-time and only unwraps when it sees a
 * Result. Non-object returns pass through unchanged.
 *
 * Only `{code, data}` cross onto the thrown Error — `name` / `stack` are
 * left as the Error's own properties so attacker-shaped `result.error`
 * payloads cannot overwrite them.
 *
 * Recommended consumer convention: `create_frontend_rpc_client` ships
 * both shapes by default — `api` (throwing) for hot-path call sites and
 * `api_result` (Result) for sites that inspect `error.data.reason`
 * without try/catch. Result is the protocol primitive; this wrapper is
 * the ergonomic layer over it. Picking is per call site — both Proxies
 * share the same underlying transport.
 *
 * Catch blocks read `err.data?.reason` — optional chaining required
 * because JSON-RPC `data` is spec-level optional.
 *
 * On unknown string-keyed methods, the get trap returns a function that
 * throws `"rpc method not found: <prop>"` on invocation — clearer than
 * the JS default `"api.foo is not a function"`. Symbol props and `then`
 * stay undefined so the Proxy isn't accidentally treated as a thenable
 * (`await api` would otherwise probe `then` and trip the thrower).
 *
 * @param api_result - typed Result-returning RPC client from
 *   `create_rpc_client<ActionsApi>(...)`. The "_result" suffix names
 *   what the underlying calls return (`Result<{value}, {error}>`).
 */
export const create_throwing_api = (api_result) => {
    return new Proxy(api_result, {
        get(target, prop) {
            const fn = target[prop];
            if (typeof fn === 'function') {
                return async (...args) => {
                    const result = await fn.apply(target, args);
                    if (result === null || typeof result !== 'object')
                        return result;
                    const r = result;
                    if (r.ok === true)
                        return r.value;
                    if (r.ok === false && r.error && typeof r.error === 'object') {
                        const e = r.error;
                        throw Object.assign(new Error(e.message), {
                            code: e.code,
                            data: e.data,
                        });
                    }
                    return result;
                };
            }
            if (fn !== undefined)
                return fn;
            // Underlying api has no member by this name. Symbol props and
            // `then` must stay undefined — `await tapi` reads `then` and
            // would otherwise trip the thrower.
            if (typeof prop !== 'string' || prop === 'then')
                return undefined;
            return () => {
                throw new Error(`rpc method not found: ${prop}`);
            };
        },
    });
};
