import { findUncloneable } from '@devvit/shared-types/findUncloneable.js';
import { Header } from '@devvit/shared-types/Header.js';
import { NonNull } from '@devvit/shared-types/NonNull.js';
import { StringUtil } from '@devvit/shared-types/StringUtil.js';
import { Buffer } from '../../lib/Buffer.js';
/**
 * Debug-only assertion for identifying envelopes that would fail a
 * [structured clone] in Worker.postMessage(). The dispatcher does not use
 * [transferrable objects] for [postMessage() transfers] any yet.
 *
 * A failing envelope is likely due to an `any` typed Envelope.message (eg, due
 * to compounding interactions with JSON.parse(), Object.values(), `as any`,
 * etc).
 *
 * postMessage() failures usually look like:
 *
 * ```
 * DOMException [DataCloneError]: encode(message, writer = _m0.Writer.create()) {
 *     if (message.severity !== 0) {
 *       writer.uint32...<omitted>... } could not be cloned.
 *   at Worker.postMessage (node:internal/worker:341:5)
 * ```
 *
 * [structured clone]: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone
 * [transferrable objects]: https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
 * [postMessage() transfers]: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage#parameters
 */
export function assertEnvelopePostable(envelope) {
    const [path, val] = findUncloneable('envelope', envelope);
    if (path != null)
        throw Error(`Envelope is not postable. See value at ${path}: ${String(val)}. ` +
            `This is likely an error in your messaging code or ` +
            `\`assertEnvelopePostable()\`. Only Protobuf generated messages and ` +
            `native Errors can be sent. Please verify you are using ` +
            `\`fromPartial()\` to generate request / response messages and only ` +
            `throwing Error itself, not subclasses.`);
}
/**
 * Strip any "Error: " prefix. Errors do not have a message type in the
 * enveloping system and are frequently encoded as strings starting with
 * "Error: ". This can lead to lengthy concatenations like
 * "Error: Error: Error: Error: source of the error."
 */
function stripErrorPrefix(message) {
    return message.replaceAll(/^(Error: )*/g, '');
}
function isErrorish(err) {
    return !!err && typeof err === 'object' && 'name' in err && ('message' in err || 'stack' in err);
}
const NULLISH_ERROR_MESSAGE = '<nullish>';
export function envelopeAsError(envelope) {
    if (envelope.message == null) {
        return new Error(NULLISH_ERROR_MESSAGE);
    }
    if (envelope.message instanceof Error) {
        envelope.message.message = stripErrorPrefix(envelope.message.message);
        return envelope.message;
    }
    if (typeof envelope.message === 'string') {
        return new Error(stripErrorPrefix(envelope.message));
    }
    if (isErrorish(envelope.message)) {
        return new Error(stripErrorPrefix(envelope.message.message));
    }
    return new Error(envelope.message.toString());
}
export function extractMetadataTraceID(metadata) {
    return metadata?.[Header.TraceID]?.values[0];
}
export function envelopeMethod(def, name) {
    const method = def.methods[name];
    return `/${def.fullName}/${method?.name ?? name}`;
}
/**
 * Return the method's `name` from an Envelope's `method`
 * @see {@link MethodDefinition.name}
 * @see {@link Envelope.method}
 * @param envelope
 */
export function envelopeMethodToName(envelope) {
    const name = NonNull(envelope.method.split('/').at(-1));
    return name[0].toLocaleLowerCase() + name.slice(1);
}
/**
 * Return the service's `fullName` from an Envelope's `method`
 * @see {@link Definition.fullName}
 * @see {@link Envelope.method}
 * @param envelope
 */
export function envelopeMethodToServiceName(envelope) {
    return NonNull(envelope.method.split('/').at(1));
}
export function requestMessageToJSON(message, method) {
    if (message == null)
        return message;
    return method.requestType.toJSON(message);
}
export function responseMessageToJSON(message, method) {
    if (!message)
        return undefined;
    return method.responseType.toJSON(message);
}
export function requestMessageFromJSON(envelope, method) {
    // Type unknown. Message cannot be unpacked.
    if (!envelope.success || !envelope.message)
        return envelope.message;
    return method.requestType.fromJSON(envelope.message);
}
export function responseMessageFromJSON(envelope, method) {
    // Type unknown. Message cannot be unpacked.
    if (!envelope.success || !envelope.message)
        return envelope.message;
    return method.responseType.fromJSON(envelope.message);
}
export function requestMessageToBinary(message, method) {
    if (message == null)
        return undefined;
    return Buffer.from(method.requestType.encode(message).finish()).toString('base64');
}
export function requestMessageFromBinary(envelope, method) {
    // Type unknown. Message cannot be unpacked.
    if (!envelope.success || envelope.message == null)
        return envelope.message;
    const bytes = Buffer.from(envelope.message, 'base64');
    return method.requestType.decode(bytes);
}
export function responseMessageToBinary(message, method) {
    if (message == null)
        return undefined;
    return Buffer.from(method.responseType.encode(message).finish()).toString('base64');
}
export function responseMessageFromBinary(envelope, method) {
    // Type unknown. Message cannot be unpacked.
    if (!envelope.success || envelope.message == null || typeof envelope.message !== 'string')
        return envelope.message;
    const bytes = Buffer.from(envelope.message, 'base64');
    return method.responseType.decode(bytes);
}
/**
 * Sends an envelope's message (Envelope.message) to a stream, usually a
 * subscriber.
 */
export function sendEnvelopeMessageToStream(envelope, stream) {
    // Stream may be going or it might not be.
    // message nil, complete true is a signal to close the stream
    if (envelope.success) {
        if (envelope.complete) {
            // Some complete envelopes are just close signals without a message inside. Don't call next on them.
            // Complete envelopes might also contain the last message.
            // 1---2-----------------3-------------<EOF>. I can always send an EOF (success true, message nil)
            // might not know when i am going to be terminated and have previously sent what was to be my last message.
            if (envelope.message) {
                stream.next(envelope.message);
            }
            stream.complete();
        }
        else {
            stream.next(envelope.message);
        }
    }
    else {
        stream.error(envelope.message);
        stream.complete();
    }
}
export var EnvelopeUtil;
(function (EnvelopeUtil) {
    // to-do: replace all the custom production Envelope error logging in
    // CentralDispatcher, EnvelopeServer, and EnvelopeClient with this.
    /**
     * Format basic envelope identification for error messages. Includes source,
     * destination, truncated stream ID, and method-ish.
     *
     * The output of this function may be used in production logs. Do not print
     * sensitive information.
     *
     * This is not a stable API.
     */
    function toLogString(envelope) {
        // Draw route with stable endpoints.
        const path = envelope.request
            ? `${envelope.src} → ${envelope.dst}`
            : `${envelope.dst} ← ${envelope.src}`;
        const streamIDOctet = envelope.streamId.split('-')[0];
        const method = StringUtil.capitalize(envelopeMethodToName(envelope));
        return `${path}@${streamIDOctet}/${method}`;
    }
    EnvelopeUtil.toLogString = toLogString;
})(EnvelopeUtil || (EnvelopeUtil = {}));
