import { DEBUG_CONFIG, isProd } from '@devvit/shared-types/debugConfig.js';
import { Hostname, parentHostname } from '@devvit/shared-types/HostnameUtil.js';
import { isHeadless, isPlatformNode } from '@devvit/shared-types/PlatformUtil.js';
import { StringUtil } from '@devvit/shared-types/StringUtil.js';
import { v4 as UUIDv4 } from 'uuid';
import { envelopeMethodToName } from '../../common/envelope/EnvelopeUtil.js';
// Name is shared across a thread. Some tests start multiple runtimes or actors
// in the same thread, so it may not be unique per actor.
const NAME = (typeof self !== 'undefined' && self.name) || globalThis.location?.hostname || UUIDv4();
const LogLevel = Object.freeze({
    /**
     * Verbose logs are useful for tracing but usually a hindrance for others not
     * working where the log is placed. Keep our logs clean and add with care.
     *
     * These logs are disabled by default but can be enabled.
     */
    VERBOSE: 0,
    /**
     * Informational logs should be used to mark significant non-erroneous events.
     * Little information is significant.
     *
     * These logs are enabled by default in development builds but disabled in
     * production.
     */
    INFO: 1,
    /**
     * Debug logs should never be committed.
     *
     * These logs are always enabled in development builds but disabled in
     * production.
     */
    DEBUG: 2,
    /**
     * Warnings should be reported as long as they do not contain sensitive data.
     *
     * These logs are always enabled.
     */
    WARN: 3,
    /**
     * Errors should always be reported as long as they do not contain sensitive
     * data.
     *
     * These logs are always enabled.
     */
    ERROR: 4,
    SILENT: 10,
});
// to-do: isProd ? LogLevel.WARN : LogLevel.INFO;
const LEVEL = LogLevel.VERBOSE;
const LEVEL_PREFIX = Object.freeze({
    [LogLevel.VERBOSE]: 'V',
    [LogLevel.INFO]: 'I',
    [LogLevel.DEBUG]: 'D',
    [LogLevel.WARN]: 'W',
    [LogLevel.ERROR]: 'E',
    [LogLevel.SILENT]: '',
});
/**
 * The base configuration used for unspecified options in calls to
 * `Log.envelope()`. The intent for the default is to be as minimal as possible
 * because there are many envelopes.
 */
const defaultEnvelopeLoggerConfig = {
    // When debugging local changes, abbreviating lengthy strings is helpful. For
    // unexpected errors in non-debug, it may be harmful.
    abbreviateStrings: !isProd,
    showClientAddress: true,
    showServerAddress: true,
    // We expect browser devtools to be able to compact messages nicely
    // even if they're huge. That's not the case on the server and, in the
    // browser, including messages impedes log aggregation so let's not display
    // them by default there.
    showAllRequestMessages: false,
    showAllResponseMessages: false,
    showAllErrorMessages: true,
    showAllSuccessMessages: false,
    showMetadata: LEVEL <= LogLevel.VERBOSE,
    // If we're in a browser and _not_ in a worker, then we have a much nicer
    // console.log() which allows clicking through the various fields in the
    // DevTools console. Everybody else must stringify.
    stringifyObjects: isHeadless(),
    point: '',
};
function prefix(level, config = {}) {
    config = { ...defaultEnvelopeLoggerConfig, ...config };
    return `${LEVEL_PREFIX[level]}[${config.abbreviateStrings ? abbreviateName() : NAME}]`;
}
/** No buffer console logger for Node.js. */
export const nodeDebugConsole = Object.freeze({
    async debug(...args) {
        console.debug(...args);
        await flush(process.stdout);
    },
    async info(...args) {
        console.info(...args);
        await flush(process.stdout);
    },
    async log(...args) {
        console.log(...args);
        await flush(process.stdout);
    },
    async warn(...args) {
        console.warn(...args);
        await flush(process.stderr);
    },
    async error(...args) {
        console.error(...args);
        await flush(process.stderr);
    },
});
export const voidConsole = Object.freeze({
    debug() { },
    info() { },
    log() { },
    warn() { },
    error() { },
});
// Don't enforce awaiting logs. It's optional and unnecessary in many contexts.
let logger = DEBUG_CONFIG.flushNodeLogs && isPlatformNode()
    ? nodeDebugConsole
    : console;
