import { ConfigurationError } from '@pellux/goodvibes-errors';
import { invokeObserver } from './observer/index.js';
import { AutoRefreshCoordinator, PermissionResolver, SessionManager, TokenStore, } from './client-auth/index.js';
// Re-export focused responsibility classes for consumers who prefer narrower,
// single-concern APIs over the combined GoodVibesAuthClient facade.
// OAuthClient is intentionally omitted from this client-side surface because it
// depends on Node runtime facilities. Daemon OAuth flows belong on the server
// side; clients receive acquired tokens via TokenStore.
export { PermissionResolver, SessionManager, TokenStore } from './client-auth/index.js';
function requireStorage(storage) {
    const resolved = storage ?? globalThis.localStorage;
    if (!resolved) {
        throw new ConfigurationError('Browser token storage is unavailable. Pass BrowserTokenStoreOptions.storage or use createMemoryTokenStore().');
    }
    return resolved;
}
/**
 * Create a simple in-memory token store.
 *
 * The token is held in a closure variable — it does not survive page
 * refreshes or process restarts. Suitable for server-side scripts and tests.
 *
 * @example
 * import { createMemoryTokenStore } from '@pellux/goodvibes-sdk';
 *
 * const store = createMemoryTokenStore('initial-token');
 * const sdk = createGoodVibesSdk({ baseUrl: '...', tokenStore: store });
 * await sdk.auth.clearToken(); // clears only in-memory
 */
export function createMemoryTokenStore(initialToken = null, initialExpiresAt) {
    let token = initialToken;
    let expiresAt = initialExpiresAt;
    return {
        async getToken() {
            return token;
        },
        async setToken(nextToken) {
            // Normalize: treat whitespace-only tokens as null.
            token = nextToken && nextToken.trim() ? nextToken.trim() : null;
            // When setToken is called directly (not via setTokenEntry), clear expiry
            // to avoid stale expiry data attached to a manually-set token.
            expiresAt = undefined;
        },
        async clearToken() {
            token = null;
            expiresAt = undefined;
        },
        async getTokenEntry() {
            return expiresAt !== undefined ? { token, expiresAt } : { token };
        },
        async setTokenEntry(nextToken, nextExpiresAt) {
            token = nextToken;
            expiresAt = nextExpiresAt;
        },
    };
}
/**
 * Create a token store backed by `localStorage` (or a custom `Storage`).
 *
 * The token is persisted across page refreshes under the key
 * `'goodvibes.token'` (overridable via `options.key`).
 * Pass `options.storage` to use `sessionStorage` or a custom adapter.
 *
 * @example
 * import { createBrowserTokenStore, createBrowserGoodVibesSdk } from '@pellux/goodvibes-sdk/browser';
 *
 * const tokenStore = createBrowserTokenStore({ storage: sessionStorage });
 * const sdk = createBrowserGoodVibesSdk({ tokenStore });
 * await sdk.auth.login({ username: 'alice', password: 's3cr3t' });
 * // token is now stored in sessionStorage
 */
export function createBrowserTokenStore(options = {}) {
    const storage = requireStorage(options.storage);
    const key = options.key?.trim() || 'goodvibes.token';
    const expiresAtKey = `${key}.expiresAt`;
    return {
        async getToken() {
            const value = storage.getItem(key);
            return value && value.trim() ? value : null;
        },
        async setToken(token) {
            const normalized = token && token.trim() ? token.trim() : null;
            if (!normalized) {
                storage.removeItem(key);
                storage.removeItem(expiresAtKey);
                return;
            }
            storage.setItem(key, normalized);
            // Clear expiry when set via setToken (no expiry info provided).
            storage.removeItem(expiresAtKey);
        },
        async clearToken() {
            storage.removeItem(key);
            storage.removeItem(expiresAtKey);
        },
        async getTokenEntry() {
            const value = storage.getItem(key);
            const token = value && value.trim() ? value : null;
            const expiresAtStr = storage.getItem(expiresAtKey);
            const expiresAtRaw = expiresAtStr ? Number(expiresAtStr) : NaN;
            const finalExpiresAt = Number.isFinite(expiresAtRaw) && expiresAtRaw > 0
                ? expiresAtRaw
                : undefined;
            return finalExpiresAt !== undefined ? { token, expiresAt: finalExpiresAt } : { token };
        },
        async setTokenEntry(token, expiresAt) {
            if (!token) {
                storage.removeItem(key);
                storage.removeItem(expiresAtKey);
                return;
            }
            storage.setItem(key, token);
            if (expiresAt !== undefined) {
                storage.setItem(expiresAtKey, String(expiresAt));
            }
            else {
                storage.removeItem(expiresAtKey);
            }
        },
    };
}
async function readToken(tokenStore, getAuthToken) {
    if (tokenStore) {
        return await tokenStore.getToken();
    }
    if (getAuthToken) {
        return (await getAuthToken()) ?? null;
    }
    return null;
}
function assertWritableTokenStore(tokenStore) {
    if (!tokenStore) {
        throw new ConfigurationError('This SDK instance uses a read-only auth token resolver. Pass tokenStore to enable token persistence and mutation.');
    }
    return tokenStore;
}
/**
 * Create the auth client attached to an SDK instance.
 *
 * Normally called internally by `createGoodVibesSdk`. Access the result via
 * `sdk.auth`.
 *
 * @example
 * import { createGoodVibesSdk, createBrowserTokenStore } from '@pellux/goodvibes-sdk';
 *
 * const sdk = createGoodVibesSdk({
 *   baseUrl: 'https://daemon.example.com',
 *   tokenStore: createBrowserTokenStore(),
 * });
 *
 * // Login and persist the token automatically:
 * const { token } = await sdk.auth.login({ username: 'alice', password: 's3cr3t' });
 * console.log('logged in, token stored:', token.slice(0, 8) + '...');
 *
 * // Later: clear the session
 * await sdk.auth.clearToken();
 */
