import { ServiceException, toServiceException, fromResponse } from '@agentuity/core';
import { appendFileSync } from 'node:fs';
/**
 * Builds standard headers for Agentuity service clients.
 *
 * This helper creates the header object needed by `createServerFetchAdapter`,
 * handling authentication and multi-tenant scoping consistently across all clients.
 *
 * @param options - Options containing apiKey and/or orgId
 * @returns Headers object ready to pass to createServerFetchAdapter
 *
 * @example
 * ```typescript
 * import { buildClientHeaders, createServerFetchAdapter } from '@agentuity/adapter';
 *
 * const headers = buildClientHeaders({ apiKey: 'sk_xxx', orgId: 'org_xxx' });
 * const adapter = createServerFetchAdapter({ headers }, logger);
 * ```
 */
export function buildClientHeaders(options) {
    const headers = options.apiKey
        ? {
            Authorization: `Bearer ${options.apiKey}`,
            'Content-Type': 'application/json',
        }
        : { 'Content-Type': 'application/json' };
    if (options.orgId) {
        headers['x-agentuity-orgid'] = options.orgId;
    }
    return headers;
}
// =============================================================================
// Redaction Utilities
// =============================================================================
/**
 * Headers that contain sensitive information and should be redacted in debug logs.
 * Includes authentication tokens, API keys, cookies, and proxy credentials.
 */
const sensitiveHeaders = new Set([
    'authorization',
    'x-api-key',
    'cookie',
    'set-cookie',
    'proxy-authorization',
]);
/**
 * Redacts the middle of a string while keeping a prefix and suffix visible.
 * Ensures that if the string is too short, everything is redacted.
 *
 * @param input The string to redact
 * @param prefix Number of chars to keep at the start
 * @param suffix Number of chars to keep at the end
 * @param mask Character used for redaction
 */
export function redact(input, prefix = 4, suffix = 4, mask = '*') {
    if (!input)
        return '';
    // If revealing prefix+suffix would leak too much, fully mask
    if (input.length <= prefix + suffix) {
        return mask.repeat(input.length);
    }
    const start = input.slice(0, prefix);
    const end = input.slice(-suffix);
    const hiddenLength = input.length - prefix - suffix;
    return start + mask.repeat(hiddenLength) + end;
}
/**
 * Format a sensitive header value, preserving Bearer prefix if present.
 */
function redactSensitiveHeader(key, value) {
    const _k = key.toLowerCase();
    // Handle Bearer tokens in authorization and proxy-authorization headers
    if ((_k === 'authorization' || _k === 'proxy-authorization') && value.startsWith('Bearer ')) {
        return `Bearer ${redact(value.substring(7))}`;
    }
    return redact(value);
}
/**
 * Format headers as a readable string for debug logging.
 * Sensitive headers (auth tokens, cookies, API keys) are redacted.
 */
function formatHeaders(headers) {
    const entries = [];
    if (headers instanceof Headers) {
        headers.forEach((value, key) => {
            const _k = key.toLowerCase();
            if (sensitiveHeaders.has(_k)) {
                entries.push(`  ${key}: ${redactSensitiveHeader(key, value)}`);
            }
            else {
                entries.push(`  ${key}: ${value}`);
            }
        });
    }
    else {
        for (const [key, value] of Object.entries(headers)) {
            const _k = key.toLowerCase();
            if (sensitiveHeaders.has(_k)) {
                entries.push(`  ${key}: ${redactSensitiveHeader(key, value)}`);
            }
            else {
                entries.push(`  ${key}: ${value}`);
            }
        }
    }
    return entries.join('\n');
}
const redactHeaders = (kv) => {
    const values = [];
    for (const k of Object.keys(kv)) {
        const _k = k.toLowerCase();
        const v = kv[k];
        if (v === undefined) {
            continue;
        }
        if (sensitiveHeaders.has(_k)) {
            values.push(`${_k}=${redactSensitiveHeader(k, v)}`);
        }
        else {
            values.push(`${_k}=${v}`);
        }
    }
    return '[' + values.join(',') + ']';
};
// =============================================================================
// Debug Logging
// =============================================================================
/**
 * Check if API debug logging is enabled and return the output destination.
 * Returns:
 * - 'console' if CI=1/true or AGENTUITY_API_DEBUG=1/true
 * - file path string if AGENTUITY_API_DEBUG is set to a path
 * - null if debug logging is disabled (including AGENTUITY_API_DEBUG=0/false)
 */
