// engine/spec-registry/client.ts — AC-2: Registry API Client
// HTTP client for the Planu spec registry. Uses node:https/http only (no external deps).
import * as https from 'node:https';
import * as http from 'node:http';
const DEFAULT_BASE_URL = 'https://registry.planu.dev/api/v1';
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 1000;
/** Build a URL with query parameters from filters. */
function buildSearchUrl(baseUrl, filters) {
    const url = new URL(`${baseUrl}/specs`);
    if (filters.query) {
        url.searchParams.set('q', filters.query);
    }
    if (filters.stack) {
        url.searchParams.set('stack', filters.stack);
    }
    if (filters.category) {
        url.searchParams.set('category', filters.category);
    }
    if (filters.sort) {
        url.searchParams.set('sort', filters.sort);
    }
    if (filters.page !== undefined) {
        url.searchParams.set('page', String(filters.page));
    }
    if (filters.limit !== undefined) {
        url.searchParams.set('limit', String(filters.limit));
    }
    if (filters.difficulty?.min !== undefined) {
        url.searchParams.set('difficulty_min', String(filters.difficulty.min));
    }
    if (filters.difficulty?.max !== undefined) {
        url.searchParams.set('difficulty_max', String(filters.difficulty.max));
    }
    return url.toString();
}
/** Choose http or https module based on URL protocol. */
function getTransport(url) {
    return url.startsWith('https') ? https : http;
}
/** Make an HTTP request with retry and exponential backoff. */
async function request(url, options, body) {
    let lastError;
    for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
        if (attempt > 0) {
            const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
            await new Promise((resolve) => setTimeout(resolve, delay));
        }
        try {
            const result = await executeRequest(url, options, body);
            // Don't retry client errors (4xx) except 429
            if (result.status >= 400 && result.status < 500 && result.status !== 429) {
                const message = tryParseErrorMessage(result.data);
                throw new Error(`Registry API error ${result.status}: ${message}`);
            }
            // Retry server errors (5xx) and 429
            if (result.status >= 500 || result.status === 429) {
                lastError = new Error(`Registry API error ${result.status}: server error`);
                continue;
            }
            return result;
        }
        catch (error) {
            if (error instanceof Error && error.message.startsWith('Registry API error 4')) {
                throw error; // Don't retry 4xx
            }
            lastError = error instanceof Error ? error : new Error(String(error));
        }
    }
    throw new Error(`Registry API request failed after ${MAX_RETRIES} attempts: ${lastError?.message ?? 'unknown error'}`);
}
/** Execute a single HTTP request. */
function executeRequest(url, options, body) {
    return new Promise((resolve, reject) => {
        const transport = getTransport(url);
        const req = transport.request(url, options, (res) => {
            const chunks = [];
            res.on('data', (chunk) => chunks.push(chunk));
            res.on('end', () => {
                resolve({
                    status: res.statusCode ?? 0,
                    data: Buffer.concat(chunks),
                });
            });
        });
        /* v8 ignore start -- network error/timeout handlers */
        req.on('error', (err) => {
            reject(new Error(`Network error: ${err.message}`));
        });
        req.setTimeout(30_000, () => {
            req.destroy();
            reject(new Error('Request timed out after 30s'));
        });
        /* v8 ignore stop */
        if (body) {
            req.write(body);
        }
        req.end();
    });
}
/** Try to parse a JSON error message from response body. */
/* v8 ignore start -- error parsing defensive branches */
function tryParseErrorMessage(data) {
    try {
        const parsed = JSON.parse(data.toString('utf-8'));
        return parsed.message ?? parsed.error ?? data.toString('utf-8').slice(0, 200);
    }
    catch {
        return data.toString('utf-8').slice(0, 200);
    }
}
/* v8 ignore stop */
/** Build common request headers. */
function buildHeaders(token, contentType) {
    const headers = {
        'User-Agent': 'planu-cli',
        Accept: 'application/json',
    };
    if (token) {
        headers.Authorization = `Bearer ${token}`;
    }
    if (contentType) {
        headers['Content-Type'] = contentType;
    }
    return headers;
}
/**
 * Create a registry API client instance.
 * @param token - Optional authentication token for write operations.
 * @returns Object with methods for interacting with the registry API.
 */
export function createRegistryClient(token) {
    const baseUrl = process.env.PLANU_REGISTRY_URL ?? DEFAULT_BASE_URL;
    const options = { token, baseUrl };
    return {
        /** Search specs in the registry with optional filters. */
        async searchSpecs(filters) {
            const url = buildSearchUrl(options.baseUrl ?? DEFAULT_BASE_URL, filters);
            const result = await request(url, {
                method: 'GET',
                headers: buildHeaders(options.token),
            });
            return JSON.parse(result.data.toString('utf-8'));
        },
        /** Get metadata for a specific spec by org and name. */
        async getSpec(org, name, version) {
            const versionSuffix = version ? `/${version}` : '';
            const url = `${options.baseUrl}/specs/${encodeURIComponent(org)}/${encodeURIComponent(name)}${versionSuffix}`;
            const result = await request(url, {
                method: 'GET',
                headers: buildHeaders(options.token),
            });
            return JSON.parse(result.data.toString('utf-8'));
        },
        /** Download a spec tarball from the registry. */
        async downloadSpec(org, name, version) {
            const url = `${options.baseUrl}/specs/${encodeURIComponent(org)}/${encodeURIComponent(name)}/${version}/download`;
            const result = await request(url, {
                method: 'GET',
                headers: buildHeaders(options.token),
            });
            return result.data;
        },
        /** Publish a spec tarball to the registry. Requires authentication. */
        async publishSpec(tarball, meta) {
            if (!options.token) {
                throw new Error('Authentication required to publish specs. Run planu login first.');
            }
            const payload = JSON.stringify({ tarball: tarball.toString('base64'), meta });
            const url = `${options.baseUrl}/specs`;
            const result = await request(url, {
                method: 'POST',
                headers: buildHeaders(options.token, 'application/json'),
            }, payload);
            return JSON.parse(result.data.toString('utf-8'));
        },
        /** Rate a spec in the registry. Requires authentication. */
        async rateSpec(org, name, rating) {
            if (!options.token) {
                throw new Error('Authentication required to rate specs. Run planu login first.');
            }
            if (rating < 1 || rating > 5 || !Number.isInteger(rating)) {
                throw new Error('Rating must be an integer between 1 and 5');
            }
            const url = `${options.baseUrl}/specs/${encodeURIComponent(org)}/${encodeURIComponent(name)}/rate`;
            const payload = JSON.stringify({ rating });
            await request(url, {
                method: 'POST',
                headers: buildHeaders(options.token, 'application/json'),
            }, payload);
        },
    };
}
//# sourceMappingURL=client.js.map