export function createGoodVibesAuthClient(operator, tokenStore, getAuthToken, observer, autoRefreshOptions, 
/**
 * Optional pre-built `AutoRefreshCoordinator`. When provided, it is reused
 * directly (no new coordinator is constructed). This allows `createGoodVibesSdk`
 * to share a single coordinator instance between the transport middleware and
 * the auth client, avoiding duplicate refresh calls.
 */
existingCoordinator) {
    // Construct the split-class instances that own each concern.
    // The facade delegates to these rather than duplicating logic.
    const ts = tokenStore ? new TokenStore(tokenStore) : null;
    const sm = new SessionManager(operator, ts, observer);
    // Use the provided coordinator if available; otherwise build one.
    // `existingCoordinator !== undefined` handles the null case: null means
    // "caller explicitly decided no coordinator"; undefined means "not provided, build one".
    const coordinator = existingCoordinator !== undefined
        ? existingCoordinator
        : (() => {
            const autoRefresh = autoRefreshOptions?.autoRefresh ?? true;
            const refreshLeewayMs = autoRefreshOptions?.refreshLeewayMs ?? 60_000;
            return tokenStore && autoRefresh
                ? new AutoRefreshCoordinator({
                    tokenStore,
                    autoRefresh,
                    refreshLeewayMs,
                    ...(autoRefreshOptions?.refresh !== undefined ? { refresh: autoRefreshOptions.refresh } : {}),
                    observer,
                })
                : null;
        })();
    return {
        get writable() {
            return sm.writable;
        },
        get tokenStore() {
            return ts;
        },
        get sessionManager() {
            return sm;
        },
        permissionResolver(snapshot) {
            return new PermissionResolver(snapshot);
        },
        async current() {
            if (coordinator) {
                await coordinator.ensureFreshToken();
                return coordinator.withRetryOn401(() => sm.current());
            }
            return sm.current();
        },
        async login(input, options = {}) {
            // Capture prior auth state BEFORE the login mutation so the observer's
            // `from` field reflects the actual pre-login state (anonymous vs token
            // refresh), not the post-login state.
            const priorToken = await ts?.getToken();
            const result = await sm.login(input, options);
            // Notify observer of the auth state transition. Observer errors are
            // isolated and reported by invokeObserver.
            invokeObserver(() => observer?.onAuthTransition?.({
                from: priorToken ? 'token' : 'anonymous',
                to: 'token',
                reason: 'login',
            }), { label: 'onAuthTransition' });
            return result;
        },
        async getToken() {
            if (ts) {
                return ts.getToken();
            }
            return await readToken(null, getAuthToken);
        },
        async setToken(token) {
            const priorToken = await ts?.getToken();
            await assertWritableTokenStore(ts).setToken(token);
            // Notify observer of the token state transition.
            invokeObserver(() => observer?.onAuthTransition?.({
                from: priorToken ? 'token' : 'anonymous',
                to: token ? 'token' : 'anonymous',
                reason: token ? 'refresh' : 'logout',
            }), { label: 'onAuthTransition' });
        },
        async clearToken() {
            // assertWritableTokenStore first: throws ConfigurationError when no tokenStore,
            // regardless of current token value (read-only resolvers have no store at all).
            const store = assertWritableTokenStore(ts);
            const currentToken = await ts?.getToken();
            await store.clearToken();
            // Notify observer of the auth state transition. Always fire — even for
            // anonymous→anonymous — so callers that explicitly call clearToken receive
            // the expected transition event.
            invokeObserver(() => observer?.onAuthTransition?.({
                from: currentToken ? 'token' : 'anonymous',
                to: 'anonymous',
                reason: 'logout',
            }), { label: 'onAuthTransition' });
        },
    };
}
