/**
 * Event Emission Plugin for Zapier SDK
 *
 * Provides silent telemetry event emission capability to the SDK.
 * All events are emitted asynchronously and failures are silently handled.
 */
import { definePlugin } from "../../utils/plugin-utils";
import { createTransport } from "./transport";
import { generateEventId, getCurrentTimestamp, getReleaseId } from "./utils";
import { extractUserIdsFromJwt } from "../../api/auth";
import { resolveAuthToken } from "../../auth";
import { buildApplicationLifecycleEvent, buildErrorEventWithContext, buildMethodCalledEvent, } from "./builders";
import { getTrackingBaseUrl } from "../../utils/url-utils";
// Maximum time to wait for telemetry emission before allowing process to exit
const TELEMETRY_EMIT_TIMEOUT_MS = 300;
let registeredListeners = {};
function removeExistingListeners() {
    const events = [
        "exit",
        "uncaughtException",
        "unhandledRejection",
        "SIGINT",
        "SIGTERM",
    ];
    for (const event of events) {
        const listener = registeredListeners[event];
        if (listener) {
            globalThis.process?.removeListener(event, listener);
        }
    }
    registeredListeners = {};
}
/**
 * Removes all registered process event listeners.
 * Useful for test cleanup to prevent listener accumulation.
 */
