import fs from "node:fs/promises";
import os from "node:os";
import path from "pathe";
import { Lock } from "./util/lock.js";
import { singleFlight } from "./util/memoize.js";
var Path;
(function (Path) {
    Path.rootDir = path.join(os.homedir(), ".alchemy");
    Path.configFile = path.join(Path.rootDir, "config.json");
    Path.credentialsDir = path.join(Path.rootDir, "credentials");
    Path.credentialsFile = (provider, profile) => path.join(Path.credentialsDir, profile, `${provider}.json`);
})(Path || (Path = {}));
var Config;
(function (Config) {
    Config.read = async () => {
        const config = await FS.readJSON(Path.configFile);
        return config ?? { version: 1, profiles: {} };
    };
    Config.patch = async (updater) => {
        const config = await Config.read();
        const updated = updater(config);
        await FS.writeJSON(Path.configFile, updated);
    };
})(Config || (Config = {}));
export var Profile;
(function (Profile) {
    Profile.get = async (name) => {
        const config = await Config.read();
        return config.profiles[name];
    };
})(Profile || (Profile = {}));
export var Provider;
(function (Provider) {
    Provider.get = async (props) => {
        const profile = await Profile.get(props.profile);
        return profile?.[props.provider];
    };
    Provider.getWithCredentials = async (props) => {
        const [provider, credentials] = await Promise.all([
            Provider.get(props),
            Credentials.get(props),
        ]);
        const suffix = props.profile !== "default" ? ` -p ${props.profile}` : "";
        if (!provider) {
            throw new Error(`Provider "${props.provider}" not found in profile "${props.profile}". Please run \`alchemy configure${suffix}\` to configure this provider.`);
        }
        if (!credentials) {
            throw new Error(`Credentials not found for provider "${props.provider}" and profile "${props.profile}". Please run \`alchemy login ${props.provider}${suffix}\` to login to this provider.`);
        }
        return { provider, credentials };
    };
    Provider.set = async (props, provider) => {
        await Config.patch((config) => {
            config.profiles[props.profile] ??= {};
            config.profiles[props.profile][props.provider] = provider;
            return config;
        });
    };
    Provider.del = async (props) => {
        await Config.patch((config) => {
            if (config.profiles[props.profile]) {
                delete config.profiles[props.profile][props.provider];
            }
            if (Object.keys(config.profiles[props.profile]).length === 0) {
                delete config.profiles[props.profile];
            }
            return config;
        });
    };
})(Provider || (Provider = {}));
export var Credentials;
(function (Credentials) {
    /**
     * Gets the credentials file.
     * @param props The profile and provider of the credentials.
     */
    Credentials.get = async (props) => {
        return await FS.readJSON(Path.credentialsFile(props.provider, props.profile));
    };
    /**
     * Sets the credentials file.
     * @param props The profile and provider of the credentials.
     * @param credentials The credentials to set.
     */
    Credentials.set = async (props, credentials) => {
        await FS.writeJSON(Path.credentialsFile(props.provider, props.profile), credentials);
    };
    /**
     * Deletes the credentials file.
     * @param props The profile and provider of the credentials.
     */
    Credentials.del = async (props) => {
        await fs.unlink(Path.credentialsFile(props.provider, props.profile));
    };
    /**
     * Internal function to fetch and refresh credentials.
     * Uses a lock so the `refresh` function is called by only one process at a time.
     * If another process is refreshing the credentials, this function will wait for the
     * other process to release the lock before calling itself recursively to retrieve the updated credentials.
     */
    const getRefreshedInternal = async (props, refresh) => {
        // 1. Get credentials
        const credentials = await Credentials.get(props);
        if (!credentials) {
            throw new Error(`Credentials for provider "${props.provider}" not found in profile "${props.profile}"`);
        }
        // 2. Return credentials if they are not expired
        if (!Credentials.isOAuthExpired(credentials)) {
            return credentials;
        }
        // 3. Refresh credentials with lock for thread safety
        const lock = new Lock(`${props.provider}-${props.profile}`);
        const release = await lock.acquire();
        if (release) {
            try {
                const refreshed = await refresh(credentials);
                await Credentials.set(props, refreshed);
                return refreshed;
            }
            finally {
                await release();
            }
        }
        // 4. Another process has the lock, so wait for it to be released
        await lock.wait();
        // 5. Call this function again, bypassing the single flight mechanism to avoid a deadlock
        return await getRefreshedInternal(props, refresh);
    };
    /**
     * Fetches OAuth credentials for the given provider and profile, refreshing them if they are expired.
     * @param props The properties of the credentials.
     * @param refresh The function to refresh the credentials.
     * @returns The refreshed credentials.
     */
    Credentials.getRefreshed = singleFlight(
    // The locking mechanism works within the same process, but since the lock uses IO and polling,
    // wrapping it with `singleFlight` makes it more efficient.
    getRefreshedInternal, (props) => `${props.provider}-${props.profile}`);
    /**
     * Returns true if the given credentials are OAuth and expired.
     */
    Credentials.isOAuthExpired = (credentials, tolerance = 1000 * 10) => {
        return (credentials.type === "oauth" &&
            credentials.expires < Date.now() + tolerance);
    };
})(Credentials || (Credentials = {}));
var FS;
(function (FS) {
    FS.readJSON = async (path) => {
        try {
            const data = await fs.readFile(path, "utf-8");
            return JSON.parse(data);
        }
        catch {
            return undefined;
        }
    };
    FS.writeJSON = async (name, data) => {
        await fs.mkdir(path.dirname(name), { recursive: true });
        await fs.writeFile(name, JSON.stringify(data, null, 2), { mode: 0o600 });
    };
})(FS || (FS = {}));
//# sourceMappingURL=auth.js.map