import { getEnv, getEnvironmentVariable } from "./env.js";
import * as fsUtils from "./fs.js";
import { acquireOAuthRefreshLock } from "./profile-lock.js";
export const DEFAULT_API_URL = "https://api.smith.langchain.com";
const OAUTH_CLIENT_ID = "langsmith-cli";
const TOKEN_REFRESH_LEEWAY_MS = 60_000;
const TOKEN_REFRESH_TIMEOUT_MS = 10_000;
function isBrowserLikeRuntime() {
    const env = getEnv();
    return env === "browser" || env === "webworker";
}
function getProfileConfigPath() {
    const explicitPath = getEnvironmentVariable("LANGSMITH_CONFIG_FILE");
    if (explicitPath) {
        return explicitPath;
    }
    const home = getEnvironmentVariable("HOME") ?? getEnvironmentVariable("USERPROFILE");
    if (!home) {
        return undefined;
    }
    return fsUtils.path.join(home, ".langsmith", "config.json");
}
function resolveProfileName(config) {
    const envProfile = getEnvironmentVariable("LANGSMITH_PROFILE");
    if (envProfile) {
        return envProfile;
    }
    if (config.current_profile) {
        return config.current_profile;
    }
    if (config.profiles?.default) {
        return "default";
    }
    return undefined;
}
function loadProfileState() {
    if (isBrowserLikeRuntime()) {
        return undefined;
    }
    const configPath = getProfileConfigPath();
    if (!configPath || !fsUtils.existsSync(configPath)) {
        return undefined;
    }
    try {
        const config = JSON.parse(fsUtils.readFileSync(configPath));
        const profileName = resolveProfileName(config);
        const profile = profileName ? config.profiles?.[profileName] : undefined;
        if (!profileName || !profile) {
            return undefined;
        }
        return { configPath, config, profileName, profile };
    }
    catch {
        return undefined;
    }
}
export function hasValue(value) {
    return value !== undefined && value !== null && value.trim() !== "";
}
function trimConfigValue(value) {
    return value?.trim().replace(/^["']|["']$/g, "");
}
function shouldRefreshProfileToken(profile) {
    const oauth = profile.oauth;
    if (!oauth?.refresh_token) {
        return false;
    }
    if (!oauth.access_token) {
        return true;
    }
    if (!oauth.expires_at) {
        return false;
    }
    const expiresAt = Date.parse(oauth.expires_at);
    if (Number.isNaN(expiresAt)) {
        return false;
    }
    return expiresAt <= Date.now() + TOKEN_REFRESH_LEEWAY_MS;
}
function normalizeConfigUrl(apiUrl) {
    let normalized = apiUrl;
    while (normalized.endsWith("/")) {
        normalized = normalized.slice(0, -1);
    }
    const apiV1Suffix = "/api/v1";
    return normalized.endsWith(apiV1Suffix)
        ? normalized.slice(0, -apiV1Suffix.length)
        : normalized;
}
function applyTokenResponse(profile, token) {
    profile.oauth ??= {};
    if (token.access_token) {
        profile.oauth.access_token = token.access_token;
    }
    if (token.refresh_token) {
        profile.oauth.refresh_token = token.refresh_token;
    }
    if (typeof token.expires_in === "number" && token.expires_in > 0) {
        profile.oauth.expires_at = new Date(Date.now() + token.expires_in * 1000).toISOString();
    }
}
function getAbortReason(signal) {
    return (signal.reason ??
        new Error("The operation was aborted."));
}
async function waitForAbortSignal(promise, signal) {
    if (!signal) {
        return promise;
    }
    if (signal.aborted) {
        throw getAbortReason(signal);
    }
    let cleanup;
    const abortPromise = new Promise((_, reject) => {
        const onAbort = () => {
            reject(getAbortReason(signal));
        };
        signal.addEventListener("abort", onAbort, { once: true });
        cleanup = () => {
            signal.removeEventListener("abort", onAbort);
        };
    });
    try {
        return await Promise.race([promise, abortPromise]);
    }
    finally {
        cleanup?.();
    }
}
export function loadProfileClientConfig() {
    const state = loadProfileState();
    const profile = state?.profile;
    if (!state || !profile) {
        return {};
    }
    const apiKey = trimConfigValue(profile.api_key);
    const oauthAccessToken = trimConfigValue(profile.oauth?.access_token);
    const oauthRefreshToken = trimConfigValue(profile.oauth?.refresh_token);
    return {
        apiUrl: profile.api_url,
        apiKey,
        workspaceId: profile.workspace_id,
        oauthAccessToken,
        oauthRefreshToken,
        profileAuth: apiKey || oauthAccessToken || oauthRefreshToken
            ? new ProfileAuth(state)
            : undefined,
    };
}
export class ProfileAuth {
    constructor(state) {
        Object.defineProperty(this, "state", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: state
        });
        Object.defineProperty(this, "refreshPromise", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "managedAuthorizationValue", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        this.rememberProfileAuthHeader(this.currentAuthHeader());
    }
    currentAuthHeader() {
        const header = currentAuthHeaderFromProfile(this.state.profile);
        this.rememberProfileAuthHeader(header);
        return header;
    }
    async getAuthHeader(fetchImplementation, signal) {
        if (shouldRefreshProfileToken(this.state.profile)) {
            if (!this.refreshPromise) {
                this.refreshPromise = this.refreshOAuthToken(fetchImplementation).finally(() => {
                    this.refreshPromise = undefined;
                });
            }
            await waitForAbortSignal(this.refreshPromise, signal);
        }
        const header = authHeaderFromProfile(this.state.profile);
        this.rememberProfileAuthHeader(header);
        return header;
    }
    isProfileAuthorizationHeader(value) {
        return value === this.managedAuthorizationValue;
    }
    reloadProfile() {
        try {
            const config = JSON.parse(fsUtils.readFileSync(this.state.configPath));
            const profile = config.profiles?.[this.state.profileName];
            if (!profile) {
                return undefined;
            }
            this.state.config = config;
            this.state.profile = profile;
            return profile;
        }
        catch {
            return undefined;
        }
    }
    async refreshOAuthToken(fetchImplementation) {
        const refreshToken = this.state.profile.oauth?.refresh_token;
        if (!refreshToken) {
            return;
        }
        const refreshApiUrl = trimConfigValue(this.state.profile.api_url) ?? DEFAULT_API_URL;
        const deadline = Date.now() + TOKEN_REFRESH_TIMEOUT_MS;
        let lock;
        try {
            lock = await acquireOAuthRefreshLock(this.state.configPath, deadline);
            const fresh = this.reloadProfile();
            if (fresh && !shouldRefreshProfileToken(this.state.profile)) {
                return;
            }
            const body = new URLSearchParams({
                grant_type: "refresh_token",
                client_id: OAUTH_CLIENT_ID,
                refresh_token: this.state.profile.oauth?.refresh_token ?? refreshToken,
            });
            const response = await fetchImplementation(`${normalizeConfigUrl(refreshApiUrl)}/oauth/token`, {
                method: "POST",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                body: body.toString(),
                signal: AbortSignal.timeout(Math.max(0, deadline - Date.now())),
            });
            if (!response.ok) {
                return;
            }
            const token = (await response.json());
            if (!token.access_token) {
                return;
            }
            applyTokenResponse(this.state.profile, token);
            this.state.config.profiles ??= {};
            this.state.config.profiles[this.state.profileName] = this.state.profile;
            await fsUtils.writeFileAtomic(this.state.configPath, `${JSON.stringify(this.state.config, null, 2)}\n`);
        }
        catch {
            return;
        }
        finally {
            await lock?.release();
        }
    }
    rememberProfileAuthHeader(header) {
        this.managedAuthorizationValue =
            header?.name === "Authorization" ? header.value : undefined;
    }
}
function currentAuthHeaderFromProfile(profile) {
    const oauthAccessToken = trimConfigValue(profile.oauth?.access_token);
    if (oauthAccessToken) {
        return { name: "Authorization", value: `Bearer ${oauthAccessToken}` };
    }
    if (trimConfigValue(profile.oauth?.refresh_token)) {
        return undefined;
    }
    return authHeaderFromProfile(profile);
}
function authHeaderFromProfile(profile) {
    const oauthAccessToken = trimConfigValue(profile.oauth?.access_token);
    if (oauthAccessToken) {
        return { name: "Authorization", value: `Bearer ${oauthAccessToken}` };
    }
    const apiKey = trimConfigValue(profile.api_key);
    if (apiKey) {
        return { name: "x-api-key", value: apiKey };
    }
    return undefined;
}
