import { definePlugin } from "../../utils/plugin-utils";
import { ZapierRelayError, ZapierTimeoutError } from "../../types/errors";
import { FetchUrlSchema, FetchInitSchema, FetchInitZapierFieldsSchema, } from "./schemas";
import { z } from "zod";
import { resolveConnectionId } from "../../utils/domain-utils";
import { combineAbortSignals, createTimeoutAbortSignal, } from "../../utils/abort-utils";
import { isPlainObject } from "../../utils/type-guard-utils";
import { buildHttpRequestContext } from "@zapier/policy-context";
// fetch() doesn't use createFunction/createPaginatedFunction (its signature
// doesn't fit the single-options-object pattern), so we wire up telemetry
// context tracking manually.
import { isTelemetryNested, runWithTelemetryContext, } from "../../utils/telemetry-context";
import { createValidator } from "../../utils/validation";
/**
 * Transforms full URLs into Relay format: /relay/{domain}/{path}
 */
function transformUrlToRelayPath(url) {
    const targetUrl = new URL(url);
    return `/relay/${targetUrl.host}${targetUrl.pathname}${targetUrl.search}${targetUrl.hash}`;
}
/**
 * Normalizes various header formats into a plain Record<string, string>.
 */
function normalizeHeaders(optionsHeaders) {
    const headers = {};
    if (!optionsHeaders) {
        return headers;
    }
    const headerEntries = optionsHeaders instanceof Headers
        ? Array.from(optionsHeaders.entries())
        : Array.isArray(optionsHeaders)
            ? optionsHeaders
            : Object.entries(optionsHeaders);
    for (const [key, value] of headerEntries) {
        headers[key] = value;
    }
    return headers;
}
function inferContentType(body) {
    if (body instanceof URLSearchParams) {
        return "application/x-www-form-urlencoded; charset=utf-8";
    }
    if (typeof body === "string") {
        const trimmed = body.trimStart();
        if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
            return undefined;
        }
        try {
            JSON.parse(trimmed);
            return "application/json; charset=utf-8";
        }
        catch {
            return undefined;
        }
    }
    if (isPlainObject(body) || Array.isArray(body)) {
        return "application/json; charset=utf-8";
    }
    return undefined;
}
/**
 * If the request aborted because the client-side `maxTime` timer fired, rewrap the
 * underlying error as a `ZapierTimeoutError` so callers (and telemetry) can
 * distinguish it from a caller-triggered abort or an unrelated network
 * cancellation. The original error is preserved as `cause`.
 *
 * Caller-triggered aborts are left unwrapped so the caller's own abort
 * semantics (reason, name) propagate.
 */
function rewrapIfMaxTimeTimeout({ error, abortSignal, maxTimeSeconds, }) {
    if (maxTimeSeconds === undefined || !abortSignal?.aborted)
        return error;
    const reason = abortSignal.reason;
    if (!reason || reason.name !== "TimeoutError")
        return error;
    return new ZapierTimeoutError(`fetch timed out after ${maxTimeSeconds}s (maxTime)`, { cause: error });
}
/**
 * Builds an AbortSignal (and a dispose callback) from the optional maxTime
 * timeout and an optional caller-provided signal. Callers must invoke
 * `dispose()` when the request completes to release timers and listeners.
 */
function buildAbortHandle({ maxTimeSeconds, callerSignal, }) {
    const handles = [];
    if (callerSignal)
        handles.push({ signal: callerSignal, dispose: () => { } });
    if (maxTimeSeconds !== undefined) {
        handles.push(createTimeoutAbortSignal({ timeoutMs: maxTimeSeconds * 1000 }));
    }
    return combineAbortSignals({ handles });
}
const FetchInitSdkValidationSchema = z
    .looseObject(FetchInitZapierFieldsSchema.shape)
    .optional();
const validateFetchInit = createValidator(FetchInitSdkValidationSchema);
/**
 * Fetch plugin, the primary way to make authenticated HTTP requests through Zapier's Relay service.
 * Mirrors the native fetch(url, init?) signature with additional Zapier-specific options.
 */