export function cleanupEventListeners() {
    removeExistingListeners();
}
const APPLICATION_LIFECYCLE_EVENT_SUBJECT = "platform.sdk.ApplicationLifecycleEvent";
const ERROR_OCCURRED_EVENT_SUBJECT = "platform.sdk.ErrorOccurredEvent";
const METHOD_CALLED_EVENT_SUBJECT = "platform.sdk.MethodCalledEvent";
async function emitWithTimeout(transport, subject, event) {
    try {
        await Promise.race([
            transport.emit(subject, event),
            new Promise((resolve) => {
                const timer = setTimeout(resolve, TELEMETRY_EMIT_TIMEOUT_MS);
                if (typeof timer.unref === "function") {
                    timer.unref();
                }
            }),
        ]);
    }
    catch {
        // Silently ignore telemetry failures
    }
}
function mergeUserContext(event, userContext) {
    const merged = { ...event };
    for (const [key, value] of Object.entries(userContext)) {
        if (merged[key] == null) {
            merged[key] = value;
        }
    }
    return merged;
}
async function silentEmit(transport, subject, event, userContextPromise) {
    try {
        let enrichedEvent = event;
        if (userContextPromise) {
            try {
                const userContext = await userContextPromise;
                enrichedEvent = mergeUserContext(event, userContext);
            }
            catch {
                // If user context promise fails, continue with original event
            }
        }
        await transport.emit(subject, enrichedEvent);
    }
    catch {
        // Silently ignore all errors
    }
}
// Helper to get transport config from environment or options
function getTransportConfig(options) {
    const envTransport = globalThis.process?.env?.ZAPIER_SDK_TELEMETRY_TRANSPORT;
    if (envTransport === "noop" || envTransport === "disabled") {
        return { type: "noop" };
    }
    if (envTransport === "console") {
        return { type: "console" };
    }
    // Default to HTTP transport with resolved tracking URL
    const endpoint = globalThis.process?.env?.ZAPIER_SDK_TELEMETRY_ENDPOINT ||
        `${getTrackingBaseUrl({
            trackingBaseUrl: options?.trackingBaseUrl,
            baseUrl: options?.baseUrl,
        })}/api/v4/tracking/event/`;
    return {
        type: "http",
        endpoint,
    };
}
export const eventEmissionPlugin = definePlugin((sdk) => {
    const options = sdk.context.options ?? {};
    const defaultTransport = getTransportConfig({
        trackingBaseUrl: options.trackingBaseUrl,
        baseUrl: options.baseUrl,
    });
    // Merge config: env var takes precedence over options, options take precedence over defaults
    const config = {
        enabled: options.eventEmission?.enabled ?? true,
        callContext: options.eventEmission?.callContext,
        transport: 
        // If env var is set, use it (defaultTransport will be from env)
        globalThis.process?.env?.ZAPIER_SDK_TELEMETRY_TRANSPORT
            ? defaultTransport
            : // Otherwise, use option transport or default
                (options.eventEmission?.transport ?? defaultTransport),
    };
    // Create getUserContext promise for dynamic user context injection
    const getUserContext = (async () => {
        if (config.enabled) {
            try {
                const token = await resolveAuthToken({
                    ...options,
                });
                if (token) {
                    return extractUserIdsFromJwt(token);
                }
            }
            catch {
                // Fall back to null context on any error
            }
        }
        return { customuser_id: null, account_id: null };
    })();
    const startupTime = Date.now();
    let shutdownStartTime = null;
    let closed = false;
    const pendingEmissions = new Set();
    function trackEmission(promise) {
        pendingEmissions.add(promise);
        promise.finally(() => {
            pendingEmissions.delete(promise);
        });
    }
    async function flush() {
        while (pendingEmissions.size > 0) {
            await Promise.allSettled([...pendingEmissions]);
        }
    }
    // If disabled, return noop implementations
    if (!config.enabled) {
        return {
            context: {
                eventEmission: {
                    transport: createTransport({ type: "noop" }),
                    config,
                    emit: () => { },
                    createBaseEvent: async () => ({
                        event_id: generateEventId(),
                        timestamp_ms: getCurrentTimestamp(),
                        release_id: getReleaseId(),
                        customuser_id: null,
                        account_id: null,
                        identity_id: null,
                        visitor_id: null,
                        correlation_id: null,
                    }),
                    emitMethodCalled: () => { },
                    flush: async () => { },
                    close: async () => { },
                },
            },
        };
    }
    let transport;
    try {
        transport = createTransport(config.transport || { type: "noop" });
    }
    catch {
        // If transport creation fails, fallback to noop to maintain silent operation
        transport = createTransport({ type: "noop" });
    }
    // Helper to create base event
    const createBaseEventHelper = async () => {
        const baseEvent = {
            event_id: generateEventId(),
            timestamp_ms: getCurrentTimestamp(),
            release_id: getReleaseId(),
            customuser_id: null,
            account_id: null,
            identity_id: null,
            visitor_id: null,
            correlation_id: null,
        };
        // Enrich with user context if available
        try {
            const userContext = await getUserContext;
            return mergeUserContext(baseEvent, userContext);
        }
        catch {
            // Return base event if user context fails
            return baseEvent;
        }
    };
    // Register lifecycle event handlers if enabled
    if (config.enabled) {
        // Emit startup event
        const startupEvent = buildApplicationLifecycleEvent({
            lifecycle_event_type: "startup",
        });
        trackEmission(silentEmit(transport, APPLICATION_LIFECYCLE_EVENT_SUBJECT, startupEvent, getUserContext));
        // Register process event handlers (Node.js only)
        if (typeof globalThis.process?.on === "function") {
            // Remove any existing listeners from previous SDK instances to prevent memory leaks
            removeExistingListeners();
            // Handle normal process exit
            const exitHandler = (code) => {
                if (closed)
                    return;
                const uptime = Date.now() - startupTime;
                const shutdownDuration = shutdownStartTime
                    ? Date.now() - shutdownStartTime
                    : null;
                const exitEvent = buildApplicationLifecycleEvent({
                    lifecycle_event_type: "exit",
                    exit_code: code,
                    uptime_ms: uptime,
                    is_graceful_shutdown: code === 0,
                    shutdown_duration_ms: shutdownDuration,
                });
                trackEmission(silentEmit(transport, APPLICATION_LIFECYCLE_EVENT_SUBJECT, exitEvent, getUserContext));
            };
            registeredListeners.exit = exitHandler;
            globalThis.process.on("exit", exitHandler);
            // Handle uncaught exceptions
            const uncaughtExceptionHandler = async (error) => {
                let errorEvent = buildErrorEventWithContext({
                    error_message: error.message || "Unknown error",
                    error_type: "UncaughtException",
                    error_stack_trace: error.stack || null,
                    error_severity: "critical",
                    is_user_facing: false,
                    is_recoverable: false,
                    execution_start_time: startupTime,
                });
                // Enrich with user context if available
                try {
                    const userContext = await getUserContext;
                    errorEvent = mergeUserContext(errorEvent, userContext);
                }
                catch {
                    // Continue with original event if user context fails
                }
                await emitWithTimeout(transport, ERROR_OCCURRED_EVENT_SUBJECT, errorEvent);
            };
            registeredListeners.uncaughtException = uncaughtExceptionHandler;
            globalThis.process.on("uncaughtException", uncaughtExceptionHandler);
            // Handle unhandled promise rejections
            const unhandledRejectionHandler = async (reason, promise) => {
                const errorMessage = reason instanceof Error
                    ? reason.message
                    : typeof reason === "string"
                        ? reason
                        : "Unhandled promise rejection";
                const errorStack = reason instanceof Error ? reason.stack : null;
                let errorEvent = buildErrorEventWithContext({
                    error_message: errorMessage,
                    error_type: "UnhandledRejection",
                    error_stack_trace: errorStack,
                    error_severity: "critical",
                    is_user_facing: false,
                    is_recoverable: false,
                    execution_start_time: startupTime,
                    error_metadata: {
                        promise: String(promise),
                    },
                });
                // Enrich with user context if available
                try {
                    const userContext = await getUserContext;
                    errorEvent = mergeUserContext(errorEvent, userContext);
                }
                catch {
                    // Continue with original event if user context fails
                }
                await emitWithTimeout(transport, ERROR_OCCURRED_EVENT_SUBJECT, errorEvent);
            };
            registeredListeners.unhandledRejection = unhandledRejectionHandler;
            globalThis.process.on("unhandledRejection", unhandledRejectionHandler);
            // Handle termination signals
            const handleSignal = async (signal) => {
                shutdownStartTime = Date.now();
                const uptime = Date.now() - startupTime;
                let signalEvent = buildApplicationLifecycleEvent({
                    lifecycle_event_type: "signal_termination",
                    signal_name: signal,
                    uptime_ms: uptime,
                    is_graceful_shutdown: true,
                });
                // Enrich with user context if available
                try {
                    const userContext = await getUserContext;
                    signalEvent = mergeUserContext(signalEvent, userContext);
                }
                catch {
                    // Continue with original event if user context fails
                }
                await emitWithTimeout(transport, APPLICATION_LIFECYCLE_EVENT_SUBJECT, signalEvent);
                // Exit with appropriate code (128 + signal number)
                const exitCode = signal === "SIGINT" ? 130 : 143;
                globalThis.process.exit(exitCode);
            };
            // Register signal handlers
            const sigintHandler = () => handleSignal("SIGINT");
            const sigtermHandler = () => handleSignal("SIGTERM");
            registeredListeners.SIGINT = sigintHandler;
            registeredListeners.SIGTERM = sigtermHandler;
            globalThis.process.on("SIGINT", sigintHandler);
            globalThis.process.on("SIGTERM", sigtermHandler);
        }
    }
    const close = async (exitCode) => {
        if (closed)
            return;
        closed = true;
        await flush();
        const uptime = Date.now() - startupTime;
        const shutdownDuration = shutdownStartTime
            ? Date.now() - shutdownStartTime
            : null;
        let exitEvent = buildApplicationLifecycleEvent({
            lifecycle_event_type: "exit",
            exit_code: exitCode ?? 0,
            uptime_ms: uptime,
            is_graceful_shutdown: (exitCode ?? 0) === 0,
            shutdown_duration_ms: shutdownDuration,
        });
        try {
            const userContext = await getUserContext;
            exitEvent = mergeUserContext(exitEvent, userContext);
        }
        catch {
            // Continue with original event if user context fails
        }
        await emitWithTimeout(transport, APPLICATION_LIFECYCLE_EVENT_SUBJECT, exitEvent);
        removeExistingListeners();
    };
    return {
        context: {
            eventEmission: {
                transport,
                config,
                emit: (subject, event) => {
                    trackEmission(silentEmit(transport, subject, event, getUserContext));
                },
                createBaseEvent: createBaseEventHelper,
                emitMethodCalled: (data) => {
                    const event = {
                        ...buildMethodCalledEvent(data),
                        call_context: config.callContext ?? "sdk",
                    };
                    trackEmission(silentEmit(transport, METHOD_CALLED_EVENT_SUBJECT, event, getUserContext));
                },
                flush,
                close,
            },
        },
    };
});
export { buildApplicationLifecycleEvent, buildErrorEventWithContext, buildErrorEvent, createBaseEvent, buildMethodCalledEvent, } from "./builders";
export * from "./utils";