// Console wrappers are bound rather than wrapped to preserve correct callstack
// information.
export const Log = {
    // These Log APIs are logical. The verbose implementation uses console.debug()
    // because it corresponds to the verbose logging level in Chromium. Similarly,
    // debug uses log as a default.
    verbose: LEVEL > LogLevel.VERBOSE
        ? () => { }
        : (...args) => logger.debug(prefix(LogLevel.VERBOSE), ...args),
    info: LEVEL > LogLevel.INFO
        ? () => { }
        : (...args) => logger.info(prefix(LogLevel.INFO), ...args),
    debug: LEVEL > LogLevel.DEBUG
        ? () => { }
        : (...args) => logger.log(prefix(LogLevel.DEBUG), ...args),
    warn: LEVEL > LogLevel.WARN
        ? () => { }
        : (...args) => logger.warn(prefix(LogLevel.WARN), ...args),
    error: LEVEL > LogLevel.ERROR
        ? () => { }
        : (...args) => logger.error(prefix(LogLevel.ERROR), ...args),
    /**
     * Update the base envelope config for all invocations of `Log.envelope()`
     */
    setDefaultConfig(config) {
        // Update the base config in-place so that existing references stay valid
        Object.assign(defaultEnvelopeLoggerConfig, config);
    },
    /**
     * Dump the contents of an envelope to the debug logs.
     *
     * Accepts a logger config partial, which will be mixed with the default logger config
     */
    envelope(envelope, config = {}) {
        config = { ...defaultEnvelopeLoggerConfig, ...config };
        if (isProd)
            return;
        const errSym = envelope.success ? '' : '🗙';
        let streamId = envelope.streamId;
        if (config.abbreviateStrings)
            streamId = abbreviateUUID(streamId);
        let msg = '';
        if ((envelope.request && config.showAllRequestMessages) ||
            (!envelope.request && config.showAllResponseMessages) ||
            (envelope.success && config.showAllSuccessMessages) ||
            (!envelope.success && config.showAllErrorMessages)) {
            msg = envelope.message || envelope;
        }
        if (msg !== '' && config.stringifyObjects) {
            msg = JSON.stringify(msg);
            if (config.abbreviateStrings)
                msg = `[msg (${msg.length}) ${StringUtil.ellipsize(msg, 80)}]`;
        }
        let path;
        if (envelope.request)
            path = [envelope.src, '→', envelope.dst];
        else
            path = [envelope.dst, '←', envelope.src];
        if (!config.showClientAddress)
            path[0] = 'Client';
        if (!config.showServerAddress)
            path[2] = 'Server';
        if (config.abbreviateStrings) {
            path[0] = abbreviateClientHostname(path[0]);
            // Don't abbreviate server which may be like bootstrap.no-beans.node or
            // no-beans.node, or node.
        }
        const method = config.abbreviateStrings ? envelopeMethodToName(envelope) : envelope.method;
        const requestResponse = `${path.join(' ')}@${streamId}/${method}`;
        let metadata = envelope.metadata;
        if (config.stringifyObjects)
            metadata = stringifyMetadata(metadata);
        const logs = [
            `${errSym}${config.point ? `${config.point} ` : ''}${requestResponse}`,
        ];
        if (msg)
            logs.push(msg);
        if (config.showMetadata)
            logs.push(metadata);
        Log.debug(...logs);
    },
    setLogger(console) {
        logger = console;
    },
};
/** @internal */
export function abbreviateClientHostname(hostname) {
    // Could be an actor client like `logger-123-456.no-beans.local`,
    // `runtime-123-456.supervisor.system.node`, or `no-beans-123-456.local`.
    const uuid = extractHostnameUUID(Hostname.getChild(hostname))[0];
    if (uuid == null)
        return hostname;
    const uuidAbbreviated = abbreviateUUID(uuid);
    const dispatcher = Hostname.getChild(parentHostname(hostname));
    return `${uuidAbbreviated}.${dispatcher}`;
}
function abbreviateName() {
    // Could be an actor like `no-beans.local` or `supervisor.system.node`. Could
    // be the runtime like 'localhost', 'node', or '123-456'.
    if (isUUID(NAME))
        return abbreviateUUID(NAME);
    return NAME;
}
/** @internal */
export function abbreviateUUID(uuid) {
    return uuid.split('-')[0];
}
/** @internal */
export function extractHostnameUUID(hostname) {
    return hostname.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i) ?? [];
}
/** @internal */
export function isUUID(str) {
    return /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.test(str);
}
function stringifyMetadata(metadata) {
    const entries = Object.entries(metadata ?? {});
    if (entries.length === 0)
        return '';
    const entriesStr = entries
        .map(([header, { values }]) => `${header}: "${values.join(', ')}"`)
        .join(', ');
    return `[meta ${entriesStr}]`;
}
/**
 * @internal
 *
 * There is no fflush() for Node.js. Write an empty string to the stream and
 * wait for that specific string to be written to guarantee that all prior
 * writes have been written. Other runtimes can `await Logger.Log()`.
 */
export async function flush(stream) {
    await new Promise((resolve) => stream.write('', resolve));
}