function getDebugOutput() {
    const apiDebug = process.env.AGENTUITY_API_DEBUG?.trim();
    if (apiDebug) {
        const normalized = apiDebug.toLowerCase();
        // Check if explicitly disabled
        if (normalized === '0' || normalized === 'false') {
            return null;
        }
        // Check if it's a truthy value (console output)
        if (normalized === '1' || normalized === 'true') {
            return 'console';
        }
        // Treat any other non-empty value as a file path
        return apiDebug;
    }
    // Fall back to CI environment check
    if (process.env.CI === '1' || process.env.CI === 'true') {
        return 'console';
    }
    return null;
}
/**
 * Format request body for CI debug logging
 */
function formatRequestBody(body) {
    if (body === undefined || body === null) {
        return '[no body]';
    }
    if (typeof body === 'string') {
        return body;
    }
    if (body instanceof Uint8Array) {
        return `[binary data: ${body.length} bytes]`;
    }
    if (body instanceof ArrayBuffer) {
        return `[binary data: ${body.byteLength} bytes]`;
    }
    if (body instanceof ReadableStream) {
        return '[stream]';
    }
    return String(body);
}
/**
 * Log detailed debug information when API requests fail.
 * Output destination is determined by AGENTUITY_API_DEBUG or CI environment variables.
 */
