/**
 * ------------------------------
 * Plugin authoring helpers
 * ------------------------------
 *
 * - `definePlugin`: identity wrapper that constrains a plugin's return to
 *   `PluginProvides` while preserving the narrow inferred shape, so callers
 *   can derive their `*PluginProvides` type via `ReturnType<typeof plugin>`
 *   instead of declaring it by hand.
 * - `createPluginMethod` / `createPaginatedPluginMethod`: per-method
 *   primitives that sit inside a `definePlugin` callback and build the
 *
 *     { [name]: wrappedFn, context: { meta: { [name]: meta } } }
 *
 *   fragment a plugin returns for a single method, wiring up
 *   `createFunction` / `createPaginatedFunction`, the telemetry callback,
 *   and the doubled `name` (function key + meta key) in one place.
 *
 * Two method helpers (rather than one with a `paginated: true` discriminant)
 * because the handler signature changes shape across pagination, and
 * discriminated unions on optional booleans produce noisy TS errors.
 */
import { createFunction, createPaginatedFunction } from "./function-utils";
import { createTelemetryCallback } from "./telemetry-utils";
/**
 * Identity helper that preserves the narrow inferred return type of a plugin.
 *
 * Wrapping a plugin function with `definePlugin(...)` lets callers derive the
 * plugin's contribution via `ReturnType<typeof plugin>` instead of declaring a
 * matching `*PluginProvides` interface by hand. The constraint
 * `TProvides extends PluginProvides` enforces the structural floor (must
 * return a valid plugin output); beyond that the inferred shape flows through.
 */
export function definePlugin(fn) {
    return fn;
}
/**
 * Build a telemetry callback only when the SDK actually has eventEmission
 * wired up. Plugin tests sometimes assemble a minimal SDK without the
 * eventEmission plugin; in that case telemetry is silently skipped, matching
 * the legacy `createFunction` behavior of accepting an undefined telemetry
 * arg.
 */
function maybeTelemetry(sdk, name) {
    const ctx = sdk.context;
    if (!ctx.eventEmission)
        return undefined;
    return createTelemetryCallback(ctx.eventEmission.emitMethodCalled, name);
}
/**
 * Build the method fragment for a non-paginated SDK method. Used inside a
 * `definePlugin(...)` callback:
 *
 *   export const getProfilePlugin = definePlugin(
 *     (sdk: ApiPluginProvides & EventEmissionProvides) =>
 *       createPluginMethod(sdk, {
 *         name: "getProfile",
 *         categories: ["account"],
 *         inputSchema: GetProfileSchema,
 *         handler: async ({ sdk }) => { ... },
 *       }),
 *   );
 */
export function createPluginMethod(sdk, config) {
    const { name, inputSchema, handler, ...metaFields } = config;
    // Preserve the method name on the inner function so telemetry and any
    // `function.name`-based diagnostics see the right value.
    const namedHandlers = {
        [name]: async function (options) {
            return handler({ sdk, options });
        },
    };
    const wrappedFn = createFunction(namedHandlers[name], inputSchema, maybeTelemetry(sdk, name));
    return {
        [name]: wrappedFn,
        context: {
            meta: {
                [name]: {
                    ...metaFields,
                    ...(inputSchema ? { inputSchema } : {}),
                },
            },
        },
    };
}
/**
 * Paginated variant of `createPluginMethod`. The handler receives
 * `{ cursor?, pageSize? }` in `options` and returns whatever shape the
 * upstream API gives back, as long as it has a `data: TItem[]` field.
 * Pair JSON:API responses with `extractCursor` to make the cursor
 * extraction visible at the call site:
 *
 *   createPaginatedPluginMethod(sdk, {
 *     name: "listApps",
 *     inputSchema: ListAppsSchema,
 *     handler: ({ sdk, options }) =>
 *       sdk.context.api.get<ListAppsResponse>("/api/v0/apps", {...}),
 *     extractCursor: (response) => extractCursor(response.links),
 *   });
 */
export function createPaginatedPluginMethod(sdk, config) {
    const { name, inputSchema, handler, extractCursor: extractCursorFn, defaultPageSize, ...metaFields } = config;
    const namedPageHandlers = {
        [name]: async function (options) {
            const response = await handler({ sdk, options });
            if (extractCursorFn) {
                const extracted = extractCursorFn(response);
                // Pass through the original response when the extractor doesn't
                // produce a cursor — that way the runtime in `createPageFunction`
                // can still auto-detect `nextCursor` or JSON:API `links.next` if
                // they're present (e.g. from test mocks that don't go through the
                // real API envelope).
                if (extracted !== undefined) {
                    return { data: response.data, nextCursor: extracted };
                }
            }
            return response;
        },
    };
    const wrappedFn = createPaginatedFunction(namedPageHandlers[name], inputSchema, maybeTelemetry(sdk, name), name, defaultPageSize);
    return {
        [name]: wrappedFn,
        context: {
            meta: {
                [name]: {
                    ...metaFields,
                    ...(inputSchema ? { inputSchema } : {}),
                },
            },
        },
    };
}
// ----------------------------------------------------------------------------
// composePlugins — runtime merge helpers
// ----------------------------------------------------------------------------
// Three merge rules, one per shape of key the function deals with. Each is
// a small, single-purpose helper so the main loop reads top-to-bottom.
/**
 * Merge root-level keys (the SDK methods a plugin contributes) into the
 * accumulator. Throws on collision: composePlugins is bag-mode, so two
 * sub-plugins registering the same method name is a programmer error.
 */
