/*
 * Copyright (c) 2025, Salesforce, Inc.
 * SPDX-License-Identifier: Apache-2
 * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
 */
/**
 * Stateless HTTP client for the B2C Commerce Script Debugger API (SDAPI 2.0).
 *
 * All methods use global `fetch()` with Basic auth and the required
 * `x-dw-client-id` header. The client is stateless — callers are responsible
 * for session lifecycle (create/delete client, polling, keepalive).
 *
 * @module operations/debug/sdapi-client
 */
import { wrapNetworkError } from '../../errors/network-error.js';
import { getLogger } from '../../logging/logger.js';
/**
 * Converts a Headers object to a plain object for trace logging.
 */
function headersToObject(headers) {
    const result = {};
    headers.forEach((value, key) => {
        result[key] = value;
    });
    return result;
}
/**
 * Error thrown when the SDAPI returns a fault response.
 */
export class SdapiError extends Error {
    fault;
    status;
    constructor(fault, status) {
        super(`${fault.type}: ${fault.message}`);
        this.name = 'SdapiError';
        this.fault = fault;
        this.status = status;
    }
}
/**
 * Creates a new SDAPI client.
 */
export function createSdapiClient(config) {
    return new SdapiClient(config);
}
export class SdapiClient {
    baseUrl;
    headers;
    logger = getLogger();
    /**
     * Cookies (name -> value) returned by the SDAPI via Set-Cookie, replayed on
     * subsequent requests. The debugger establishes a session (e.g. `dwsid`) on
     * the first request that must be honored for the lifetime of this client.
     *
     * Replaying these cookies is required for server affinity: in multi-app
     * server instances/environments the `dwsid` pins the session to the app
     * server that holds the debugger state, so without it subsequent requests may
     * be routed to a different app server with no knowledge of the session.
     */
    cookies = new Map();
    constructor(config) {
        this.baseUrl = `https://${config.hostname}/s/-/dw/debugger/v2_0`;
        const credentials = btoa(`${config.username}:${config.password}`);
        this.headers = {
            Authorization: `Basic ${credentials}`,
            'x-dw-client-id': config.clientId ?? 'b2c-cli',
            'Content-Type': 'application/json',
        };
    }
    // -----------------------------------------------------------------------
    // Client resource (debugger lifecycle)
    // -----------------------------------------------------------------------
    async createClient() {
        await this.request('POST', '/client', { expect204: true });
    }
    async deleteClient() {
        await this.request('DELETE', '/client', { expect204: true });
    }
    // -----------------------------------------------------------------------
    // Breakpoints
    // -----------------------------------------------------------------------
    /**
     * Retrieves all currently set breakpoints for the debugger session.
     * @returns Promise resolving to array of breakpoints, or empty array if none are set.
     */
    async getBreakpoints() {
        const data = await this.request('GET', '/breakpoints');
        return data.breakpoints ?? [];
    }
    /**
     * Sets or replaces breakpoints for the debugger.
     * @param breakpoints Array of breakpoint definitions to set.
     * @returns Promise resolving to array of confirmed breakpoints from the server.
     */
    async setBreakpoints(breakpoints) {
        const data = await this.request('POST', '/breakpoints', {
            body: { breakpoints },
        });
        return data.breakpoints ?? [];
    }
    async deleteBreakpoints() {
        await this.request('DELETE', '/breakpoints', { expect204: true });
    }
    async deleteBreakpoint(id) {
        await this.request('DELETE', `/breakpoints/${id}`, { expect204: true });
    }
    // -----------------------------------------------------------------------
    // Threads
    // -----------------------------------------------------------------------
    /**
     * Gets all active script threads.
     * @returns Promise resolving to array of script threads, or empty array if none are active.
     */
    async getThreads() {
        const data = await this.request('GET', '/threads');
        return data.script_threads ?? [];
    }
    /**
     * Retrieves a specific thread by ID.
     * @param threadId The thread identifier.
     * @returns Promise resolving to the thread object with its current state.
     */
    async getThread(threadId) {
        return this.request('GET', `/threads/${threadId}`);
    }
    async resetThreads() {
        await this.request('POST', '/threads/reset', { expect204: true });
    }
    // -----------------------------------------------------------------------
    // Execution control
    // -----------------------------------------------------------------------
    async resume(threadId) {
        return this.request('POST', `/threads/${threadId}/resume`);
    }
    async stepOver(threadId) {
        return this.request('POST', `/threads/${threadId}/over`);
    }
    async stepInto(threadId) {
        return this.request('POST', `/threads/${threadId}/into`);
    }
    async stepOut(threadId) {
        return this.request('POST', `/threads/${threadId}/out`);
    }
    async stopThread(threadId) {
        await this.request('DELETE', `/threads/${threadId}/stop`, { expect204: true });
    }
    // -----------------------------------------------------------------------
    // Variables & evaluation
    // -----------------------------------------------------------------------
    async getVariables(threadId, frameIndex) {
        return this.request('GET', `/threads/${threadId}/frames/${frameIndex}/variables`);
    }
    async getMembers(threadId, frameIndex, objectPath, start, count) {
        const params = new URLSearchParams();
        if (objectPath)
            params.set('object_path', objectPath);
        if (start !== undefined)
            params.set('start', String(start));
        if (count !== undefined)
            params.set('count', String(count));
        const qs = params.toString();
        const suffix = qs ? `?${qs}` : '';
        return this.request('GET', `/threads/${threadId}/frames/${frameIndex}/members${suffix}`);
    }
    /**
     * Evaluates a JavaScript expression in a frame context.
     * @param threadId The thread ID of the halted thread.
     * @param frameIndex The stack frame index (0 = topmost frame).
     * @param expr The JavaScript expression to evaluate.
     * @returns Promise resolving to the evaluation result.
     */
    async evaluate(threadId, frameIndex, expr) {
        const params = new URLSearchParams({ expr });
        return this.request('GET', `/threads/${threadId}/frames/${frameIndex}/eval?${params.toString()}`);
    }
    // -----------------------------------------------------------------------
    // Internal
    // -----------------------------------------------------------------------
    async request(method, path, options) {
        const url = `${this.baseUrl}${path}`;
        const headers = { ...this.headers, ...this.cookieHeader() };
        this.logger.trace({ method, url, headers, body: options?.body }, `[SDAPI REQ] ${method} ${url}`);
        let response;
        try {
            response = await fetch(url, {
                method,
                headers,
                body: options?.body ? JSON.stringify(options.body) : undefined,
            });
        }
        catch (err) {
            const host = new URL(url).host;
            throw wrapNetworkError(err, { operation: 'Script Debugger API request', host });
        }
        this.storeCookies(response.headers);
        this.logger.trace({ method, url, status: response.status, headers: headersToObject(response.headers) }, `[SDAPI RESP] ${method} ${url} ${response.status}`);
        if (options?.expect204) {
            if (!response.ok) {
                await this.throwSdapiError(response);
            }
            return undefined;
        }
        if (!response.ok) {
            await this.throwSdapiError(response);
        }
        return (await response.json());
    }
    async throwSdapiError(response) {
        let fault;
        try {
            const body = (await response.json());
            // SDAPI may return fault at top level or nested under a "fault" property
            const faultData = body.fault ?? body;
            fault = {
                _v: String(faultData._v ?? body._v ?? '2.0'),
                type: String(faultData.type ?? 'UnknownError'),
                message: String(faultData.message ?? `HTTP ${response.status} ${response.statusText}`),
            };
        }
        catch {
            fault = { _v: '2.0', type: 'UnknownError', message: `HTTP ${response.status} ${response.statusText}` };
        }
        throw new SdapiError(fault, response.status);
    }
    /**
     * Returns the value of a stored session cookie, or `undefined` if it has not
     * been set yet. The session cookie (`dwsid`) pins requests to the app server
     * holding the debugger session — callers may need it to route external
     * requests (e.g. a storefront browser) to the same app server.
     */
    getCookie(name) {
        return this.cookies.get(name);
    }
    /**
     * Builds the `Cookie` request header from stored session cookies, or an empty
     * object if none have been set yet.
     */
    cookieHeader() {
        if (this.cookies.size === 0) {
            return {};
        }
        const cookie = Array.from(this.cookies, ([name, value]) => `${name}=${value}`).join('; ');
        return { Cookie: cookie };
    }
    /**
     * Parses `Set-Cookie` headers from a response and stores them for replay on
     * subsequent requests (only the name=value pair is retained, attributes are
     * ignored). Uses `Headers.getSetCookie()` so multiple cookies are not
     * collapsed into a single comma-joined value.
     */
    storeCookies(headers) {
        const setCookies = typeof headers.getSetCookie === 'function' ? headers.getSetCookie() : [];
        for (const setCookie of setCookies) {
            const pair = setCookie.split(';', 1)[0]?.trim();
            if (!pair) {
                continue;
            }
            const eq = pair.indexOf('=');
            if (eq <= 0) {
                continue;
            }
            const name = pair.slice(0, eq).trim();
            const value = pair.slice(eq + 1).trim();
            this.cookies.set(name, value);
        }
    }
}
//# sourceMappingURL=sdapi-client.js.map