function logAPIDebug(url, method, requestHeaders, requestBody, response, responseBody) {
    const output = getDebugOutput();
    if (!output) {
        return;
    }
    const separator = '='.repeat(60);
    const timestamp = new Date().toISOString();
    const lines = [
        '',
        separator,
        `API DEBUG: Request Failed [${timestamp}]`,
        separator,
        '',
        '>>> REQUEST',
        `URL: ${url}`,
        `Method: ${method}`,
        'Headers:',
        formatHeaders(requestHeaders),
        'Body:',
        `  ${formatRequestBody(requestBody)}`,
        '',
        '<<< RESPONSE',
        `Status: ${response.status} ${response.statusText}`,
        'Headers:',
        formatHeaders(response.headers),
        'Body:',
        `  ${responseBody || '[empty]'}`,
        '',
        separator,
        '',
    ];
    const content = lines.join('\n');
    if (output === 'console') {
        console.error(content);
    }
    else {
        // Append to file
        try {
            appendFileSync(output, content + '\n');
        }
        catch {
            // If file write fails, fall back to console.error
            console.error(`[API DEBUG] Failed to write to ${output}, falling back to console`);
            console.error(content);
        }
    }
}
// =============================================================================
// Server Fetch Adapter
// =============================================================================
class ServerFetchAdapter {
    #config;
    #logger;
    constructor(config, logger) {
        this.#config = config;
        this.#logger = logger;
    }
    /**
     * Build the final URL with query params appended.
     * This is extracted so both invoke() and _invoke() use the same URL.
     */
    #buildUrl(url) {
        if (this.#config.queryParams && Object.keys(this.#config.queryParams).length > 0) {
            const urlObj = new URL(url);
            for (const [key, value] of Object.entries(this.#config.queryParams)) {
                urlObj.searchParams.set(key, value);
            }
            return urlObj.toString();
        }
        return url;
    }
    async _invoke(url, options) {
        // URL already has query params appended by invoke() or direct caller
        const headers = {
            ...options.headers,
            ...this.#config.headers,
        };
        if (options.contentType) {
            headers['Content-Type'] = options.contentType;
        }
        else if (typeof options.body === 'string' ||
            options.body instanceof Uint8Array ||
            options.body instanceof ArrayBuffer) {
            headers['Content-Type'] = 'application/octet-stream';
        }
        // Ensure we request JSON responses for proper error handling
        if (!headers['Accept'] && !headers['accept']) {
            headers['Accept'] = 'application/json';
        }
        const method = options.method ?? 'POST';
        this.#logger.trace('sending %s to %s with headers: %s', method, url, redactHeaders(headers));
        const res = await fetch(url, {
            method,
            body: options.body,
            headers,
            signal: options.signal,
            ...(options.duplex ? { duplex: options.duplex } : {}),
        });
        if (res.ok) {
            switch (res.status) {
                case 100:
                case 101:
                case 102:
                case 204:
                case 304:
                    return {
                        ok: true,
                        data: undefined,
                        response: res,
                    };
                default:
                    break;
            }
            if (options?.binary) {
                return {
                    ok: true,
                    data: undefined,
                    response: res,
                };
            }
            const data = await fromResponse(res);
            return {
                ok: true,
                data,
                response: res,
            };
        }
        if (res.status === 404) {
            // Log debug info for 404 errors if debugging is enabled
            if (getDebugOutput()) {
                const responseBody = await res.clone().text();
                logAPIDebug(url, method, headers, options.body, res, responseBody);
            }
            return {
                ok: false,
                response: res,
            };
        }
        // Clone response to read body for debug logging before toServiceException consumes it
        const responseBody = getDebugOutput() ? await res.clone().text() : '';
        logAPIDebug(url, method, headers, options.body, res, responseBody);
        const err = await toServiceException(method, url, res);
        throw err;
    }
    async invoke(url, options = { method: 'POST' }) {
        // Build final URL with query params BEFORE hooks, so hooks receive the actual URL
        const finalUrl = this.#buildUrl(url);
        if (this.#config.onBefore) {
            let result = undefined;
            let err = undefined;
            await this.#config.onBefore(finalUrl, options, async () => {
                try {
                    result = await this._invoke(finalUrl, options);
                    if (this.#config.onAfter) {
                        await this.#config.onAfter(finalUrl, options, result);
                    }
                }
                catch (ex) {
                    err = ex;
                    if (this.#config.onAfter && err instanceof ServiceException) {
                        await this.#config.onAfter(finalUrl, options, {
                            ok: false,
                            response: new Response(err.message, {
                                status: err.statusCode,
                            }),
                        }, err);
                    }
                }
            });
            if (err) {
                throw err;
            }
            return result;
        }
        else {
            return await this._invoke(finalUrl, options);
        }
    }
}
// =============================================================================
// Factory Function
// =============================================================================
/**
 * Create a Server Side Fetch Adapter to allow the server to add headers and track outgoing requests.
 *
 * This adapter is used by all Agentuity service clients (KeyValueClient, QueueClient, etc.)
 * to communicate with Agentuity cloud services.
 *
 * @param config - Configuration containing headers and optional hooks
 * @param logger - Logger instance for debug output
 * @returns A FetchAdapter instance
 *
 * @example
 * ```typescript
 * import { createServerFetchAdapter, buildClientHeaders } from '@agentuity/adapter';
 * import { createMinimalLogger } from '@agentuity/core';
 *
 * const headers = buildClientHeaders({ apiKey: 'sk_xxx' });
 * const adapter = createServerFetchAdapter({ headers }, createMinimalLogger());
 *
 * // Use with a service
 * const service = new KeyValueStorageService('https://api.agentuity.sh', adapter);
 * ```
 */
export function createServerFetchAdapter(config, logger) {
    return new ServerFetchAdapter(config, logger);
}
//# sourceMappingURL=index.js.map