function mergeRootKeysWithCollisionCheck(target, source, seen) {
    for (const key of Object.keys(source)) {
        if (seen.has(key)) {
            throw new Error(`composePlugins: duplicate root key "${key}". Plugins composed in the same call must register distinct method names.`);
        }
        seen.add(key);
        target[key] = source[key];
    }
}
/**
 * Merge per-method metadata (the registry view of input/output schemas,
 * categories, etc.) with the same collision rule as root keys. Even when
 * two sub-plugins' root method keys differ, registry-level shadowing
 * is dangerous, so silent overwrites aren't allowed.
 */
function mergeMetaKeysWithCollisionCheck(target, source, seen) {
    for (const key of Object.keys(source)) {
        if (seen.has(key)) {
            throw new Error(`composePlugins: duplicate context.meta key "${key}". Two sub-plugins registered metadata for the same name.`);
        }
        seen.add(key);
        target[key] = source[key];
    }
}
/**
 * Merge non-meta context keys (everything under `context.*` that isn't
 * `meta`) into the accumulator. Throws on collision — bag mode says
 * sub-plugins shouldn't be re-providing shared infrastructure like
 * `context.api`, so two of them claiming the same context slot is a
 * programmer error. Note that this only applies *inside* a single
 * `composePlugins(...)` call; cross-`addPlugin` overrides go through
 * `addPlugin`'s last-write-wins merge, which is unaffected by this check.
 */
function mergeContextKeysWithCollisionCheck(target, source, seen) {
    for (const key of Object.keys(source)) {
        if (seen.has(key)) {
            throw new Error(`composePlugins: duplicate context key "${key}". Two sub-plugins contributed the same context.${key} field.`);
        }
        seen.add(key);
        target[key] = source[key];
    }
}
/**
 * Bundle N plugins into a single plugin so a consumer can call
 * `addPlugin(combined)` once. Use this when an extension package wants to
 * expose granular sub-plugins for tree-shaking *and* a one-call convenience
 * entry point.
 *
 * Three commitments (per the boilerplate-reduction design):
 * 1. **Bag mode.** Sub-plugins must not depend on each other; order is
 *    irrelevant. If you need pipeline composition (later sub-plugins
 *    reading earlier sub-plugins' outputs), use a regular `definePlugin`
 *    callback that calls each `createPluginMethod` in turn.
 * 2. **Over-approximation of TSdk.** The composed plugin requires the
 *    intersection of every sub-plugin's requirements (all of them, all at
 *    once). Works fine for homogeneous-deps groups; widens slightly for
 *    heterogeneous ones, which keeps the variadic types tractable.
 * 3. **Collision detection.** Throws on duplicate root keys, duplicate
 *    `context.meta` keys, and duplicate non-meta `context.*` keys
 *    *inside* a single `composePlugins(...)` call. Cross-`addPlugin`
 *    collisions are handled separately by `addPlugin` itself, which
 *    warns-and-skips the colliding plugin (the first registration
 *    wins). Pure meta-only overrides (e.g. `cliOverridesPlugin`
 *    patching `meta.fetch.deprecated` without re-registering `fetch`)
 *    stay legitimate in both code paths.
 *
 * Why prefer this over an `addPlugin([...])` array overload: TypeScript's
 * overload resolution interacts badly with arrays carrying a mix of
 * `TProvides` shapes — chained inference downstream of `addPlugin([...])`
 * tends to widen and lose individual method types. A single composed
 * plugin flows through the existing single-plugin `addPlugin` cleanly.
 */
export function composePlugins(...plugins) {
    return ((sdk) => {
        const merged = { context: { meta: {} } };
        const seenRoot = new Set();
        const seenMeta = new Set();
        const seenContext = new Set();
        for (const plugin of plugins) {
            const { context, ...rootKeys } = plugin(sdk);
            mergeRootKeysWithCollisionCheck(merged, rootKeys, seenRoot);
            if (context) {
                const { meta, ...nonMetaContext } = context;
                if (meta) {
                    mergeMetaKeysWithCollisionCheck(merged.context.meta, meta, seenMeta);
                }
                mergeContextKeysWithCollisionCheck(merged.context, nonMetaContext, seenContext);
            }
        }
        return merged;
    });
}
