import { ConfigurationError, ContractError } from '@pellux/goodvibes-errors';
import { createHttpTransport, openRawServerSentEventStream, } from './transport-http.js';
import { createGoodVibesAuthClient, } from './auth.js';
const SHARED_BROWSER_ROUTES = {
    'accounts.snapshot': { method: 'GET', path: '/api/accounts' },
    'control.auth.current': { method: 'GET', path: '/api/control-plane/auth' },
    'control.auth.login': { method: 'POST', path: '/login' },
    'control.snapshot': { method: 'GET', path: '/api/control-plane' },
    'control.status': { method: 'GET', path: '/status' },
    'providers.get': { method: 'GET', path: '/api/providers/{providerId}' },
    'providers.list': { method: 'GET', path: '/api/providers' },
    'providers.usage.get': { method: 'GET', path: '/api/providers/{providerId}/usage' },
    'sessions.create': { method: 'POST', path: '/api/sessions' },
    'sessions.followUp': { method: 'POST', path: '/api/sessions/{sessionId}/follow-up' },
    'sessions.get': { method: 'GET', path: '/api/sessions/{sessionId}' },
    'sessions.inputs.cancel': { method: 'POST', path: '/api/sessions/{sessionId}/inputs/{inputId}/cancel' },
    'sessions.inputs.list': { method: 'GET', path: '/api/sessions/{sessionId}/inputs' },
    'sessions.list': { method: 'GET', path: '/api/sessions' },
    'sessions.messages.create': { method: 'POST', path: '/api/sessions/{sessionId}/messages' },
    'sessions.messages.list': { method: 'GET', path: '/api/sessions/{sessionId}/messages' },
    'sessions.steer': { method: 'POST', path: '/api/sessions/{sessionId}/steer' },
};
export function resolveBrowserBaseUrl(baseUrl) {
    const explicit = baseUrl?.trim();
    if (explicit)
        return explicit;
    if (typeof globalThis.location?.origin === 'string' && globalThis.location.origin.trim()) {
        return globalThis.location.origin.trim();
    }
    throw new ConfigurationError('Browser baseUrl is required when location.origin is unavailable.');
}
function resolveFetch(fetchImpl) {
    const resolved = fetchImpl ?? globalThis.fetch;
    if (typeof resolved !== 'function') {
        throw new ConfigurationError('Fetch implementation is required. Pass options.fetch or use a browser/runtime that provides global fetch.');
    }
    return resolved;
}
function buildRouteError(methodId) {
    return new ContractError(`Operator method "${methodId}" is not available from this scoped browser SDK entrypoint. Import the matching scoped entrypoint instead of the full browser client.`);
}
function createScopedOperatorClient(routes, options) {
    const baseUrl = resolveBrowserBaseUrl(options.baseUrl);
    const fetchImpl = resolveFetch(options.fetch);
    const transport = createHttpTransport({
        baseUrl,
        authToken: options.authToken ?? null,
        getAuthToken: options.tokenStore
            ? () => options.tokenStore.getToken()
            : options.getAuthToken,
        fetch: fetchImpl,
        headers: options.headers,
        getHeaders: options.getHeaders,
        retry: options.retry,
        middleware: options.middleware,
        observer: options.observer,
    });
    const operator = {
        async invoke(methodId, input, invokeOptions = {}) {
            const route = routes[methodId];
            if (!route)
                throw buildRouteError(methodId);
            const resolved = transport.resolveContractRequest(route.method, route.path, input && typeof input === 'object' && !Array.isArray(input)
                ? input
                : {});
            return await transport.requestJson(resolved.url, {
                method: resolved.method,
                body: resolved.body,
                headers: invokeOptions.headers,
                signal: invokeOptions.signal,
                methodId,
                idempotent: false,
            });
        },
    };
    return {
        operator,
        requestJson: (pathOrUrl, requestOptions) => transport.requestJson(pathOrUrl, requestOptions),
        openEventStream: async ({ pathOrUrl, handlers, options: streamOptions = {} }) => {
            const url = pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://')
                ? pathOrUrl
                : new URL(pathOrUrl, baseUrl).toString();
            return await openRawServerSentEventStream(fetchImpl, url, handlers, {
                signal: streamOptions.signal,
                headers: streamOptions.headers,
                reconnect: streamOptions.reconnect,
                getAuthToken: () => transport.getAuthToken(),
            });
        },
        getAuthToken: () => transport.getAuthToken(),
        use: (middleware) => transport.use(middleware),
    };
}
function createAuthOperatorShim(operator) {
    return {
        control: {
            auth: {
                current: () => operator.invoke('control.auth.current', {}),
                login: (input) => operator.invoke('control.auth.login', input),
            },
        },
    };
}
function createFeed(subscribe) {
    return {
        on(type, listener) {
            return subscribe(type, (envelope) => {
                listener(envelope.payload);
            });
        },
        onEnvelope(type, listener) {
            return subscribe(type, listener);
        },
    };
}
function normalizeEnvelope(value) {
    if (!value || typeof value !== 'object')
        return null;
    const envelope = value;
    const payload = envelope.payload;
    if (!payload || typeof payload !== 'object')
        return null;
    const payloadType = payload.type;
    if (typeof payloadType !== 'string')
        return null;
    return {
        type: typeof envelope.type === 'string' ? envelope.type : payloadType,
        ts: typeof envelope.ts === 'number'
            ? envelope.ts
            : typeof envelope.timestamp === 'number'
                ? envelope.timestamp
                : Date.now(),
        traceId: typeof envelope.traceId === 'string' ? envelope.traceId : undefined,
        sessionId: typeof envelope.sessionId === 'string' ? envelope.sessionId : undefined,
        source: typeof envelope.source === 'string' ? envelope.source : undefined,
        payload: payload,
    };
}
function buildEventUrl(baseUrl, domain) {
    const url = new URL('/api/control-plane/events', baseUrl);
    url.searchParams.set('domains', domain);
    return url.toString();
}
function createScopedRealtime(domains, options, getAuthToken) {
    return {
        viaSse() {
            const baseUrl = resolveBrowserBaseUrl(options.baseUrl);
            const fetchImpl = resolveFetch(options.fetch);
            const feeds = new Map();
            const activeByDomain = new Map();
            const ensureDomain = (domain) => {
                let active = activeByDomain.get(domain);
                if (active)
                    return active;
                active = { listenersByType: new Map(), closed: false };
                activeByDomain.set(domain, active);
                void openRawServerSentEventStream(fetchImpl, buildEventUrl(baseUrl, domain), {
                    onEvent: (eventName, payload) => {
                        if (eventName !== domain)
                            return;
                        const envelope = normalizeEnvelope(payload);
                        if (!envelope)
                            return;
                        for (const listener of [...(active.listenersByType.get(envelope.payload.type) ?? [])]) {
                            listener(envelope);
                        }
                    },
                    onError: (error) => options.realtime?.onError?.(error),
                }, {
                    reconnect: options.realtime?.sseReconnect,
                    getAuthToken,
                }).then((disconnect) => {
                    if (active.closed || activeByDomain.get(domain) !== active) {
                        disconnect();
                        return;
                    }
                    active.disconnect = disconnect;
                }).catch((error) => {
                    options.realtime?.onError?.(error);
                });
                return active;
            };
            const getFeed = (domain) => {
                let feed = feeds.get(domain);
                if (feed)
                    return feed;
                feed = createFeed((type, listener) => {
                    const active = ensureDomain(domain);
                    const listeners = active.listenersByType.get(type) ?? new Set();
                    listeners.add(listener);
                    active.listenersByType.set(type, listeners);
                    return () => {
                        listeners.delete(listener);
                        if (listeners.size === 0)
                            active.listenersByType.delete(type);
                        if (active.listenersByType.size === 0) {
                            active.closed = true;
                            active.disconnect?.();
                            activeByDomain.delete(domain);
                        }
                    };
                });
                feeds.set(domain, feed);
                return feed;
            };
            return Object.freeze({
                domains,
                domain: getFeed,
                ...Object.fromEntries(domains.map((domain) => [domain, getFeed(domain)])),
            });
        },
    };
}
export function forScopedBrowserSession(events, sessionId) {
    const feeds = new Map();
    const getFeed = (domain) => {
        let feed = feeds.get(domain);
        if (feed)
            return feed;
        const baseFeed = events.domain(domain);
        feed = {
            on(type, listener) {
                return baseFeed.onEnvelope(type, (envelope) => {
                    if (envelope.sessionId !== sessionId)
                        return;
                    listener(envelope.payload);
                });
            },
            onEnvelope(type, listener) {
                return baseFeed.onEnvelope(type, (envelope) => {
                    if (envelope.sessionId !== sessionId)
                        return;
                    listener(envelope);
                });
            },
        };
        feeds.set(domain, feed);
        return feed;
    };
    return Object.freeze({
        domains: events.domains,
        domain: getFeed,
        ...Object.fromEntries(events.domains.map((domain) => [domain, getFeed(domain)])),
    });
}
export function createScopedBrowserSdk(routes, domains, options = {}) {
    const scoped = createScopedOperatorClient(routes, options);
    const auth = createGoodVibesAuthClient(createAuthOperatorShim(scoped.operator), options.tokenStore ?? null, options.getAuthToken, options.observer, undefined, null);
    return {
        operator: scoped.operator,
        auth,
        realtime: createScopedRealtime(domains, options, scoped.getAuthToken),
        streams: {
            open: (pathOrUrl, handlers, streamOptions) => scoped.openEventStream({ pathOrUrl, handlers, options: streamOptions }),
        },
        use: scoped.use,
    };
}
export { SHARED_BROWSER_ROUTES };
