import { decryptWithKey, encrypt } from "./encrypt.js";
import { isScope } from "./scope.js";
import { isSecret, Secret } from "./secret.js";
// zero-dependency type guard for ArkType
function isType(value) {
    return (value &&
        typeof value === "object" &&
        typeof value.toJsonSchema === "function");
}
export async function serialize(scope, value, options) {
    if (options?.transform) {
        value = options.transform(value);
    }
    if (Array.isArray(value)) {
        return Promise.all(value.map((value) => serialize(scope, value, options)));
    }
    else if (isSecret(value)) {
        if (!scope.password) {
            throw new Error("Cannot serialize secret without password, did you forget to set password when initializing your alchemy app?\n" +
                "See: https://alchemy.run/concepts/secret/#encryption-password");
        }
        return {
            "@secret": options?.encrypt !== false
                ? typeof value.unencrypted === "string"
                    ? await encrypt(value.unencrypted, scope.password)
                    : {
                        data: await encrypt(JSON.stringify(value.unencrypted), scope.password),
                    }
                : value.unencrypted,
        };
    }
    else if (isType(value)) {
        return {
            "@schema": value.toJSON(),
        };
    }
    else if (value instanceof Date) {
        return {
            "@date": value.toISOString(),
        };
    }
    else if (typeof value === "symbol") {
        assertNotUniqueSymbol(value);
        return {
            "@symbol": value.toString(),
        };
    }
    else if (isScope(value)) {
        return {
            "@scope": null,
        };
    }
    else if (isImportMeta(value)) {
        // ImportMeta serialized as {}, so we are mapping it
        // TODO(sam):
        return Object.fromEntries(Object.keys(Object.getPrototypeOf(value))
            // exlcude import.meta.env
            .filter((prop) => prop === "env")
            .map((prop) => [prop, value[prop]]));
    }
    else if (isBinary(value)) {
        return {
            "@buffer": await serializeBinary(value),
        };
    }
    else if (value && typeof value === "object") {
        for (const symbol of Object.getOwnPropertySymbols(value)) {
            assertNotUniqueSymbol(symbol);
        }
        return Object.fromEntries(await Promise.all([...Object.getOwnPropertySymbols(value), ...Object.keys(value)].map(async (key) => [
            key.toString(),
            await serialize(scope, value[key], options),
        ])));
    }
    else if (typeof value === "function") {
        // can't serialize functions
        return undefined;
    }
    return value;
}
function isImportMeta(value) {
    return (value &&
        typeof value === "object" &&
        typeof value.dirname === "string" &&
        typeof value.filename === "string" &&
        typeof value.url === "string");
}
export async function deserialize(scope, value, options) {
    const replacement = options?.transform?.(value);
    if (replacement) {
        return replacement.value;
    }
    if (Array.isArray(value)) {
        return await Promise.all(value.map(async (item) => await deserialize(scope, item, options)));
    }
    if (value && typeof value === "object") {
        if (value["@secret"]) {
            // If eraseSecrets is enabled, return undefined instead of decrypting
            if (scope.eraseSecrets) {
                return undefined;
            }
            if (!scope.password) {
                throw new Error("Cannot deserialize secret without password, did you forget to set password when initializing your alchemy app?\n" +
                    "See: https://alchemy.run/concepts/secret/#encryption-password");
            }
            if (typeof value["@secret"] === "object" && "data" in value["@secret"]) {
                return new Secret(JSON.parse(await decryptWithKey(value["@secret"].data, scope.password)));
            }
            return new Secret(await decryptWithKey(value["@secret"], scope.password));
        }
        else if ("@schema" in value) {
            return value["@schema"];
        }
        else if ("@date" in value) {
            return new Date(value["@date"]);
        }
        else if ("@symbol" in value) {
            return parseSymbol(value["@symbol"]);
        }
        else if ("@scope" in value) {
            return scope;
        }
        else if ("@buffer" in value) {
            return Buffer.from(value["@buffer"], "base64");
        }
        return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, value]) => [
            parseSymbol(key) ?? key,
            await deserialize(scope, value, options),
        ])));
    }
    return value;
}
const symbolPattern = /^Symbol\((.*)\)$/;
function parseSymbol(value) {
    const match = value.match(symbolPattern);
    if (!match) {
        return undefined;
    }
    return Symbol.for(match[1]);
}
function assertNotUniqueSymbol(symbol) {
    if (symbol.description === undefined ||
        symbol !== Symbol.for(symbol.description)) {
        throw new Error(`Cannot serialize unique symbol: ${symbol.description}`);
    }
}
export const isBinary = (value) => {
    return (value instanceof Buffer ||
        value instanceof Uint8Array ||
        value instanceof ArrayBuffer ||
        value instanceof Blob ||
        value instanceof ReadableStream ||
        ArrayBuffer.isView(value));
};
export const serializeBinary = async (value) => {
    return value instanceof Buffer
        ? value.toString("base64")
        : value instanceof Uint8Array
            ? value.toString("base64")
            : value instanceof ArrayBuffer
                ? serializeBinary(Buffer.from(value))
                : value instanceof Blob
                    ? serializeBinary(await value.arrayBuffer())
                    : value instanceof ReadableStream
                        ? serializeBinary(await streamToBuffer(value))
                        : value;
};
export async function streamToBuffer(stream) {
    const reader = stream.getReader();
    const chunks = [];
    while (true) {
        const { done, value } = await reader.read();
        if (done)
            break;
        chunks.push(value);
    }
    return Buffer.concat(chunks);
}
//# sourceMappingURL=serde.js.map