export const fetchPlugin = definePlugin((sdk) => {
    return {
        fetch: async function fetch(url, init) {
            return runWithTelemetryContext(async () => {
                const { api } = sdk.context;
                const startTime = Date.now();
                const isNested = isTelemetryNested();
                try {
                    if (init) {
                        validateFetchInit(init);
                    }
                    const { connectionId, connection, authenticationId, callbackUrl, maxTime, ...fetchInit } = init || {};
                    const resolvedConnectionId = await resolveConnectionId({
                        connectionId,
                        connection,
                        authenticationId,
                        resolveConnection: sdk.context.resolveConnection,
                        label: "fetch",
                    });
                    const relayPath = transformUrlToRelayPath(url);
                    const headers = normalizeHeaders(fetchInit.headers);
                    const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
                    if (fetchInit.body && !hasContentType) {
                        const inferred = inferContentType(fetchInit.body);
                        if (inferred) {
                            headers["Content-Type"] = inferred;
                        }
                    }
                    if (resolvedConnectionId) {
                        headers["X-Relay-Authentication-Id"] =
                            String(resolvedConnectionId);
                    }
                    if (callbackUrl) {
                        headers["X-Relay-Callback-Url"] = callbackUrl;
                    }
                    if (maxTime !== undefined) {
                        headers["X-Zapier-Sdk-Max-Time"] = String(maxTime);
                    }
                    const upstreamUrl = new URL(url).toString();
                    const method = (fetchInit.method ?? "GET").toUpperCase();
                    const abortHandle = buildAbortHandle({
                        maxTimeSeconds: maxTime,
                        callerSignal: fetchInit.signal,
                    });
                    try {
                        const result = await api.fetch(relayPath, {
                            method,
                            body: fetchInit.body,
                            headers,
                            redirect: fetchInit.redirect,
                            signal: abortHandle?.signal,
                            authRequired: true,
                            approvalContext: () => buildHttpRequestContext({
                                method,
                                url: upstreamUrl,
                                headers,
                                body: typeof fetchInit.body === "string"
                                    ? fetchInit.body
                                    : undefined,
                                connection_id: resolvedConnectionId
                                    ? String(resolvedConnectionId)
                                    : undefined,
                            }),
                        });
                        const relayError = result.headers.get("x-relay-error");
                        if (relayError) {
                            throw new ZapierRelayError(relayError, {
                                statusCode: result.status,
                            });
                        }
                        if (!isNested) {
                            sdk.context.eventEmission.emitMethodCalled({
                                method_name: "fetch",
                                execution_duration_ms: Date.now() - startTime,
                                success_flag: true,
                                error_message: null,
                                error_type: null,
                                argument_count: init ? 2 : 1,
                                is_paginated: false,
                            });
                        }
                        return result;
                    }
                    catch (error) {
                        throw rewrapIfMaxTimeTimeout({
                            error,
                            abortSignal: abortHandle?.signal,
                            maxTimeSeconds: maxTime,
                        });
                    }
                    finally {
                        abortHandle?.dispose();
                    }
                }
                catch (error) {
                    if (!isNested) {
                        sdk.context.eventEmission.emitMethodCalled({
                            method_name: "fetch",
                            execution_duration_ms: Date.now() - startTime,
                            success_flag: false,
                            error_message: error instanceof Error ? error.message : String(error),
                            error_type: error instanceof Error ? error.constructor.name : "Unknown",
                            argument_count: init ? 2 : 1,
                            is_paginated: false,
                        });
                    }
                    throw error;
                }
            });
        },
        context: {
            meta: {
                fetch: {
                    description: "Make authenticated HTTP requests to any API through Zapier. " +
                        "Pass a connectionId to automatically inject the user's stored credentials " +
                        "(OAuth tokens, API keys, etc.) into the outgoing request. " +
                        "Mirrors the native fetch(url, init?) signature with additional Zapier-specific options.",
                    packages: ["sdk", "cli", "mcp"],
                    categories: ["http"],
                    returnType: "Response",
                    inputParameters: [
                        { name: "url", schema: FetchUrlSchema },
                        { name: "init", schema: FetchInitSchema },
                    ],
                },
            },
        },
    };
});
